Skip to content

Commit 37ad22a

Browse files
authored
Merge pull request #1243 from OneSignal/5.0.0/migrating_v3_sdk_users
[5.0.0] Migrating SDK 3.x players
2 parents 4f05bec + 2d321fc commit 37ad22a

File tree

5 files changed

+189
-21
lines changed

5 files changed

+189
-21
lines changed

iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@
6565
#define OSUD_PERMISSION_EPHEMERAL_FROM @"OSUD_PERMISSION_EPHEMERAL_FROM" // * OSUD_PERMISSION_EPHEMERAL_FROM
6666
#define OSUD_LANGUAGE @"OSUD_LANGUAGE" // * OSUD_LANGUAGE
6767
#define DEFAULT_LANGUAGE @"en" // * OSUD_LANGUAGE
68-
// Push Subscription
69-
#define OSUD_PUSH_SUBSCRIPTION_ID @"GT_PLAYER_ID" // * OSUD_PUSH_SUBSCRIPTION_ID
70-
#define OSUD_PUSH_TOKEN @"GT_DEVICE_TOKEN" // * OSUD_PUSH_TOKEN
68+
69+
/* Push Subscription */
70+
#define OSUD_LEGACY_PLAYER_ID @"GT_PLAYER_ID" // The legacy player ID from SDKs prior to 5.x.x
71+
#define OSUD_PUSH_SUBSCRIPTION_ID @"OSUD_PUSH_SUBSCRIPTION_ID"
72+
#define OSUD_PUSH_TOKEN @"GT_DEVICE_TOKEN"
73+
7174
// Notification
7275
#define OSUD_LAST_MESSAGE_OPENED @"GT_LAST_MESSAGE_OPENED_" // * OSUD_MOST_RECENT_NOTIFICATION_OPENED
7376
#define OSUD_NOTIFICATION_OPEN_LAUNCH_URL @"ONESIGNAL_INAPP_LAUNCH_URL" // * OSUD_NOTIFICATION_OPEN_LAUNCH_URL

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModel.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,8 @@ class OSSubscriptionModel: OSModel {
189189
guard self.type == .push && _isDisabled != oldValue else {
190190
return
191191
}
192-
notificationTypes = -2
193192
firePushSubscriptionChanged(.isDisabled(oldValue))
193+
notificationTypes = -2
194194
}
195195
}
196196

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSUserRequests.swift

Lines changed: 142 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import OneSignalOSCore
3131

3232
/**
3333
Involved in the login process and responsible for Identify User and Create User.
34-
Can execute `OSRequestCreateUser`, `OSRequestIdentifyUser`, `OSRequestTransferSubscription`, `OSRequestFetchUser`.
34+
Can execute `OSRequestCreateUser`, `OSRequestIdentifyUser`, `OSRequestTransferSubscription`, `OSRequestFetchUser`, `OSRequestFetchIdentityBySubscription`.
3535
*/
3636
class OSUserExecutor {
3737
static var userRequestQueue: [OSUserRequest] = []
@@ -42,11 +42,24 @@ class OSUserExecutor {
4242
static func start() {
4343
var userRequestQueue: [OSUserRequest] = []
4444

45-
// Read unfinished Create User + Identify User requests from cache, if any...
45+
// Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any...
4646
if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] {
4747
// Hook each uncached Request to the right model reference
4848
for request in cachedRequestQueue {
49-
if request.isKind(of: OSRequestCreateUser.self), let req = request as? OSRequestCreateUser {
49+
if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription {
50+
if let identityModel = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: req.identityModel.modelId) {
51+
// 1. The model exist in the store, set it to be the Request's model
52+
req.identityModel = identityModel
53+
} else if let identityModel = identityModels[req.identityModel.modelId] {
54+
// 2. The model exists in the dict of identityModels already processed to use
55+
req.identityModel = identityModel
56+
} else {
57+
// 3. The models do not exist, use the model on the request, and add to dict.
58+
identityModels[req.identityModel.modelId] = req.identityModel
59+
}
60+
userRequestQueue.append(req)
61+
62+
} else if request.isKind(of: OSRequestCreateUser.self), let req = request as? OSRequestCreateUser {
5063
if let identityModel = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: req.identityModel.modelId) {
5164
// 1. The model exist in the store, set it to be the Request's model
5265
req.identityModel = identityModel
@@ -144,8 +157,11 @@ class OSUserExecutor {
144157
if !request.prepareForExecution() {
145158
return
146159
}
147-
148-
if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser {
160+
161+
if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let fetchIdentityRequest = request as? OSRequestFetchIdentityBySubscription {
162+
executeFetchIdentityBySubscriptionRequest(fetchIdentityRequest)
163+
return
164+
} else if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser {
149165
executeCreateUserRequest(createUserRequest)
150166
return
151167
} else if request.isKind(of: OSRequestIdentifyUser.self), let identifyUserRequest = request as? OSRequestIdentifyUser {
@@ -262,8 +278,8 @@ class OSUserExecutor {
262278
return response?["properties"] as? [String: Any]
263279
}
264280

265-
static func parseIdentityObjectResponse(_ response: [AnyHashable: Any]?) -> [String: Any]? {
266-
return response?["identity"] as? [String: Any]
281+
static func parseIdentityObjectResponse(_ response: [AnyHashable: Any]?) -> [String: String]? {
282+
return response?["identity"] as? [String: String]
267283
}
268284

269285
// We will pass minimal properties to this request
@@ -304,7 +320,7 @@ class OSUserExecutor {
304320
// If this user already exists and we logged into an external_id, fetch the user data
305321
// TODO: Only do this if response code is 200 or 202
306322
// Fetch the user only if its the current user
307-
if let _ = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModel.modelId),
323+
if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel),
308324
let identity = request.parameters?["identity"] as? [String: String],
309325
let externalId = identity[OS_EXTERNAL_ID] {
310326
fetchUser(aliasLabel: OS_EXTERNAL_ID, aliasId: externalId, identityModel: request.identityModel)
@@ -323,7 +339,60 @@ class OSUserExecutor {
323339
executePendingRequests()
324340
}
325341
}
342+
343+
static func fetchIdentityBySubscription(_ user: OSUserInternal) {
344+
let request = OSRequestFetchIdentityBySubscription(identityModel: user.identityModel, pushSubscriptionModel: user.pushSubscriptionModel)
345+
346+
appendToQueue(request)
347+
executePendingRequests()
348+
}
349+
350+
/**
351+
For migrating legacy players from 3.x to 5.x. This request will fetch the identity object for a subscription ID, and we will use the returned onesignalId to fetch and hydrate the local user.
352+
*/
353+
static func executeFetchIdentityBySubscriptionRequest(_ request: OSRequestFetchIdentityBySubscription) {
354+
guard !request.sentToClient else {
355+
return
356+
}
357+
guard request.prepareForExecution() else {
358+
return
359+
}
360+
request.sentToClient = true
326361

362+
OneSignalClient.shared().execute(request) { response in
363+
removeFromQueue(request)
364+
365+
if let identityObject = parseIdentityObjectResponse(response),
366+
let onesignalId = identityObject[OS_ONESIGNAL_ID]
367+
{
368+
request.identityModel.hydrate(identityObject)
369+
370+
// Fetch this user's data if it is the current user
371+
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel)
372+
else {
373+
executePendingRequests()
374+
return
375+
}
376+
377+
fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModel)
378+
}
379+
} onFailure: { error in
380+
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor executeFetchIdentityBySubscriptionRequest failed with error: \(error.debugDescription)")
381+
382+
// TODO: Differentiate error cases
383+
384+
// If the error is not retryable, remove from cache and queue
385+
if let nsError = error as? NSError,
386+
nsError.code < 500 && nsError.code != 0 {
387+
// remove the subscription_id but keep the same push subscription model
388+
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStore.getModels()[OS_PUSH_SUBSCRIPTION_MODEL_KEY]?.subscriptionId = nil
389+
removeFromQueue(request)
390+
}
391+
// Otherwise it is a retryable error
392+
executePendingRequests()
393+
}
394+
}
395+
327396
static func identifyUser(externalId: String, identityModelToIdentify: OSIdentityModel, identityModelToUpdate: OSIdentityModel) {
328397
let request = OSRequestIdentifyUser(
329398
aliasLabel: OS_EXTERNAL_ID,
@@ -352,22 +421,21 @@ class OSUserExecutor {
352421

353422
// the anonymous user has been identified, still need to Fetch User as we cleared local data
354423
// Fetch the user only if its the current user
355-
if let _ = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModelToUpdate.modelId) {
424+
if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) {
356425
fetchUser(aliasLabel: OS_EXTERNAL_ID, aliasId: request.aliasId, identityModel: request.identityModelToUpdate)
357426
} else {
358427
executePendingRequests()
359428
}
360429
} onFailure: { error in
361430
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "executeIdentifyUserRequest failed with error \(error.debugDescription)")
362-
removeFromQueue(request)
363431
// Returns 409 if any provided (label, id) pair exists on another User, so the SDK will switch to this user.
364432
if let nsError = error as? NSError {
365433
if nsError.code == 409 {
366434
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "executeIdentifyUserRequest returned 409, failed due to alias already assigned to a different user. Now switch to this user.")
367435

368436
removeFromQueue(request)
369437
// Fetch the user only if its the current user
370-
if let _ = OneSignalUserManagerImpl.sharedInstance.identityModelStore.getModel(modelId: request.identityModelToUpdate.modelId) {
438+
if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) {
371439
fetchUser(aliasLabel: OS_EXTERNAL_ID, aliasId: request.aliasId, identityModel: request.identityModelToUpdate)
372440
// TODO: Link ^ to the new user... what was this todo for?
373441
}
@@ -581,6 +649,69 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest {
581649
}
582650
}
583651

652+
class OSRequestFetchIdentityBySubscription: OneSignalRequest, OSUserRequest {
653+
var sentToClient = false
654+
let stringDescription: String
655+
656+
override var description: String {
657+
return stringDescription
658+
}
659+
660+
var identityModel: OSIdentityModel
661+
var pushSubscriptionModel: OSSubscriptionModel
662+
663+
func prepareForExecution() -> Bool {
664+
guard let appId = OneSignalConfigManager.getAppId() else {
665+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the FetchIdentityBySubscription request due to null app ID.")
666+
return false
667+
}
668+
669+
if let subscriptionId = pushSubscriptionModel.subscriptionId {
670+
self.path = "apps/\(appId)/subscriptions/\(subscriptionId)/user/identity"
671+
return true
672+
} else {
673+
// This is an error, and should never happen
674+
OneSignalLog.onesignalLog(.LL_ERROR, message: "Cannot generate the FetchIdentityBySubscription request due to null subscriptionId.")
675+
self.path = ""
676+
return false
677+
}
678+
}
679+
680+
init(identityModel: OSIdentityModel, pushSubscriptionModel: OSSubscriptionModel) {
681+
self.identityModel = identityModel
682+
self.pushSubscriptionModel = pushSubscriptionModel
683+
self.stringDescription = "OSRequestFetchIdentityBySubscription with subscriptionId: \(pushSubscriptionModel.subscriptionId ?? "nil")"
684+
super.init()
685+
self.method = GET
686+
}
687+
688+
func encode(with coder: NSCoder) {
689+
coder.encode(identityModel, forKey: "identityModel")
690+
coder.encode(pushSubscriptionModel, forKey: "pushSubscriptionModel")
691+
coder.encode(method.rawValue, forKey: "method") // Encodes as String
692+
coder.encode(timestamp, forKey: "timestamp")
693+
}
694+
695+
required init?(coder: NSCoder) {
696+
guard
697+
let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel,
698+
let pushSubscriptionModel = coder.decodeObject(forKey: "pushSubscriptionModel") as? OSSubscriptionModel,
699+
let rawMethod = coder.decodeObject(forKey: "method") as? UInt32,
700+
let timestamp = coder.decodeObject(forKey: "timestamp") as? Date
701+
else {
702+
// Log error
703+
return nil
704+
}
705+
self.identityModel = identityModel
706+
self.pushSubscriptionModel = pushSubscriptionModel
707+
708+
self.stringDescription = "OSRequestFetchIdentityBySubscription with subscriptionId: \(pushSubscriptionModel.subscriptionId ?? "nil")"
709+
super.init()
710+
self.method = HTTPMethod(rawValue: rawMethod)
711+
self.timestamp = timestamp
712+
}
713+
}
714+
584715
/**
585716
The `identityModelToIdentify` is used for the `onesignal_id` of the user we want to associate with this alias.
586717
This request will tell us if we should continue with the previous user who is now identitfied, or to change users to the one this alias already exists on.

iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
185185

186186
OSNotificationsManager.delegate = self
187187

188-
// Load user from cache, if any
188+
// Path 1. Load user from cache, if any
189189
// Corrupted state if any of these models exist without the others
190190
if let identityModel = identityModelStore.getModels()[OS_IDENTITY_MODEL_KEY],
191191
let propertiesModel = propertiesModelStore.getModels()[OS_PROPERTIES_MODEL_KEY],
@@ -211,8 +211,16 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
211211
OSOperationRepo.sharedInstance.addExecutor(propertyExecutor)
212212
OSOperationRepo.sharedInstance.addExecutor(subscriptionExecutor)
213213

214-
// Creates an anonymous user if there isn't one in the cache
215-
createUserIfNil()
214+
// Path 2. There is a legacy player to migrate
215+
if let legacyPlayerId = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_LEGACY_PLAYER_ID, defaultValue: nil) {
216+
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OneSignalUserManager: creating user linked to legacy subscription \(legacyPlayerId)")
217+
createUserFromLegacyPlayer(legacyPlayerId)
218+
OneSignalUserDefaults.initStandard().removeValue(forKey: OSUD_LEGACY_PLAYER_ID)
219+
OneSignalUserDefaults.initShared().removeValue(forKey: OSUD_LEGACY_PLAYER_ID)
220+
} else {
221+
// Path 3. Creates an anonymous user if there isn't one in the cache nor a legacy player
222+
createUserIfNil()
223+
}
216224

217225
// Model store listeners subscribe to their models
218226
identityModelStoreListener.start()
@@ -235,6 +243,20 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
235243
_ = _login(externalId: externalId, token: token)
236244
}
237245

246+
/**
247+
Converting a 3.x player to a 5.x user. There is a cached legacy player, so we will create the user based on the legacy player ID.
248+
*/
249+
private func createUserFromLegacyPlayer(_ playerId: String) {
250+
// 1. Create the Push Subscription Model
251+
let pushSubscriptionModel = createDefaultPushSubscription(subscriptionId: playerId)
252+
253+
// 2. Set the internal user
254+
let newUser = setNewInternalUser(externalId: nil, pushSubscriptionModel: pushSubscriptionModel)
255+
256+
// 3. Make the request
257+
OSUserExecutor.fetchIdentityBySubscription(newUser)
258+
}
259+
238260
private func createNewUser(externalId: String?, token: String?) -> OSUserInternal {
239261
guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else {
240262
return _mockUser
@@ -289,6 +311,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
289311
)
290312
}
291313

314+
/**
315+
Returns if the OSIdentityModel passed in belongs to the current user. This method is used in deciding whether or not to hydrate via a server response, for example.
316+
*/
317+
func isCurrentUser(_ identityModel: OSIdentityModel) -> Bool {
318+
return self.identityModelStore.getModel(modelId: identityModel.modelId) != nil
319+
}
320+
292321
/**
293322
Clears the existing user's data in preparation for hydration via a fetch user call.
294323
*/
@@ -378,7 +407,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
378407

379408
// TODO: We will have to save subscription_id and push_token to user defaults when we get them
380409

381-
let pushSubscription = pushSubscriptionModel ?? createDefaultPushSubscription()
410+
let pushSubscription = pushSubscriptionModel ?? createDefaultPushSubscription(subscriptionId: nil)
382411

383412
// Add pushSubscription to store if not present
384413
if !pushSubscriptionModelStore.getModels().keys.contains(OS_PUSH_SUBSCRIPTION_MODEL_KEY) {
@@ -389,11 +418,14 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager {
389418
return self.user
390419
}
391420

392-
func createDefaultPushSubscription() -> OSSubscriptionModel {
421+
/**
422+
Creates a default Push Subscription Model using the optionally passed in subscriptionId. An scenario where the subscriptionId will be passed in is when we are converting a legacy player's information from 3.x into a Push Subscription Model.
423+
*/
424+
func createDefaultPushSubscription(subscriptionId: String?) -> OSSubscriptionModel {
393425
let sharedUserDefaults = OneSignalUserDefaults.initShared()
394426
let reachable = OSNotificationsManager.currentPermissionState.reachable
395427
let token = sharedUserDefaults.getSavedString(forKey: OSUD_PUSH_TOKEN, defaultValue: nil)
396-
let subscriptionId = sharedUserDefaults.getSavedString(forKey: OSUD_PUSH_SUBSCRIPTION_ID, defaultValue: nil)
428+
let subscriptionId = subscriptionId ?? sharedUserDefaults.getSavedString(forKey: OSUD_PUSH_SUBSCRIPTION_ID, defaultValue: nil)
397429

398430
return OSSubscriptionModel(type: .push,
399431
address: token,

iOS_SDK/OneSignalSDK/Source/OneSignal.m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,8 @@ + (void)handleAppIdChange:(NSString*)appId {
613613
// Remove player_id from both standard and shared NSUserDefaults
614614
[standardUserDefaults removeValueForKey:OSUD_PUSH_SUBSCRIPTION_ID];
615615
[sharedUserDefaults removeValueForKey:OSUD_PUSH_SUBSCRIPTION_ID];
616+
[standardUserDefaults removeValueForKey:OSUD_LEGACY_PLAYER_ID];
617+
[sharedUserDefaults removeValueForKey:OSUD_LEGACY_PLAYER_ID];
616618

617619
// Clear all cached data, does not start User Module nor call logout.
618620
[OneSignalUserManagerImpl.sharedInstance clearAllModelsFromStores];

0 commit comments

Comments
 (0)