Skip to content

Commit 7a31edd

Browse files
committed
[User Model] Support migrating user from SDK 4.x to SDK 5.x
* On initialization, check for old player_id and build local user as required * New LoginUserFromSubscriptionOperation and execution to support converting a local user to the user that owns a subscription.
1 parent 7f06356 commit 7a31edd

File tree

8 files changed

+213
-5
lines changed

8 files changed

+213
-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: 45 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
@@ -15,6 +16,9 @@ import com.onesignal.core.internal.application.impl.ApplicationService
1516
import com.onesignal.core.internal.config.ConfigModel
1617
import com.onesignal.core.internal.config.ConfigModelStore
1718
import com.onesignal.core.internal.operations.IOperationRepo
19+
import com.onesignal.core.internal.preferences.IPreferencesService
20+
import com.onesignal.core.internal.preferences.PreferenceOneSignalKeys
21+
import com.onesignal.core.internal.preferences.PreferenceStores
1822
import com.onesignal.core.internal.startup.StartupService
1923
import com.onesignal.debug.IDebugManager
2024
import com.onesignal.debug.LogLevel
@@ -32,6 +36,7 @@ import com.onesignal.user.UserModule
3236
import com.onesignal.user.internal.backend.IdentityConstants
3337
import com.onesignal.user.internal.identity.IdentityModel
3438
import com.onesignal.user.internal.identity.IdentityModelStore
39+
import com.onesignal.user.internal.operations.LoginUserFromSubscriptionOperation
3540
import com.onesignal.user.internal.operations.LoginUserOperation
3641
import com.onesignal.user.internal.operations.RefreshUserOperation
3742
import com.onesignal.user.internal.properties.PropertiesModel
@@ -43,6 +48,7 @@ import com.onesignal.user.internal.subscriptions.SubscriptionType
4348
import kotlinx.coroutines.sync.Mutex
4449
import kotlinx.coroutines.sync.withLock
4550
import kotlinx.coroutines.yield
51+
import org.json.JSONObject
4652

4753
internal class OneSignalImp : IOneSignal, IServiceProvider {
4854
override val sdkVersion: String = OneSignalUtils.sdkVersion
@@ -88,6 +94,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
8894
private var _propertiesModelStore: PropertiesModelStore? = null
8995
private var _subscriptionModelStore: SubscriptionModelStore? = null
9096
private var _startupService: StartupService? = null
97+
private var _preferencesService: IPreferencesService? = null
9198

9299
// Other State
93100
private val _services: ServiceProvider
@@ -183,14 +190,48 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
183190
_propertiesModelStore = _services.getService()
184191
_identityModelStore = _services.getService()
185192
_subscriptionModelStore = _services.getService()
193+
_preferencesService = _services.getService()
186194

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

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

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

282323
// create a new identity and properties model locally
@@ -321,7 +362,7 @@ internal class OneSignalImp : IOneSignal, IServiceProvider {
321362
_subscriptionModelStore!!.clear(ModelChangeTags.NO_PROPOGATE)
322363
_identityModelStore!!.replace(identityModel)
323364
_propertiesModelStore!!.replace(propertiesModel)
324-
_subscriptionModelStore!!.replaceAll(subscriptions)
365+
_subscriptionModelStore!!.replaceAll(subscriptions, if (suppressBackendOperation) ModelChangeTags.NO_PROPOGATE else ModelChangeTags.NORMAL)
325366
}
326367

327368
override fun <T> hasService(c: Class<T>): Boolean = _services.hasService(c)

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)