Skip to content

Commit 8faa246

Browse files
brismithersjinliu9508
authored andcommitted
Merge pull request #1727 from OneSignal/user-model/sdk-migration
[User Model] Support migrating user from SDK 4.x to SDK 5.x
2 parents 2e571f8 + 9365747 commit 8faa246

File tree

8 files changed

+211
-4
lines changed

8 files changed

+211
-4
lines changed

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/core/internal/preferences/IPreferencesService.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,17 @@ object PreferencePlayerPurchasesKeys {
134134
}
135135

136136
object PreferenceOneSignalKeys {
137+
// Legacy
138+
/**
139+
* (String) The legacy player ID from SDKs prior to 5.
140+
*/
141+
const val PREFS_LEGACY_PLAYER_ID = "GT_PLAYER_ID"
142+
143+
/**
144+
* (String) The legacy player sync values from SDKS prior to 5.
145+
*/
146+
const val PREFS_LEGACY_USER_SYNCVALUES = "ONESIGNAL_USERSTATE_SYNCVALYES_CURRENT_STATE"
147+
137148
// Location
138149
/**
139150
* (Long) The last time the device location was captured, in Unix time milliseconds.

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/internal/OneSignalImp.kt

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.onesignal.common.IDManager
66
import com.onesignal.common.OneSignalUtils
77
import com.onesignal.common.modeling.ModelChangeTags
88
import com.onesignal.common.modules.IModule
9+
import com.onesignal.common.safeString
910
import com.onesignal.common.services.IServiceProvider
1011
import com.onesignal.common.services.ServiceBuilder
1112
import com.onesignal.common.services.ServiceProvider
@@ -16,6 +17,9 @@ import com.onesignal.core.internal.application.impl.ApplicationService
1617
import com.onesignal.core.internal.config.ConfigModel
1718
import com.onesignal.core.internal.config.ConfigModelStore
1819
import com.onesignal.core.internal.operations.IOperationRepo
20+
import com.onesignal.core.internal.preferences.IPreferencesService
21+
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
22+
import com.onesignal.core.internal.preferences.PreferenceStores
1923
import com.onesignal.core.internal.startup.StartupService
2024
import com.onesignal.debug.IDebugManager
2125
import com.onesignal.debug.LogLevel
@@ -33,6 +37,7 @@ import com.onesignal.user.UserModule
3337
import com.onesignal.user.internal.backend.IdentityConstants
3438
import com.onesignal.user.internal.identity.IdentityModel
3539
import com.onesignal.user.internal.identity.IdentityModelStore
40+
import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation
3641
import com.onesignal.user.internal.operations.LoginUserOperation
3742
import com.onesignal.user.internal.operations.RefreshUserOperation
3843
import com.onesignal.user.internal.operations.TransferSubscriptionOperation
@@ -87,6 +92,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
8792
private var _propertiesModelStore: PropertiesModelStore? = null
8893
private var _subscriptionModelStore: SubscriptionModelStore? = null
8994
private var _startupService: StartupService? = null
95+
private var _preferencesService: IPreferencesService? = null
9096

9197
// Other State
9298
private val _services: ServiceProvider
@@ -182,14 +188,48 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
182188
_propertiesModelStore = _services.getService()
183189
_identityModelStore = _services.getService()
184190
_subscriptionModelStore = _services.getService()
191+
_preferencesService = _services.getService()
185192

186193
// Instantiate and call the IStartableServices
187194
_startupService = _services.getService()
188195
_startupService!!.bootstrap()
189196

190197
if (forceCreateUser || !_identityModelStore!!.model.hasProperty(IdentityConstants.ONESIGNAL_ID)) {
191-
createAndSwitchToNewUser()
192-
_operationRepo!!.enqueue(LoginUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, _identityModelStore!!.model.externalId))
198+
val legacyPlayerId = _preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID)
199+
if(legacyPlayerId == null) {
200+
Logging.debug("initWithContext: creating new device-scoped user")
201+
createAndSwitchToNewUser()
202+
_operationRepo!!.enqueue(LoginUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, _identityModelStore!!.model.externalId))
203+
}
204+
else {
205+
Logging.debug("initWithContext: creating user linked to subscription $legacyPlayerId")
206+
207+
// Converting a 4.x SDK to the 5.x SDK. We pull the legacy user sync values to create the subscription model, then enqueue
208+
// a specialized `LoginUserFromSubscriptionOperation`, which will drive fetching/refreshing of the local user
209+
// based on the subscription ID we do have.
210+
val legacyUserSyncString = _preferencesService!!.getString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_USER_SYNCVALUES)
211+
var suppressBackendOperation = false
212+
213+
if(legacyUserSyncString != null) {
214+
val legacyUserSyncJSON = JSONObject(legacyUserSyncString)
215+
val notificationTypes = legacyUserSyncJSON.getInt("notification_types")
216+
217+
val pushSubscriptionModel = SubscriptionModel()
218+
pushSubscriptionModel.id = legacyPlayerId
219+
pushSubscriptionModel.type = SubscriptionType.PUSH
220+
pushSubscriptionModel.optedIn = notificationTypes != SubscriptionStatus.NO_PERMISSION.value && notificationTypes != SubscriptionStatus.UNSUBSCRIBE.value
221+
pushSubscriptionModel.address = legacyUserSyncJSON.safeString("identifier") ?: ""
222+
pushSubscriptionModel.status = SubscriptionStatus.fromInt(notificationTypes) ?: SubscriptionStatus.NO_PERMISSION
223+
_configModel!!.pushSubscriptionId = legacyPlayerId
224+
_subscriptionModelStore!!.add(pushSubscriptionModel, ModelChangeTags.NO_PROPOGATE)
225+
suppressBackendOperation = true
226+
}
227+
228+
createAndSwitchToNewUser(suppressBackendOperation = suppressBackendOperation)
229+
230+
_operationRepo!!.enqueue(LoginUserFromSubscriptionOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId, legacyPlayerId))
231+
_preferencesService!!.saveString(PreferenceStores.ONESIGNAL, PreferenceOneSignalKeys.PREFS_LEGACY_PLAYER_ID, null)
232+
}
193233
} else {
194234
Logging.debug("initWithContext: using cached user ${_identityModelStore!!.model.onesignalId}")
195235
_operationRepo!!.enqueue(RefreshUserOperation(_configModel!!.appId, _identityModelStore!!.model.onesignalId))
@@ -299,7 +339,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
299339
}
300340
}
301341

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

305345
// create a new identity and properties model locally

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/UserModule.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import com.onesignal.user.internal.builduser.IRebuildUserService
1515
import com.onesignal.user.internal.builduser.impl.RebuildUserService
1616
import com.onesignal.user.internal.identity.IdentityModelStore
1717
import com.onesignal.user.internal.operations.impl.executors.IdentityOperationExecutor
18+
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor
1819
import com.onesignal.user.internal.operations.impl.executors.LoginUserOperationExecutor
1920
import com.onesignal.user.internal.operations.impl.executors.RefreshUserOperationExecutor
2021
import com.onesignal.user.internal.operations.impl.executors.SubscriptionOperationExecutor
@@ -57,6 +58,7 @@ internal class UserModule : IModule {
5758
.provides<UpdateUserOperationExecutor>()
5859
.provides<IOperationExecutor>()
5960
builder.register<LoginUserOperationExecutor>().provides<IOperationExecutor>()
61+
builder.register<LoginUserFromSubscriptionOperationExecutor>().provides<IOperationExecutor>()
6062
builder.register<RefreshUserOperationExecutor>().provides<IOperationExecutor>()
6163
builder.register<UserManager>().provides<IUserManager>()
6264
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/ISubscriptionBackendService.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,14 @@ interface ISubscriptionBackendService {
4444
* @param aliasValue The identifier within the [aliasLabel] that identifies the user to transfer under.
4545
*/
4646
suspend fun transferSubscription(appId: String, subscriptionId: String, aliasLabel: String, aliasValue: String)
47+
48+
/**
49+
* Given an existing subscription, retrieve all identities associated to it.
50+
*
51+
* @param appId The ID of the OneSignal application this subscription exists under.
52+
* @param subscriptionId The ID of the subscription to retrieve identities for.
53+
*
54+
* @return The identities associated to the subscription.
55+
*/
56+
suspend fun getIdentityFromSubscription(appId: String, subscriptionId: String) : Map<String, String>
4757
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/backend/impl/SubscriptionBackendService.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.onesignal.user.internal.backend.impl
22

33
import com.onesignal.common.exceptions.BackendException
44
import com.onesignal.common.safeJSONObject
5+
import com.onesignal.common.toMap
56
import com.onesignal.core.internal.http.IHttpClient
67
import com.onesignal.user.internal.backend.ISubscriptionBackendService
78
import com.onesignal.user.internal.backend.SubscriptionObject
@@ -60,4 +61,16 @@ internal class SubscriptionBackendService(
6061
throw BackendException(response.statusCode, response.payload)
6162
}
6263
}
64+
65+
override suspend fun getIdentityFromSubscription(appId: String, subscriptionId: String): Map<String, String> {
66+
val response = _httpClient.get("apps/$appId/subscriptions/$subscriptionId/user/identity")
67+
68+
if (!response.isSuccess) {
69+
throw BackendException(response.statusCode, response.payload)
70+
}
71+
72+
val responseJSON = JSONObject(response.payload!!)
73+
val identityJSON = responseJSON.safeJSONObject("identity")
74+
return identityJSON?.toMap()?.mapValues { it.value.toString() } ?: mapOf()
75+
}
6376
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.onesignal.user.internal.operations
2+
3+
import com.onesignal.core.internal.operations.GroupComparisonType
4+
import com.onesignal.core.internal.operations.Operation
5+
import com.onesignal.user.internal.operations.impl.executors.LoginUserFromSubscriptionOperationExecutor
6+
7+
/**
8+
* An [Operation] to login the user with the [subscriptionId] provided.
9+
*/
10+
class LoginUserFromSubscriptionOperation() : Operation(LoginUserFromSubscriptionOperationExecutor.LOGIN_USER_FROM_SUBSCRIPTION_USER) {
11+
/**
12+
* The application ID the user will exist/be logged in under.
13+
*/
14+
var appId: String
15+
get() = getStringProperty(::appId.name)
16+
private set(value) { setStringProperty(::appId.name, value) }
17+
18+
/**
19+
* The local OneSignal ID this user was initially logged in under. The user models with this ID
20+
* will have its ID updated with the backend-generated ID post-create.
21+
*/
22+
var onesignalId: String
23+
get() = getStringProperty(::onesignalId.name)
24+
private set(value) { setStringProperty(::onesignalId.name, value) }
25+
26+
/**
27+
* The optional external ID of this newly logged-in user. Must be unique for the [appId].
28+
*/
29+
var subscriptionId: String
30+
get() = getStringProperty(::subscriptionId.name)
31+
private set(value) { setStringProperty(::subscriptionId.name, value) }
32+
33+
override val createComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Login"
34+
override val modifyComparisonKey: String get() = "$appId.Subscription.$subscriptionId.Login"
35+
override val groupComparisonType: GroupComparisonType = GroupComparisonType.NONE
36+
override val canStartExecute: Boolean = true
37+
38+
constructor(appId: String, onesignalId: String, playerId: String) : this() {
39+
this.appId = appId
40+
this.onesignalId = onesignalId
41+
this.subscriptionId = playerId
42+
}
43+
}

OneSignalSDK/onesignal/core/src/main/java/com/onesignal/user/internal/operations/LoginUserOperation.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class LoginUserOperation() : Operation(LoginUserOperationExecutor.LOGIN_USER) {
4747
override val createComparisonKey: String get() = "$appId.User.$onesignalId"
4848
override val modifyComparisonKey: String = ""
4949
override val groupComparisonType: GroupComparisonType = GroupComparisonType.CREATE
50-
override val canStartExecute: Boolean = existingOnesignalId == null || !IDManager.isLocalId(existingOnesignalId!!)
50+
override val canStartExecute: Boolean get() = existingOnesignalId == null || !IDManager.isLocalId(existingOnesignalId!!)
5151

5252
constructor(appId: String, onesignalId: String, externalId: String?, existingOneSignalId: String? = null) : this() {
5353
this.appId = appId
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package com.onesignal.user.internal.operations.impl.executors
2+
3+
import com.onesignal.common.NetworkUtils
4+
import com.onesignal.common.exceptions.BackendException
5+
import com.onesignal.common.modeling.ModelChangeTags
6+
import com.onesignal.core.internal.operations.ExecutionResponse
7+
import com.onesignal.core.internal.operations.ExecutionResult
8+
import com.onesignal.core.internal.operations.IOperationExecutor
9+
import com.onesignal.core.internal.operations.Operation
10+
import com.onesignal.debug.internal.logging.Logging
11+
import com.onesignal.user.internal.backend.ISubscriptionBackendService
12+
import com.onesignal.user.internal.backend.IdentityConstants
13+
import com.onesignal.user.internal.identity.IdentityModelStore
14+
import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation
15+
import com.onesignal.user.internal.operations.RefreshUserOperation
16+
import com.onesignal.user.internal.properties.PropertiesModel
17+
import com.onesignal.user.internal.properties.PropertiesModelStore
18+
19+
internal class LoginUserFromSubscriptionOperationExecutor(
20+
private val _subscriptionBackend: ISubscriptionBackendService,
21+
private val _identityModelStore: IdentityModelStore,
22+
private val _propertiesModelStore: PropertiesModelStore
23+
) : IOperationExecutor {
24+
25+
override val operations: List<String>
26+
get() = listOf(LOGIN_USER_FROM_SUBSCRIPTION_USER)
27+
28+
override suspend fun execute(operations: List<Operation>): ExecutionResponse {
29+
Logging.debug("LoginUserFromSubscriptionOperationExecutor(operation: $operations)")
30+
31+
val startingOp = operations.first()
32+
33+
if (startingOp is LoginUserFromSubscriptionOperation) {
34+
return loginUser(startingOp)
35+
}
36+
37+
throw Exception("Unrecognized operation: $startingOp")
38+
}
39+
40+
private suspend fun loginUser(loginUserOp: LoginUserFromSubscriptionOperation): ExecutionResponse {
41+
try {
42+
val identities = _subscriptionBackend.getIdentityFromSubscription(
43+
loginUserOp.appId,
44+
loginUserOp.subscriptionId
45+
)
46+
val backendOneSignalId = identities.getOrDefault(IdentityConstants.ONESIGNAL_ID, null)
47+
48+
if (backendOneSignalId == null) {
49+
Logging.warn("Subscription ${loginUserOp.subscriptionId} has no ${IdentityConstants.ONESIGNAL_ID}!")
50+
return ExecutionResponse(ExecutionResult.FAIL_NORETRY)
51+
}
52+
53+
val idTranslations = mutableMapOf<String, String>()
54+
// Add the "local-to-backend" ID translation to the IdentifierTranslator for any operations that were
55+
// *not* executed but still reference the locally-generated IDs.
56+
// Update the current identity, property, and subscription models from a local ID to the backend ID
57+
idTranslations[loginUserOp.onesignalId] = backendOneSignalId
58+
59+
val identityModel = _identityModelStore.model
60+
val propertiesModel = _propertiesModelStore.model
61+
62+
if (identityModel.onesignalId == loginUserOp.onesignalId) {
63+
identityModel.setStringProperty(IdentityConstants.ONESIGNAL_ID, backendOneSignalId, ModelChangeTags.HYDRATE)
64+
}
65+
66+
if (propertiesModel.onesignalId == loginUserOp.onesignalId) {
67+
propertiesModel.setStringProperty(PropertiesModel::onesignalId.name, backendOneSignalId, ModelChangeTags.HYDRATE)
68+
}
69+
70+
return ExecutionResponse(ExecutionResult.SUCCESS, idTranslations,listOf(RefreshUserOperation(loginUserOp.appId, backendOneSignalId)))
71+
} catch (ex: BackendException) {
72+
val responseType = NetworkUtils.getResponseStatusType(ex.statusCode)
73+
74+
return when (responseType) {
75+
NetworkUtils.ResponseStatusType.RETRYABLE ->
76+
ExecutionResponse(ExecutionResult.FAIL_RETRY)
77+
NetworkUtils.ResponseStatusType.UNAUTHORIZED ->
78+
ExecutionResponse(ExecutionResult.FAIL_UNAUTHORIZED)
79+
else ->
80+
ExecutionResponse(ExecutionResult.FAIL_NORETRY)
81+
}
82+
}
83+
}
84+
85+
companion object {
86+
const val LOGIN_USER_FROM_SUBSCRIPTION_USER = "login-user-from-subscription"
87+
}
88+
}

0 commit comments

Comments
 (0)