Skip to content

Commit 2e841d7

Browse files
authored
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 674fd4a + 0478d56 commit 2e841d7

File tree

8 files changed

+216
-5
lines changed

8 files changed

+216
-5
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: 48 additions & 4 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
@@ -42,6 +47,7 @@ import com.onesignal.user.internal.subscriptions.SubscriptionModel
4247
import com.onesignal.user.internal.subscriptions.SubscriptionModelStore
4348
import com.onesignal.user.internal.subscriptions.SubscriptionStatus
4449
import com.onesignal.user.internal.subscriptions.SubscriptionType
50+
import org.json.JSONObject
4551

4652
internal class OneSignalImp : IOneSignal, IServiceProvider {
4753
override val sdkVersion: String = OneSignalUtils.sdkVersion
@@ -87,6 +93,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
8793
private var _propertiesModelStore: PropertiesModelStore? = null
8894
private var _subscriptionModelStore: SubscriptionModelStore? = null
8995
private var _startupService: StartupService? = null
96+
private var _preferencesService: IPreferencesService? = null
9097

9198
// Other State
9299
private val _services: ServiceProvider
@@ -182,14 +189,48 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
182189
_propertiesModelStore = _services.getService()
183190
_identityModelStore = _services.getService()
184191
_subscriptionModelStore = _services.getService()
192+
_preferencesService = _services.getService()
185193

186194
// Instantiate and call the IStartableServices
187195
_startupService = _services.getService()
188196
_startupService!!.bootstrap()
189197

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

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

305346
// create a new identity and properties model locally
@@ -345,7 +386,10 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
345386
_identityModelStore!!.replace(identityModel)
346387
_propertiesModelStore!!.replace(propertiesModel)
347388

348-
if (currentPushSubscription != null) {
389+
if (suppressBackendOperation) {
390+
_subscriptionModelStore!!.replaceAll(subscriptions, ModelChangeTags.NO_PROPOGATE)
391+
}
392+
else if (currentPushSubscription != null) {
349393
_operationRepo!!.enqueue(TransferSubscriptionOperation(_configModel!!.appId, currentPushSubscription.id, sdkId))
350394
_subscriptionModelStore!!.replaceAll(subscriptions, ModelChangeTags.NO_PROPOGATE)
351395
} else {

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)