Skip to content

Commit

Permalink
Merge pull request #1727 from OneSignal/user-model/sdk-migration
Browse files Browse the repository at this point in the history
[User Model] Support migrating user from SDK 4.x to SDK 5.x
  • Loading branch information
brismithers authored and jinliu9508 committed Jan 31, 2024
2 parents 2e571f8 + 9365747 commit 8faa246
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,17 @@ object PreferencePlayerPurchasesKeys {
}

object PreferenceOneSignalKeys {
// Legacy
/**
* (String) The legacy player ID from SDKs prior to 5.
*/
const val PREFS_LEGACY_PLAYER_ID = "GT_PLAYER_ID"

/**
* (String) The legacy player sync values from SDKS prior to 5.
*/
const val PREFS_LEGACY_USER_SYNCVALUES = "ONESIGNAL_USERSTATE_SYNCVALYES_CURRENT_STATE"

// Location
/**
* (Long) The last time the device location was captured, in Unix time milliseconds.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.onesignal.common.IDManager
import com.onesignal.common.OneSignalUtils
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.common.modules.IModule
import com.onesignal.common.safeString
import com.onesignal.common.services.IServiceProvider
import com.onesignal.common.services.ServiceBuilder
import com.onesignal.common.services.ServiceProvider
Expand All @@ -16,6 +17,9 @@ import com.onesignal.core.internal.application.impl.ApplicationService
import com.onesignal.core.internal.config.ConfigModel
import com.onesignal.core.internal.config.ConfigModelStore
import com.onesignal.core.internal.operations.IOperationRepo
import com.onesignal.core.internal.preferences.IPreferencesService
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
import com.onesignal.core.internal.preferences.PreferenceStores
import com.onesignal.core.internal.startup.StartupService
import com.onesignal.debug.IDebugManager
import com.onesignal.debug.LogLevel
Expand All @@ -33,6 +37,7 @@ import com.onesignal.user.UserModule
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.identity.IdentityModel
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation
import com.onesignal.user.internal.operations.LoginUserOperation
import com.onesignal.user.internal.operations.RefreshUserOperation
import com.onesignal.user.internal.operations.TransferSubscriptionOperation
Expand Down Expand Up @@ -87,6 +92,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
private var _propertiesModelStore: PropertiesModelStore? = null
private var _subscriptionModelStore: SubscriptionModelStore? = null
private var _startupService: StartupService? = null
private var _preferencesService: IPreferencesService? = null

// Other State
private val _services: ServiceProvider
Expand Down Expand Up @@ -182,14 +188,48 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
_propertiesModelStore = _services.getService()
_identityModelStore = _services.getService()
_subscriptionModelStore = _services.getService()
_preferencesService = _services.getService()

// Instantiate and call the IStartableServices
_startupService = _services.getService()
_startupService!!.bootstrap()

if (forceCreateUser || !_identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
createAndSwitchToNewUser()
_operationRepo!!.enqueue(LoginUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, _identityModelStore!!.model.externalId))
val legacyPlayerId = _preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID)
if(legacyPlayerId == null) {
Logging.debug("initWithContext: creating new device-scoped user")
createAndSwitchToNewUser()
_operationRepo!!.enqueue(LoginUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, _identityModelStore!!.model.externalId))
}
else {
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")

// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
// based on the subscription ID we do have.
val legacyUserSyncString = _preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES)
var suppressBackendOperation = false

if(legacyUserSyncString != null) {
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")

val pushSubscriptionModel = SubscriptionModel()
pushSubscriptionModel.id = legacyPlayerId
pushSubscriptionModel.type = SubscriptionType.PUSH
pushSubscriptionModel.optedIn = notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
pushSubscriptionModel.address = legacyUserSyncJSON.safeString("identifier") ?: ""
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes) ?: SubscriptionStatus.NO_PERMISSION
_configModel!!.pushSubscriptionId = legacyPlayerId
_subscriptionModelStore!!.add(pushSubscriptionModel, ModelChangeTags.NO_PROPOGATE)
suppressBackendOperation = true
}

createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)

_operationRepo!!.enqueue(LoginUserFromSubscriptionOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, legacyPlayerId))
_preferencesService!!.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID, null)
}
} else {
Logging.debug("initWithContext: using cached user ${_identityModelStore!!.model.onesignalId}")
_operationRepo!!.enqueue(RefreshUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId))
Expand Down Expand Up @@ -299,7 +339,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
}
}

private fun createAndSwitchToNewUser(modify: ((identityModel: IdentityModel, propertiesModel: PropertiesModel) -> Unit)? = null) {
private fun createAndSwitchToNewUser(suppressBackendOperation: Boolean = false, modify: ((identityModel: IdentityModel, propertiesModel: PropertiesModel) -> Unit)? = null) {
Logging.debug("createAndSwitchToNewUser()")

// create a new identity and properties model locally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.onesignal.user.internal.builduser.IRebuildUserService
import com.onesignal.user.internal.builduser.impl.RebuildUserService
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
import com.onesignal.user.internal.operations.impl.executors.RefreshUserOperationExecutor
import com.onesignal.user.internal.operations.impl.executors.SubscriptionOperationExecutor
Expand Down Expand Up @@ -57,6 +58,7 @@ internal class UserModule : IModule {
.provides<UpdateUserOperationExecutor>()
.provides<IOperationExecutor>()
builder.register<LoginUserOperationExecutor>().provides<IOperationExecutor>()
builder.register<LoginUserFromSubscriptionOperationExecutor>().provides<IOperationExecutor>()
builder.register<RefreshUserOperationExecutor>().provides<IOperationExecutor>()
builder.register<UserManager>().provides<IUserManager>()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,14 @@ interface ISubscriptionBackendService {
* @param aliasValue The identifier within the [aliasLabel] that identifies the user to transfer under.
*/
suspend fun transferSubscription(appId: String, subscriptionId: String, aliasLabel: String, aliasValue: String)

/**
* Given an existing subscription, retrieve all identities associated to it.
*
* @param appId The ID of the OneSignal application this subscription exists under.
* @param subscriptionId The ID of the subscription to retrieve identities for.
*
* @return The identities associated to the subscription.
*/
suspend fun getIdentityFromSubscription(appId: String, subscriptionId: String) : Map<String, String>
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.onesignal.user.internal.backend.impl

import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.safeJSONObject
import com.onesignal.common.toMap
import com.onesignal.core.internal.http.IHttpClient
import com.onesignal.user.internal.backend.ISubscriptionBackendService
import com.onesignal.user.internal.backend.SubscriptionObject
Expand Down Expand Up @@ -60,4 +61,16 @@ internal class SubscriptionBackendService(
throw BackendException(response.statusCode, response.payload)
}
}

override suspend fun getIdentityFromSubscription(appId: String, subscriptionId: String): Map<String, String> {
val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity")

if (!response.isSuccess) {
throw BackendException(response.statusCode, response.payload)
}

val responseJSON = JSONObject(response.payload!!)
val identityJSON = responseJSON.safeJSONObject("identity")
return identityJSON?.toMap()?.mapValues { it.value.toString() } ?: mapOf()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.onesignal.user.internal.operations

import com.onesignal.core.internal.operations.GroupComparisonType
import com.onesignal.core.internal.operations.Operation
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor

/**
* An [Operation] to login the user with the [subscriptionId] provided.
*/
class LoginUserFromSubscriptionOperation() : Operation(LoginUserFromSubscriptionOperationExecutor.LOGIN_USER_FROM_SUBSCRIPTION_USER) {
/**
* The application ID the user will exist/be logged in under.
*/
var appId: String
get() = getStringProperty(::appId.name)
private set(value) { setStringProperty(::appId.name, value) }

/**
* The local OneSignal ID this user was initially logged in under. The user models with this ID
* will have its ID updated with the backend-generated ID post-create.
*/
var onesignalId: String
get() = getStringProperty(::onesignalId.name)
private set(value) { setStringProperty(::onesignalId.name, value) }

/**
* The optional external ID of this newly logged-in user. Must be unique for the [appId].
*/
var subscriptionId: String
get() = getStringProperty(::subscriptionId.name)
private set(value) { setStringProperty(::subscriptionId.name, value) }

override val createComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Login"
override val modifyComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Login"
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
override val canStartExecute: Boolean = true

constructor(appId: String, onesignalId: String, playerId: String) : this() {
this.appId = appId
this.onesignalId = onesignalId
this.subscriptionId = playerId
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
override val createComparisonKey: String get() = "$appId.User.$onesignalId"
override val modifyComparisonKey: String = ""
override val groupComparisonType: GroupComparisonType = GroupComparisonType.CREATE
override val canStartExecute: Boolean = existingOnesignalId == null || !IDManager.isLocalId(existingOnesignalId!!)
override val canStartExecute: Boolean get() = existingOnesignalId == null || !IDManager.isLocalId(existingOnesignalId!!)

constructor(appId: String, onesignalId: String, externalId: String?, existingOneSignalId: String? = null) : this() {
this.appId = appId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.onesignal.user.internal.operations.impl.executors

import com.onesignal.common.NetworkUtils
import com.onesignal.common.exceptions.BackendException
import com.onesignal.common.modeling.ModelChangeTags
import com.onesignal.core.internal.operations.ExecutionResponse
import com.onesignal.core.internal.operations.ExecutionResult
import com.onesignal.core.internal.operations.IOperationExecutor
import com.onesignal.core.internal.operations.Operation
import com.onesignal.debug.internal.logging.Logging
import com.onesignal.user.internal.backend.ISubscriptionBackendService
import com.onesignal.user.internal.backend.IdentityConstants
import com.onesignal.user.internal.identity.IdentityModelStore
import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation
import com.onesignal.user.internal.operations.RefreshUserOperation
import com.onesignal.user.internal.properties.PropertiesModel
import com.onesignal.user.internal.properties.PropertiesModelStore

internal class LoginUserFromSubscriptionOperationExecutor(
private val _subscriptionBackend: ISubscriptionBackendService,
private val _identityModelStore: IdentityModelStore,
private val _propertiesModelStore: PropertiesModelStore
) : IOperationExecutor {

override val operations: List<String>
get() = listOf(LOGIN_USER_FROM_SUBSCRIPTION_USER)

override suspend fun execute(operations: List<Operation>): ExecutionResponse {
Logging.debug("LoginUserFromSubscriptionOperationExecutor(operation: $operations)")

val startingOp = operations.first()

if (startingOp is LoginUserFromSubscriptionOperation) {
return loginUser(startingOp)
}

throw Exception("Unrecognized operation: $startingOp")
}

private suspend fun loginUser(loginUserOp: LoginUserFromSubscriptionOperation): ExecutionResponse {
try {
val identities = _subscriptionBackend.getIdentityFromSubscription(
loginUserOp.appId,
loginUserOp.subscriptionId
)
val backendOneSignalId = identities.getOrDefault(IdentityConstants.ONESIGNAL_ID, null)

if (backendOneSignalId == null) {
Logging.warn("Subscription ${loginUserOp.subscriptionId} has no ${IdentityConstants.ONESIGNAL_ID}!")
return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}

val idTranslations = mutableMapOf<String, String>()
// Add the "local-to-backend" ID translation to the IdentifierTranslator for any operations that were
// *not* executed but still reference the locally-generated IDs.
// Update the current identity, property, and subscription models from a local ID to the backend ID
idTranslations[loginUserOp.onesignalId] = backendOneSignalId

val identityModel = _identityModelStore.model
val propertiesModel = _propertiesModelStore.model

if (identityModel.onesignalId == loginUserOp.onesignalId) {
identityModel.setStringProperty(IdentityConstants.ONESIGNAL_ID, backendOneSignalId, ModelChangeTags.HYDRATE)
}

if (propertiesModel.onesignalId == loginUserOp.onesignalId) {
propertiesModel.setStringProperty(PropertiesModel::onesignalId.name, backendOneSignalId, ModelChangeTags.HYDRATE)
}

return ExecutionResponse(ExecutionResult.SUCCESS, idTranslations,listOf(RefreshUserOperation(loginUserOp.appId, backendOneSignalId)))
} catch (ex: BackendException) {
val responseType = NetworkUtils.getResponseStatusType(ex.statusCode)

return when (responseType) {
NetworkUtils.ResponseStatusType.RETRYABLE ->
ExecutionResponse(ExecutionResult.FAIL_RETRY)
NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED)
else ->
ExecutionResponse(ExecutionResult.FAIL_NORETRY)
}
}
}

companion object {
const val LOGIN_USER_FROM_SUBSCRIPTION_USER = "login-user-from-subscription"
}
}

0 comments on commit 8faa246

Please sign in to comment.