From 3f83a50e78c6156f83d4d5716e065d957ed6a70e Mon Sep 17 00:00:00 2001 From: Nan Date: Sat, 3 Aug 2024 19:42:18 -0700 Subject: [PATCH 01/56] [dev app] for testing, revert later * Add text fields and buttons for testing * Use staging * remove app clips * Make Client more verbose with headers --- .../OneSignalDevApp/AppDelegate.m | 2 +- .../Base.lproj/Main.storyboard | 53 +++- .../OneSignalDevApp.entitlements | 2 +- .../OneSignalDevApp/ViewController.h | 2 + .../OneSignalDevApp/ViewController.m | 12 +- .../OneSignalDevAppClip/ViewController.h | 1 + .../project.pbxproj | 227 +----------------- ...lNotificationServiceExtension.entitlements | 2 +- .../Source/API/OneSignalClient.m | 2 +- .../Source/OneSignalCommonDefines.h | 4 +- .../Source/OneSignalMobileProvision.m | 2 +- 11 files changed, 66 insertions(+), 243 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index d3061a94a..dfa6df4c0 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -86,7 +86,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -#define ONESIGNAL_APP_ID_DEFAULT @"77e32082-ea27-42e3-a898-c72e141824ef" +#define ONESIGNAL_APP_ID_DEFAULT @"STAGING_APP_HERE" #define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"YOUR_APP_ID_HERE" + (NSString*)getOneSignalAppId { diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard index 9b87b6079..7e3e8014a 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -257,17 +257,25 @@ - - + + + + + + + + + @@ -528,13 +536,24 @@ + - @@ -566,22 +585,25 @@ + - + + - + + @@ -598,9 +620,11 @@ + + @@ -616,6 +640,7 @@ + @@ -630,6 +655,7 @@ + @@ -645,10 +671,12 @@ + + @@ -658,7 +686,6 @@ - @@ -667,7 +694,6 @@ - @@ -675,7 +701,6 @@ - @@ -747,7 +772,9 @@ + + @@ -760,7 +787,7 @@ - + diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/OneSignalDevApp.entitlements b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/OneSignalDevApp.entitlements index 28007cfd9..5388fb2fe 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/OneSignalDevApp.entitlements +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/OneSignalDevApp.entitlements @@ -11,7 +11,7 @@ com.apple.security.application-groups - group.com.onesignal.example.onesignal + group.com.onesignal.example.staging.onesignal diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h index 8e4b15988..6e778926d 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.h @@ -52,7 +52,9 @@ @property (weak, nonatomic) IBOutlet UIButton *removeSmsButton; @property (weak, nonatomic) IBOutlet UITextField *externalUserIdTextField; +@property (weak, nonatomic) IBOutlet UITextField *tokenTextField; @property (weak, nonatomic) IBOutlet UIButton *loginExternalUserIdButton; +@property (weak, nonatomic) IBOutlet UIButton *updateJwtButton; @property (weak, nonatomic) IBOutlet UIButton *logoutButton; @property (weak, nonatomic) IBOutlet UITextField *addAliasLabelTextField; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m index 8a5f72eaf..fcd3c591d 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m @@ -196,8 +196,16 @@ - (IBAction)inAppMessagingSegmentedControlValueChanged:(UISegmentedControl *)sen - (IBAction)loginExternalUserId:(UIButton *)sender { NSString* externalUserId = self.externalUserIdTextField.text; - NSLog(@"Dev App: Logging in to external user ID %@", externalUserId); - [OneSignal login:externalUserId]; + NSString* token = self.tokenTextField.text; + NSLog(@"❌ Dev App: Logging in to external user ID %@ and token %@", externalUserId, token); + [OneSignal login:externalUserId withToken:token]; +} + +- (IBAction)updateJwt:(id)sender { + NSString* externalUserId = self.externalUserIdTextField.text; + NSString* token = self.tokenTextField.text; + NSLog(@"❌ Dev App: updating JWT for external user ID %@ and token %@", externalUserId, token); + [OneSignal updateUserJwt:externalUserId withToken:token]; } - (IBAction)logout:(UIButton *)sender { diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevAppClip/ViewController.h b/iOS_SDK/OneSignalDevApp/OneSignalDevAppClip/ViewController.h index b11c2c05b..f1a45b976 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevAppClip/ViewController.h +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevAppClip/ViewController.h @@ -54,6 +54,7 @@ @property (weak, nonatomic) IBOutlet UITextField *addTriggerValue; @property (weak, nonatomic) IBOutlet UIButton *addTriggerButton; @property (weak, nonatomic) IBOutlet UITextField *removeTriggerKey; +- (IBAction)updateJwt:(id)sender; @property (weak, nonatomic) IBOutlet UITextField *getTriggerKey; @property (weak, nonatomic) IBOutlet UILabel *infoLabel; @property (weak, nonatomic) IBOutlet UITextField *outcomeName; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj index 556c0c10e..52c8cceae 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj @@ -47,32 +47,6 @@ DE12F3F8289B2B7F002F63AA /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE12F3F6289B2B7F002F63AA /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DE61E483294810B900CD12F1 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E482294810B900CD12F1 /* OneSignalFramework.framework */; }; DE61E484294810B900CD12F1 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E482294810B900CD12F1 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E4852948117000CD12F1 /* OneSignalExampleClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DE61E48A2948117A00CD12F1 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4892948117A00CD12F1 /* OneSignalCore.framework */; }; - DE61E48B2948117A00CD12F1 /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4892948117A00CD12F1 /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E48E2948117D00CD12F1 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E48D2948117D00CD12F1 /* OneSignalExtension.framework */; }; - DE61E48F2948117D00CD12F1 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E48D2948117D00CD12F1 /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E4912948118200CD12F1 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4902948118200CD12F1 /* OneSignalFramework.framework */; }; - DE61E4922948118200CD12F1 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4902948118200CD12F1 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E4942948118500CD12F1 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4932948118500CD12F1 /* OneSignalNotifications.framework */; }; - DE61E4952948118500CD12F1 /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4932948118500CD12F1 /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E4972948118900CD12F1 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */; }; - DE61E4982948118900CD12F1 /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E49A2948118C00CD12F1 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */; }; - DE61E49B2948118D00CD12F1 /* OneSignalOutcomes.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE61E49D2948119100CD12F1 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E49C2948119100CD12F1 /* OneSignalUser.framework */; }; - DE61E49E2948119100CD12F1 /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E49C2948119100CD12F1 /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - DE68DA5B24C7695900FC95A8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA5A24C7695900FC95A8 /* AppDelegate.m */; }; - DE68DA5E24C7695900FC95A8 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA5D24C7695900FC95A8 /* SceneDelegate.m */; }; - DE68DA6124C7695900FC95A8 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA6024C7695900FC95A8 /* ViewController.m */; }; - DE68DA6424C7695900FC95A8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6224C7695900FC95A8 /* Main.storyboard */; }; - DE68DA6624C7695A00FC95A8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6524C7695A00FC95A8 /* Assets.xcassets */; }; - DE68DA6924C7695A00FC95A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6724C7695A00FC95A8 /* LaunchScreen.storyboard */; }; - DE68DA6C24C7695A00FC95A8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA6B24C7695A00FC95A8 /* main.m */; }; - DE68DA7724C769F200FC95A8 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03432CDB1EBD426A0071FC48 /* CoreLocation.framework */; }; - DE68DA7824C769F900FC95A8 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9112E8A61E724EE00022A1CB /* SystemConfiguration.framework */; }; - DE68DA7924C76A0300FC95A8 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B6EA051E83215000B5CF01 /* UserNotifications.framework */; }; - DE68DA7A24C76A1800FC95A8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CACBAAB5218A7136000ACAA5 /* WebKit.framework */; }; DE7D180727026BB5002D3A5D /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; }; DE7D180827026BB5002D3A5D /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DE7D180927026BC5002D3A5D /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; }; @@ -107,13 +81,6 @@ remoteGlobalIDString = 945C59DB296CF2A00097041D; remoteInfo = OneSignalWidgetExtensionExtension; }; - DE61E4862948117000CD12F1 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 9112E87A1E724C320022A1CB /* Project object */; - proxyType = 1; - remoteGlobalIDString = DE68DA5624C7695900FC95A8; - remoteInfo = OneSignalExampleClip; - }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -135,28 +102,10 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/AppClips"; dstSubfolderSpec = 16; files = ( - DE61E4852948117000CD12F1 /* OneSignalExampleClip.app in Embed App Clips */, ); name = "Embed App Clips"; runOnlyForDeploymentPostprocessing = 0; }; - DE61E48C2948117A00CD12F1 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - DE61E4952948118500CD12F1 /* OneSignalNotifications.framework in Embed Frameworks */, - DE61E49E2948119100CD12F1 /* OneSignalUser.framework in Embed Frameworks */, - DE61E4922948118200CD12F1 /* OneSignalFramework.framework in Embed Frameworks */, - DE61E48B2948117A00CD12F1 /* OneSignalCore.framework in Embed Frameworks */, - DE61E48F2948117D00CD12F1 /* OneSignalExtension.framework in Embed Frameworks */, - DE61E49B2948118D00CD12F1 /* OneSignalOutcomes.framework in Embed Frameworks */, - DE61E4982948118900CD12F1 /* OneSignalOSCore.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; DEA226FA261F74060092FF58 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -232,7 +181,6 @@ DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalOSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalOutcomes.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE61E49C2948119100CD12F1 /* OneSignalUser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalUser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OneSignalExampleClip.app; sourceTree = BUILT_PRODUCTS_DIR; }; DE68DA5924C7695900FC95A8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DE68DA5A24C7695900FC95A8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; DE68DA5C24C7695900FC95A8 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; @@ -309,24 +257,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE68DA5424C7695900FC95A8 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - DE61E48A2948117A00CD12F1 /* OneSignalCore.framework in Frameworks */, - DE61E4912948118200CD12F1 /* OneSignalFramework.framework in Frameworks */, - DE61E4972948118900CD12F1 /* OneSignalOSCore.framework in Frameworks */, - DE61E48E2948117D00CD12F1 /* OneSignalExtension.framework in Frameworks */, - DE61E49A2948118C00CD12F1 /* OneSignalOutcomes.framework in Frameworks */, - DE61E49D2948119100CD12F1 /* OneSignalUser.framework in Frameworks */, - DE68DA7A24C76A1800FC95A8 /* WebKit.framework in Frameworks */, - DE61E4942948118500CD12F1 /* OneSignalNotifications.framework in Frameworks */, - DE68DA7924C76A0300FC95A8 /* UserNotifications.framework in Frameworks */, - DE68DA7724C769F200FC95A8 /* CoreLocation.framework in Frameworks */, - DE68DA7824C769F900FC95A8 /* SystemConfiguration.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -349,7 +279,6 @@ children = ( 9112E8821E724C320022A1CB /* OneSignalExample.app */, 9150E7721E73BEDC00C5D46A /* OneSignalNotificationServiceExtension.appex */, - DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */, 945C59DC296CF2A00097041D /* OneSignalWidgetExtensionExtension.appex */, ); name = Products; @@ -491,7 +420,6 @@ ); dependencies = ( 9150E7791E73BEDD00C5D46A /* PBXTargetDependency */, - DE61E4872948117000CD12F1 /* PBXTargetDependency */, 945C59EF296CF2A10097041D /* PBXTargetDependency */, ); name = OneSignalExample; @@ -537,26 +465,6 @@ productReference = 945C59DC296CF2A00097041D /* OneSignalWidgetExtensionExtension.appex */; productType = "com.apple.product-type.app-extension"; }; - DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */ = { - isa = PBXNativeTarget; - buildConfigurationList = DE68DA7424C7695A00FC95A8 /* Build configuration list for PBXNativeTarget "OneSignalExampleClip" */; - buildPhases = ( - DE68DA5324C7695900FC95A8 /* Sources */, - DE68DA5424C7695900FC95A8 /* Frameworks */, - DE68DA5524C7695900FC95A8 /* Resources */, - DE61E48C2948117A00CD12F1 /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = OneSignalExampleClip; - packageProductDependencies = ( - ); - productName = OneSignalDevAppClip; - productReference = DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */; - productType = "com.apple.product-type.application.on-demand-install-capable"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -569,7 +477,6 @@ TargetAttributes = { 9112E8811E724C320022A1CB = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 99SW8E36CT; LastSwiftMigration = 1320; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -597,9 +504,6 @@ 945C59DB296CF2A00097041D = { CreatedOnToolsVersion = 14.1; }; - DE68DA5624C7695900FC95A8 = { - CreatedOnToolsVersion = 12.0; - }; }; }; buildConfigurationList = 9112E87D1E724C320022A1CB /* Build configuration list for PBXProject "OneSignalExample" */; @@ -620,7 +524,6 @@ targets = ( 9112E8811E724C320022A1CB /* OneSignalExample */, 9150E7711E73BEDC00C5D46A /* OneSignalNotificationServiceExtension */, - DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */, 945C59DB296CF2A00097041D /* OneSignalWidgetExtensionExtension */, ); }; @@ -656,16 +559,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE68DA5524C7695900FC95A8 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DE68DA6924C7695A00FC95A8 /* LaunchScreen.storyboard in Resources */, - DE68DA6624C7695A00FC95A8 /* Assets.xcassets in Resources */, - DE68DA6424C7695900FC95A8 /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -703,17 +596,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - DE68DA5324C7695900FC95A8 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - DE68DA6124C7695900FC95A8 /* ViewController.m in Sources */, - DE68DA5B24C7695900FC95A8 /* AppDelegate.m in Sources */, - DE68DA6C24C7695A00FC95A8 /* main.m in Sources */, - DE68DA5E24C7695900FC95A8 /* SceneDelegate.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -728,12 +610,6 @@ target = 945C59DB296CF2A00097041D /* OneSignalWidgetExtensionExtension */; targetProxy = 945C59EE296CF2A10097041D /* PBXContainerItemProxy */; }; - DE61E4872948117000CD12F1 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - platformFilter = ios; - target = DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */; - targetProxy = DE61E4862948117000CD12F1 /* PBXContainerItemProxy */; - }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -887,7 +763,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging; PRODUCT_NAME = OneSignalExample; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "OneSignalDevApp/OneSignalExample-Bridging-Header.h"; @@ -919,7 +795,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging; PRODUCT_NAME = OneSignalExample; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "OneSignalDevApp/OneSignalExample-Bridging-Header.h"; @@ -947,7 +823,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalNotificationServiceExtensionA; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -975,7 +851,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalNotificationServiceExtensionA; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -1018,7 +894,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -1065,7 +941,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -1077,88 +953,6 @@ }; name = Release; }; - DE68DA7224C7695A00FC95A8 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = OneSignalDevAppClip/OneSignalDevAppClip.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.4.4; - DEVELOPMENT_TEAM = 99SW8E36CT; - "DYLIB_INSTALL_NAME_BASE[arch=*]" = "@rpath"; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - OS_APP_CLIP, - ); - INFOPLIST_FILE = OneSignalDevAppClip/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.4.4; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.Clip; - PRODUCT_NAME = OneSignalExampleClip; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - DE68DA7324C7695A00FC95A8 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = OneSignalDevAppClip/OneSignalDevAppClip.entitlements; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1.4.4; - DEVELOPMENT_TEAM = 99SW8E36CT; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_PREPROCESSOR_DEFINITIONS = OS_APP_CLIP; - INFOPLIST_FILE = OneSignalDevAppClip/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - ); - MARKETING_VERSION = 1.4.4; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.Clip; - PRODUCT_NAME = OneSignalExampleClip; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1198,15 +992,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - DE68DA7424C7695A00FC95A8 /* Build configuration list for PBXNativeTarget "OneSignalExampleClip" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - DE68DA7224C7695A00FC95A8 /* Debug */, - DE68DA7324C7695A00FC95A8 /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 9112E87A1E724C320022A1CB /* Project object */; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements b/iOS_SDK/OneSignalDevApp/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements index c70461e82..fa1031a37 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements +++ b/iOS_SDK/OneSignalDevApp/OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements @@ -4,7 +4,7 @@ com.apple.security.application-groups - group.com.onesignal.example.onesignal + group.com.onesignal.example.staging.onesignal diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m index d9066e347..82fa964dd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m @@ -190,7 +190,7 @@ - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request { NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with parameters: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, jsonString]]; + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with headers: %@, with params: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders, jsonString]]; } - (void)handleJSONNSURLResponse:(NSURLResponse*)response data:(NSData*)data error:(NSError*)error isAsync:(BOOL)async withRequest:(OneSignalRequest *)request onSuccess:(OSResultSuccessBlock)successBlock onFailure:(OSFailureBlock)failureBlock { diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 42d48d10d..9bcf51a54 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -33,8 +33,8 @@ // Networking #define OS_API_VERSION @"1" #define OS_API_ACCEPT_HEADER @"application/vnd.onesignal.v" OS_API_VERSION @"+json" -#define OS_API_SERVER_URL @"https://api.onesignal.com/" -#define OS_IAM_WEBVIEW_BASE_URL @"https://onesignal.com/" +#define OS_API_SERVER_URL @"https://api.staging.onesignal.com/" +#define OS_IAM_WEBVIEW_BASE_URL @"https://staging.onesignal.com/" // OneSignalUserDefault keys // String values start with "OSUD_" to maintain a level of uniqueness from other libs and app code diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m index 61f1aae9c..450b5cfca 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m @@ -87,7 +87,7 @@ + (OSUIApplicationReleaseMode) releaseMode { NSDictionary *entitlements = nil; NSDictionary *provision = [self getProvision]; if (provision) { - [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"provision: %@", provision]]; + // [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"provision: %@", provision]]; entitlements = [provision objectForKey:@"Entitlements"]; } else From aa7f3e862c506781e2ce1d46c7a0f69c26a2b64c Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 28 May 2024 12:47:26 -0700 Subject: [PATCH 02/56] internal dump method helper filled out For debugging --- .../Source/OSOperationRepo.swift | 6 ++- .../OSIdentityOperationExecutor.swift | 9 +++- .../OSPropertyOperationExecutor.swift | 8 +++- .../OSSubscriptionOperationExecutor.swift | 10 +++- .../Source/Executors/OSUserExecutor.swift | 7 ++- .../Source/OSIdentityModelRepo.swift | 8 +++- .../Source/OneSignalUserManagerImpl.swift | 46 ++++++++++++++++++- 7 files changed, 86 insertions(+), 8 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index a59a92ac7..c4a40be30 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -176,6 +176,10 @@ public class OSOperationRepo: NSObject { extension OSOperationRepo: OSLoggable { public func logSelf() { - // TODO: You fill in + print("💛 Operation Repo: deltaQueue: \(self.deltaQueue )") + print("💛 Operation Repo: executors that are subscribed:") + for executor in self.executors { + executor.logSelf() + } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index c6f7c468f..12087d9cb 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -296,6 +296,13 @@ class OSIdentityOperationExecutor: OSOperationExecutor { extension OSIdentityOperationExecutor: OSLoggable { func logSelf() { - // TODO: You fill in + print( + """ + 💛 OSIdentityOperationExecutor has the following queues: + addRequestQueue: \(self.addRequestQueue) + removeRequestQueue: \(self.removeRequestQueue) + deltaQueue: \(self.deltaQueue) + """ + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 377fa2bdc..08670b04f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -291,6 +291,12 @@ class OSPropertyOperationExecutor: OSOperationExecutor { extension OSPropertyOperationExecutor: OSLoggable { func logSelf() { - // TODO: You fill in + print( + """ + 💛 OSPropertyOperationExecutor has the following queues: + updateRequestQueue: \(self.updateRequestQueue) + deltaQueue: \(self.deltaQueue) + """ + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 345fb1b04..68148cc6c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -424,6 +424,14 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { extension OSSubscriptionOperationExecutor: OSLoggable { func logSelf() { - // TODO: You fill in + print( + """ + 💛 OSSubscriptionOperationExecutor has the following queues: + addRequestQueue: \(self.addRequestQueue) + removeRequestQueue: \(self.removeRequestQueue) + updateRequestQueue: \(self.updateRequestQueue) + deltaQueue: \(self.deltaQueue) + """ + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 704150099..985342f6e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -603,6 +603,11 @@ extension OSUserExecutor { extension OSUserExecutor: OSLoggable { func logSelf() { - // TODO: You fill in + print( + """ + 💛 OSUserExecutor has the following queues: + userRequestQueue: \(self.userRequestQueue) + """ + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index f59f94687..164f9d63a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -53,10 +53,14 @@ class OSIdentityModelRepo { return models[modelId] } } -} extension OSIdentityModelRepo: OSLoggable { func logSelf() { - // TODO: You fill in + print( + """ + 💛 OSIdentityModelRepo has the following models: + models: \(self.models) + """ + ) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index fde4f72f5..c7d0a1bdf 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -876,6 +876,50 @@ extension OneSignalUserManagerImpl: OneSignalNotificationsDelegate { extension OneSignalUserManagerImpl: OSLoggable { @objc public func logSelf() { - // TODO: You fill in + print("💛 _user: \(String(describing: _user))") + print( + """ + 💛 identityModel: + aliases: \(String(describing: _user?.identityModel.aliases)) + jwt: \(String(describing: _user?.identityModel.jwtBearerToken)) + modelId: \(String(describing: _user?.identityModel.modelId)) + """ + ) + print( + """ + 💛 propertiesModel: + tags: \(String(describing: _user?.propertiesModel.tags)) + language: \(String(describing: _user?.propertiesModel.language)) + modelId: \(String(describing: _user?.propertiesModel.modelId)) + """ + ) + let subscriptionModels = subscriptionModelStore.getModels().values + for sub in subscriptionModels { + print( + """ + 💛 subscription model from store + addess: \(String(describing: sub.address)) + subscriptionId: \(String(describing: sub.subscriptionId)) + enabled: \(sub.enabled) + modelId: \(sub.modelId) + """ + ) + } + let pushSubModel = pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY) + print( + """ + 💛 push sub model from store + token: \(String(describing: pushSubModel?.address)) + subscriptionId: \(String(describing: pushSubModel?.subscriptionId)) + enabled: \(String(describing: pushSubModel?.enabled)) + notification_types: \(String(describing: pushSubModel?.notificationTypes)) + optedIn: \(String(describing: pushSubModel?.optedIn)) + modelId: \(String(describing: pushSubModel?.modelId)) + """ + ) + operationRepo.logSelf() + userExecutor?.logSelf() + identityModelRepo.logSelf() + print("") } } From 408ef5590c2eb0bbaff52c0dba38e6d6b190e732 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 16 Aug 2024 18:44:11 -0700 Subject: [PATCH 03/56] Add internal JWT Config management and listener * Make OSUserJwtConfig object to handle auth status etc * User manager owns an instance of the JWT Config --- .../OneSignal.xcodeproj/project.pbxproj | 12 ++ .../Source/Jwt/OSUserJwtConfig.swift | 114 ++++++++++++++++++ .../Source/OneSignalUserManagerImpl.swift | 16 ++- 3 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 210704586..8a71e8d0f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -74,6 +74,7 @@ 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */; }; 3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */; }; 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */; }; + 3C2FF9D02C5FCD760081293B /* OSUserJwtConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */; }; 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; }; 3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; }; @@ -1229,6 +1230,7 @@ 3C2C7DC2288E007E0020F9AE /* UnitTests-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UnitTests-Bridging-Header.h"; sourceTree = ""; }; 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSSubscriptionModel.swift; sourceTree = ""; }; 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDelta.swift; sourceTree = ""; }; + 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserJwtConfig.swift; sourceTree = ""; }; 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; }; 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = ""; }; 3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = ""; }; @@ -2056,6 +2058,7 @@ isa = PBXGroup; children = ( 3C115163289A259500565C41 /* OneSignalOSCore.h */, + 3C2FF9CE2C5FCD590081293B /* Jwt */, 3C115188289ADEA300565C41 /* OSModelStore.swift */, 3C115186289ADE7700565C41 /* OSModelStoreListener.swift */, 3C115184289ADE4F00565C41 /* OSModel.swift */, @@ -2075,6 +2078,14 @@ path = Source; sourceTree = ""; }; + 3C2FF9CE2C5FCD590081293B /* Jwt */ = { + isa = PBXGroup; + children = ( + 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */, + ); + path = Jwt; + sourceTree = ""; + }; 3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */ = { isa = PBXGroup; children = ( @@ -4065,6 +4076,7 @@ 3CE5F9E3289D88DC004A156E /* OSModelStoreChangedHandler.swift in Sources */, 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */, 4710EA532B8FCFB200435356 /* OSDispatchQueue.swift in Sources */, + 3C2FF9D02C5FCD760081293B /* OSUserJwtConfig.swift in Sources */, DEFB3E672BB735B500E65DAD /* OSStubLiveActivities.swift in Sources */, 3C11518D289AF5E800565C41 /* OSModelChangedHandler.swift in Sources */, 3C8E6DF928A6D89E0031E48A /* OSOperationExecutor.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift new file mode 100644 index 000000000..83740cd87 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift @@ -0,0 +1,114 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import Foundation +import OneSignalCore + +/** + Use an enum to avoid working with optional Bool, which is unsightly to cache and uncache. + */ +enum OSRequiresUserAuth: String { + case on + case off + case unknown + // TODO: JWT 🔐 consider additional reasons such as detecting this by dev calling loginWithJWT / onViaRemoteParams + + func isRequired() -> Bool? { + return switch self { + case .on: + true + case .off: + false + default: + nil + } + } +} + +/** + Internal listener. + */ +public protocol OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: Bool?, to: Bool?) + func onJwtUpdated(externalId: String, to: String?) +} + +public class OSUserJwtConfig { + private let changeNotifier = OSEventProducer() + + private var requiresUserAuth: OSRequiresUserAuth { + didSet { + guard oldValue != requiresUserAuth else { + return + } + + print("❌ OSUserJwtConfig.requiresUserAuth: changing from \(oldValue) to \(requiresUserAuth), firing \(changeNotifier)") + + // Persist new value + OneSignalUserDefaults.initShared().saveString(forKey: OSUD_USE_IDENTITY_VERIFICATION, withValue: requiresUserAuth.rawValue) + + self.changeNotifier.fire { listener in + listener.onRequiresUserAuthChanged(from: oldValue.isRequired(), to: requiresUserAuth.isRequired()) + } + } + } + + public var isRequired: Bool? { + get { + return requiresUserAuth.isRequired() + } + set { + requiresUserAuth = switch newValue { + case true: + OSRequiresUserAuth.on + case false: + OSRequiresUserAuth.off + default: + OSRequiresUserAuth.unknown + } + } + } + + public init() { + let rawValue = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_USE_IDENTITY_VERIFICATION, defaultValue: OSRequiresUserAuth.unknown.rawValue) + + print("❌ OSUserJwtConfig init(): \(OSRequiresUserAuth(rawValue: rawValue!)))") + + requiresUserAuth = OSRequiresUserAuth(rawValue: rawValue ?? OSRequiresUserAuth.unknown.rawValue) ?? OSRequiresUserAuth.unknown + } + + public func subscribe(_ listener: OSUserJwtConfigListener, key: String) { + self.changeNotifier.subscribe(listener, key: key) + } + + public func onJwtTokenChanged(externalId: String, to: String?) { + print("❌ OSUserJwtConfig.onJwtTokenChanged \(externalId): \(to)") + changeNotifier.fire { listener in + listener.onJwtUpdated(externalId: externalId, to: to) + } + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index c7d0a1bdf..373e776a9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -97,7 +97,7 @@ import OneSignalNotifications @objc public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { - @objc public static let sharedInstance = OneSignalUserManagerImpl() + @objc public static let sharedInstance = OneSignalUserManagerImpl(jwtConfig: OSUserJwtConfig()) /** Convenience accessor. We access the push subscription model via the model store instead of via`user.pushSubscriptionModel`. @@ -125,6 +125,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var hasCalledStart = false private var jwtExpiredHandler: OSJwtExpiredHandler? + let jwtConfig: OSUserJwtConfig var user: OSUserInternal { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { @@ -150,8 +151,6 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { propertiesModel: OSPropertiesModel(changeNotifier: OSEventProducer()), pushSubscriptionModel: OSSubscriptionModel(type: .push, address: nil, subscriptionId: nil, reachable: false, isDisabled: true, changeNotifier: OSEventProducer())) - @objc public var requiresUserAuth = false - // User State Observer private var _userStateChangesObserver: OSObservable? var userStateChangesObserver: OSObservable { @@ -184,7 +183,8 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var identityExecutor: OSIdentityOperationExecutor? var subscriptionExecutor: OSSubscriptionOperationExecutor? - private override init() { + private init(jwtConfig: OSUserJwtConfig) { + self.jwtConfig = jwtConfig self.identityModelStoreListener = OSIdentityModelStoreListener(store: identityModelStore) self.propertiesModelStoreListener = OSPropertiesModelStoreListener(store: propertiesModelStore) self.subscriptionModelStoreListener = OSSubscriptionModelStoreListener(store: subscriptionModelStore) @@ -605,6 +605,14 @@ extension OneSignalUserManagerImpl { } } +// MARK: - JWT + +extension OneSignalUserManagerImpl { + @objc public func setRequiresUserAuth(_ required: Bool) { + jwtConfig.isRequired = required + } +} + extension OneSignalUserManagerImpl: OSUser { public func onJwtExpired(expiredHandler: @escaping OSJwtExpiredHandler) { jwtExpiredHandler = expiredHandler From 7af6373444827846168d9ba22ab3ad22abfe3c67 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 16 Aug 2024 21:16:55 -0700 Subject: [PATCH 04/56] mock remote params returning use JWT = true --- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 0d068e82f..4b1b077c5 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -632,10 +632,12 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { NSString *userId = nil; [OneSignalCoreImpl.sharedClient executeRequest:[OSRequestGetIosParams withUserId:userId appId:appId] onSuccess:^(NSDictionary *result) { - - if (result[IOS_REQUIRES_USER_ID_AUTHENTICATION]) { - OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = [result[IOS_REQUIRES_USER_ID_AUTHENTICATION] boolValue]; - } + + // TODO: JWT 🔐 Mock it for now to always be true + OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = true; +// if (result[IOS_REQUIRES_USER_ID_AUTHENTICATION]) { +// OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = [result[IOS_REQUIRES_USER_ID_AUTHENTICATION] boolValue]; +// } if (result[IOS_USES_PROVISIONAL_AUTHORIZATION] != (id)[NSNull null]) { [OneSignalUserDefaults.initStandard saveBoolForKey:OSUD_USES_PROVISIONAL_PUSH_AUTHORIZATION withValue:[result[IOS_USES_PROVISIONAL_AUTHORIZATION] boolValue]]; From ec84c2c4b243e67d8911b90937c1d8db91a6eb2f Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 16 Aug 2024 21:10:16 -0700 Subject: [PATCH 05/56] Add JWT constants to Common Defines * Add `OSUD_USE_IDENTITY_VERIFICATION` flag to OneSignalCommonDefines --- .../OneSignalCore/Source/OneSignalCommonDefines.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 9bcf51a54..a9c2dd312 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -77,6 +77,13 @@ // Remote Params #define OSUD_LOCATION_ENABLED @"OSUD_LOCATION_ENABLED" #define OSUD_REQUIRES_USER_PRIVACY_CONSENT @"OSUD_REQUIRES_USER_PRIVACY_CONSENT" + +/* Identity Verification */ +// TODO: JWT 🔐 Figure out the key below and may need to relate to existing key IOS_REQUIRES_USER_ID_AUTHENTICATION +#define OSUD_USE_IDENTITY_VERIFICATION @"OSUD_USE_IDENTITY_VERIFICATION" +#define OS_JWT_BEARER_TOKEN @"OS_JWT_BEARER_TOKEN" +#define OS_JWT_TOKEN_INVALID @"OS_JWT_TOKEN_INVALID" + // Remote Params - Receive Receipts #define OSUD_RECEIVE_RECEIPTS_ENABLED @"OS_ENABLE_RECEIVE_RECEIPTS" // * OSUD_RECEIVE_RECEIPTS_ENABLED // Outcomes @@ -128,7 +135,7 @@ #define IOS_USES_PROVISIONAL_AUTHORIZATION @"uses_provisional_auth" #define IOS_REQUIRES_EMAIL_AUTHENTICATION @"require_email_auth" #define IOS_REQUIRES_SMS_AUTHENTICATION @"require_sms_auth" -#define IOS_REQUIRES_USER_ID_AUTHENTICATION @"require_user_id_auth" +#define IOS_REQUIRES_USER_ID_AUTHENTICATION @"require_user_id_auth" // TODO: JWT 🔐 Figure out the key, also think about needing to migrate this value #define IOS_RECEIVE_RECEIPTS_ENABLE @"receive_receipts_enable" #define IOS_OUTCOMES_V2_SERVICE_ENABLE @"v2_enabled" #define IOS_LOCATION_SHARED @"location_shared" From d3eb08d66e61b8cfdd7732716dd4936e50e8c446 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 16 Aug 2024 21:16:28 -0700 Subject: [PATCH 06/56] public updateUserJwt method public updateUserJwt method --- .../Source/OneSignalUserManagerImpl.swift | 13 ++++++++++++- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 7 +++++++ iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h | 1 + 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 373e776a9..72f36758e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -30,13 +30,14 @@ import OneSignalOSCore import OneSignalNotifications /** - Public-facing API to access the User Manager. + Internal API to access the User Manager. */ @objc protocol OneSignalUserManager { // swiftlint:disable identifier_name var User: OSUser { get } func login(externalId: String, token: String?) func logout() + func updateUserJwt(externalId: String, token: String) // Location func setLocation(latitude: Float, longitude: Float) // Purchase Tracking @@ -525,6 +526,16 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { updatePropertiesDeltas(property: .purchases, value: purchases) } + @objc + public func updateUserJwt(externalId: String, token: String) { + guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updateUserJwt") else { + return + } + OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_VERBOSE, message: "Update User JWT called with externalId: \(externalId) and token: \(token)") + + identityModelRepo.updateJwtToken(externalId: externalId, token: token) + } + private func fireJwtExpired() { guard let externalId = user.identityModel.externalId, let jwtExpiredHandler = self.jwtExpiredHandler else { return diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 4b1b077c5..5b49733bf 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -208,6 +208,13 @@ + (void)logout { [OneSignalUserManagerImpl.sharedInstance logout]; } ++ (void)updateUserJwt:(NSString * _Nonnull)externalId withToken:(NSString * _Nonnull)token { + if ([OneSignalConfigManager shouldAwaitAppIdAndLogMissingPrivacyConsentForMethod:@"updateUserJwt"]) { + return; + } + [OneSignalUserManagerImpl.sharedInstance updateUserJwtWithExternalId:externalId token:token]; +} + #pragma mark: Namespaces + (Class)Notifications { diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h b/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h index fd5f70c55..7315d413f 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h @@ -70,6 +70,7 @@ typedef void (^OSFailureBlock)(NSError* error); + (void)login:(NSString * _Nonnull)externalId; + (void)login:(NSString * _Nonnull)externalId withToken:(NSString * _Nullable)token NS_SWIFT_NAME(login(externalId:token:)); ++ (void)updateUserJwt:(NSString * _Nonnull)externalId withToken:(NSString * _Nonnull)token; + (void)logout; #pragma mark Notifications From ca6b132a31b7f6afdc909ca44465de70946bb955 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 20 Aug 2024 09:24:43 -0700 Subject: [PATCH 07/56] Helper methods to add auth headers --- .../Source/Requests/OSUserRequest.swift | 56 ++++++++++++++++--- 1 file changed, 49 insertions(+), 7 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift index 133473ba8..547e52b42 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift @@ -34,17 +34,47 @@ protocol OSUserRequest: OneSignalRequest, NSCoding { } internal extension OneSignalRequest { - func addJWTHeader(identityModel: OSIdentityModel) { -// guard let token = identityModel.jwtBearerToken else { -// return -// } -// var additionalHeaders = self.additionalHeaders ?? [String:String]() -// additionalHeaders["Authorization"] = "Bearer \(token)" -// self.additionalHeaders = additionalHeaders + /** + Returns the alias pair to use to send this request for. Defaults to Onesignal Id, unless Identity Verification is on. + */ + func getAlias(identityModel: OSIdentityModel) -> (label: String, id: String?) { + var label = OS_ONESIGNAL_ID + var id = identityModel.onesignalId + if OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired == true { + label = OS_EXTERNAL_ID + id = identityModel.externalId + } + return (label, id) + } + + /** + Adds JWT token to header if valid, regardless of requirement. + Returns false if JWT requirement is unknown, or turned on but the token is missing or invalid. + + | | unknown | on | off | + | --------------- | -------------- | ------- | ------- | + | hasToken | | ✔️ | ✔️ | + | noToken | | | ✔️ | + | --------------- | -------------- | ------- | ------- | + */ + func addJWTHeaderIsValid(identityModel: OSIdentityModel) -> Bool { + let tokenIsValid = identityModel.isJwtValid() + let required = OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired + let canBeSent = (required == false) || (required == true && tokenIsValid) + if canBeSent && tokenIsValid, + let token = identityModel.jwtBearerToken + { + // Add the JWT token if it is valid, regardless of requirements + var additionalHeaders = self.additionalHeaders ?? [String: String]() + additionalHeaders["Authorization"] = "Bearer \(token)" + self.additionalHeaders = additionalHeaders + } + return canBeSent } /** Returns if the `OneSignal-Subscription-Id` header was added successfully. */ func addPushSubscriptionIdToAdditionalHeaders() -> Bool { + _ = addPushToken() if let pushSubscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId { var additionalHeaders = self.additionalHeaders ?? [String: String]() additionalHeaders["OneSignal-Subscription-Id"] = pushSubscriptionId @@ -54,4 +84,16 @@ internal extension OneSignalRequest { return false } } + + /** Returns if the `Device-Auth-Push-Token` header was added successfully. */ + private func addPushToken() -> Bool { + if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address { + var additionalHeaders = self.additionalHeaders ?? [String: String]() + additionalHeaders["Device-Auth-Push-Token"] = "Basic \(token)" + self.additionalHeaders = additionalHeaders + return true + } else { + return false + } + } } From f0cfe5f1bb78238aa92bb29bfac7959fac7864e3 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 19 Aug 2024 20:43:50 -0700 Subject: [PATCH 08/56] Request objects - update to consider JWT * User request instances already have a `prepareForExecution()` method that indicates if this request can be sent yet. For example, does the onesignal ID exist to make this request. * Use this existing method to check for auth and block sending this request if auth is required but invalid, or auth is unknown, etc. * OSRequestFetchUser will have onesignalId as a property, regardless of JWT on or off. The aliasLabel and aliasId pair is already expected to be onesignal ID, so make it explicit. * Keeping the onesignal ID can be used to know if the user has actually been created on the server and to check for the post-create cool-off period. The request will be translated to use the onesignal ID or the external ID based on JWT on/off. Requests Updated: * OSRequestCreateUser * OSRequestFetchUser * OSRequestAddAliases * OSRequestRemoveAlias * OSRequestUpdateProperties Requests Not Allowed with Identity Verification: * OSRequestFetchIdentityBySubscription * OSRequestIdentifyUser --- .../Source/Executors/OSUserExecutor.swift | 10 ++--- .../Source/OneSignalUserManagerImpl.swift | 2 +- .../Source/Requests/OSRequestAddAliases.swift | 21 +++++----- .../OSRequestCreateSubscription.swift | 21 +++++----- .../Source/Requests/OSRequestCreateUser.swift | 7 +++- ...OSRequestFetchIdentityBySubscription.swift | 21 +++++----- .../Source/Requests/OSRequestFetchUser.swift | 38 ++++++++++--------- .../Requests/OSRequestIdentifyUser.swift | 20 +++++----- .../Requests/OSRequestRemoveAlias.swift | 20 ++++++---- .../Requests/OSRequestUpdateProperties.swift | 24 ++++++------ .../OSRequestUpdateSubscription.swift | 1 + 11 files changed, 101 insertions(+), 84 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 985342f6e..f22e41a95 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -254,7 +254,7 @@ extension OSUserExecutor { let identity = request.parameters?["identity"] as? [String: String], let onesignalId = request.identityModel.onesignalId, identity[OS_EXTERNAL_ID] != nil { - self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModel) + self.fetchUser(onesignalId: onesignalId, identityModel: request.identityModel) } else { self.executePendingRequests() } @@ -314,7 +314,7 @@ extension OSUserExecutor { return } - self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModel) + self.fetchUser(onesignalId: onesignalId, identityModel: request.identityModel) } } onFailure: { error in OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor executeFetchIdentityBySubscriptionRequest failed with error: \(error.debugDescription)") @@ -375,7 +375,7 @@ extension OSUserExecutor { if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) { // Add onesignal ID to new records because an immediate fetch may not return the newly-applied external ID self.newRecordsState.add(onesignalId, true) - self.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: request.identityModelToUpdate) + self.fetchUser(onesignalId: onesignalId, identityModel: request.identityModelToUpdate) } else { self.executePendingRequests() } @@ -417,8 +417,8 @@ extension OSUserExecutor { } } - func fetchUser(aliasLabel: String, aliasId: String, identityModel: OSIdentityModel, onNewSession: Bool = false) { - let request = OSRequestFetchUser(identityModel: identityModel, aliasLabel: aliasLabel, aliasId: aliasId, onNewSession: onNewSession) + func fetchUser(onesignalId: String, identityModel: OSIdentityModel, onNewSession: Bool = false) { + let request = OSRequestFetchUser(identityModel: identityModel, onesignalId: onesignalId, onNewSession: onNewSession) appendToQueue(request) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 72f36758e..5a2736a11 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -566,7 +566,7 @@ extension OneSignalUserManagerImpl { // Fetch the user's data if there is a onesignal_id if let onesignalId = onesignalId { - userExecutor!.fetchUser(aliasLabel: OS_ONESIGNAL_ID, aliasId: onesignalId, identityModel: user.identityModel, onNewSession: true) + userExecutor!.fetchUser(onesignalId: onesignalId, identityModel: user.identityModel, onNewSession: true) } else { // It is possible to init a user from cache who is missing the onesignalId // This can happen if any createUser or identifyUser requests are cached diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift index c52a84e20..89a683d3f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift @@ -38,18 +38,21 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel let aliases: [String: String] - /// requires a `onesignal_id` to send this request + /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let appId = OneSignalConfigManager.getAppId() - { - self.addJWTHeader(identityModel: identityModel) - self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity" - return true - } else { + let alias = getAlias(identityModel: identityModel) + guard + let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let aliasIdToUse = alias.id, + let appId = OneSignalConfigManager.getAppId(), + addJWTHeaderIsValid(identityModel: identityModel) + else { return false } + + self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/identity" + return true } init(aliases: [String: String], identityModel: OSIdentityModel) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift index 51b383d6e..234f1ef14 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift @@ -43,18 +43,21 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest { var subscriptionModel: OSSubscriptionModel var identityModel: OSIdentityModel - // Need the onesignal_id of the user + /// Needs the `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let appId = OneSignalConfigManager.getAppId() - { - self.addJWTHeader(identityModel: identityModel) - self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/subscriptions" - return true - } else { + let alias = getAlias(identityModel: identityModel) + guard + let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let aliasIdToUse = alias.id, + let appId = OneSignalConfigManager.getAppId(), + addJWTHeaderIsValid(identityModel: identityModel) + else { return false } + + self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/subscriptions" + return true } init(subscriptionModel: OSSubscriptionModel, identityModel: OSIdentityModel) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift index ef4e4a5e3..130682567 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift @@ -45,6 +45,7 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest { var pushSubscriptionModel: OSSubscriptionModel? var originalPushToken: String? + // TODO: JWT 🔐 confirm existence of external ID is already addressed before getting here /// Checks if the subscription ID can be accessed, if a subscription is being included in the request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { guard let appId = OneSignalConfigManager.getAppId() else { @@ -59,8 +60,12 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest { return false } + guard addJWTHeaderIsValid(identityModel: identityModel) else { + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the create user request yet due to auth.") + return false + } + _ = self.addPushSubscriptionIdToAdditionalHeaders() - self.addJWTHeader(identityModel: identityModel) self.path = "apps/\(appId)/users" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift index e26c7855d..7cf4d7268 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchIdentityBySubscription.swift @@ -39,22 +39,19 @@ class OSRequestFetchIdentityBySubscription: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel var pushSubscriptionModel: OSSubscriptionModel + /// Only send this request if Identity Verification is off. func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - // newRecordsState is unused for this request - guard let appId = OneSignalConfigManager.getAppId() else { - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the FetchIdentityBySubscription request due to null app ID.") + guard + let appId = OneSignalConfigManager.getAppId(), + let subscriptionId = pushSubscriptionModel.subscriptionId, + OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired == false + else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "Cannot generate the FetchIdentityBySubscription request.") return false } - if let subscriptionId = pushSubscriptionModel.subscriptionId { - self.path = "apps/\(appId)/subscriptions/\(subscriptionId)/user/identity" - return true - } else { - // This is an error, and should never happen - OneSignalLog.onesignalLog(.LL_ERROR, message: "Cannot generate the FetchIdentityBySubscription request due to null subscriptionId.") - self.path = "" - return false - } + self.path = "apps/\(appId)/subscriptions/\(subscriptionId)/user/identity" + return true } init(identityModel: OSIdentityModel, pushSubscriptionModel: OSSubscriptionModel) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift index 2e03e0c3e..bab458d52 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift @@ -40,35 +40,39 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { } let identityModel: OSIdentityModel - let aliasLabel: String - let aliasId: String let onNewSession: Bool + /// This should always be `OS_ONESIGNAL_ID` even with JWT on, as a way to know if the user has been created on the server and for the post-create cool off period + let onesignalId: String + + // TODO: JWT 🔐 Is external ID already handled by this time? Or do we need to check the alias here? + /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - guard let appId = OneSignalConfigManager.getAppId(), - newRecordsState.canAccess(aliasId) + let alias = getAlias(identityModel: identityModel) + guard + let aliasIdToUse = alias.id, + let appId = OneSignalConfigManager.getAppId(), + newRecordsState.canAccess(onesignalId), + addJWTHeaderIsValid( identityModel: identityModel) else { - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request for \(aliasLabel): \(aliasId) yet.") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request for \(alias) yet.") return false } - self.addJWTHeader(identityModel: identityModel) - self.path = "apps/\(appId)/users/by/\(aliasLabel)/\(aliasId)" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)" return true } - init(identityModel: OSIdentityModel, aliasLabel: String, aliasId: String, onNewSession: Bool) { + init(identityModel: OSIdentityModel, onesignalId: String, onNewSession: Bool) { self.identityModel = identityModel - self.aliasLabel = aliasLabel - self.aliasId = aliasId + self.onesignalId = onesignalId self.onNewSession = onNewSession - self.stringDescription = "" + self.stringDescription = "" super.init() self.method = GET } func encode(with coder: NSCoder) { - coder.encode(aliasLabel, forKey: "aliasLabel") - coder.encode(aliasId, forKey: "aliasId") + coder.encode(onesignalId, forKey: "onesignalId") coder.encode(identityModel, forKey: "identityModel") coder.encode(onNewSession, forKey: "onNewSession") coder.encode(method.rawValue, forKey: "method") // Encodes as String @@ -78,8 +82,7 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { required init?(coder: NSCoder) { guard let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel, - let aliasLabel = coder.decodeObject(forKey: "aliasLabel") as? String, - let aliasId = coder.decodeObject(forKey: "aliasId") as? String, + let onesignalId = coder.decodeObject(forKey: "onesignalId") as? String, let rawMethod = coder.decodeObject(forKey: "method") as? UInt32, let timestamp = coder.decodeObject(forKey: "timestamp") as? Date else { @@ -87,10 +90,9 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { return nil } self.identityModel = identityModel - self.aliasLabel = aliasLabel - self.aliasId = aliasId + self.onesignalId = onesignalId self.onNewSession = coder.decodeBool(forKey: "onNewSession") - self.stringDescription = "" + self.stringDescription = "" super.init() self.method = HTTPMethod(rawValue: rawMethod) self.timestamp = timestamp diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift index b63c275a2..32806bb13 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestIdentifyUser.swift @@ -48,20 +48,20 @@ class OSRequestIdentifyUser: OneSignalRequest, OSUserRequest { let aliasId: String /// requires a `onesignal_id` to send this request + /// Only send this request if Identity Verification is off func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let onesignalId = identityModelToIdentify.onesignalId, - newRecordsState.canAccess(onesignalId), - let appId = OneSignalConfigManager.getAppId() - { - self.addJWTHeader(identityModel: identityModelToIdentify) - self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity" - return true - } else { - // self.path is non-nil, so set to empty string - self.path = "" + guard + let onesignalId = identityModelToIdentify.onesignalId, + newRecordsState.canAccess(onesignalId), + let appId = OneSignalConfigManager.getAppId(), + OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired == false + else { OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the Identify User request yet.") return false } + + self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity" + return true } /** diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift index adf98e568..f83800a4a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift @@ -38,17 +38,21 @@ class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { let labelToRemove: String var identityModel: OSIdentityModel + /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let appId = OneSignalConfigManager.getAppId() - { - self.addJWTHeader(identityModel: identityModel) - self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)/identity/\(labelToRemove)" - return true - } else { + let alias = getAlias(identityModel: identityModel) + guard + let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let aliasIdToUse = alias.id, + let appId = OneSignalConfigManager.getAppId(), + addJWTHeaderIsValid(identityModel: identityModel) + else { return false } + + self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/identity/\(labelToRemove)" + return true } init(labelToRemove: String, identityModel: OSIdentityModel) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift index b46ff3894..b4f99116e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift @@ -37,20 +37,22 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { var identityModel: OSIdentityModel - // TODO: Decide if addPushSubscriptionIdToAdditionalHeadersIfNeeded should block. - // Note Android adds it to requests, if the push sub ID exists + /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let appId = OneSignalConfigManager.getAppId() - { - _ = self.addPushSubscriptionIdToAdditionalHeaders() - self.addJWTHeader(identityModel: identityModel) - self.path = "apps/\(appId)/users/by/\(OS_ONESIGNAL_ID)/\(onesignalId)" - return true - } else { + let alias = getAlias(identityModel: identityModel) + guard + let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let aliasIdToUse = alias.id, + let appId = OneSignalConfigManager.getAppId(), + addJWTHeaderIsValid(identityModel: identityModel) + else { return false } + + _ = self.addPushSubscriptionIdToAdditionalHeaders() + self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)" + return true } init(params: [String: Any], identityModel: OSIdentityModel) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift index 39fba8c5c..d13ecbc68 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift @@ -42,6 +42,7 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { // Need the subscription_id func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { + addPushSubscriptionIdToAdditionalHeaders() if let subscriptionId = subscriptionModel.subscriptionId, newRecordsState.canAccess(subscriptionId), let appId = OneSignalConfigManager.getAppId() From 2d1b3178bccfa11dfb25b27082a21e13cc526c95 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 20 Aug 2024 09:05:04 -0700 Subject: [PATCH 09/56] Update Event Producer to accept multiple subscribers --- .../Source/OneSignalCommonDefines.h | 1 + .../Source/OSEventProducer.swift | 25 +++++++++++-------- .../OneSignalOSCore/Source/OSModelStore.swift | 6 ++--- .../Source/OSModelStoreListener.swift | 6 +++-- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index a9c2dd312..c3ef4056b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -326,6 +326,7 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_PUSH_SUBSCRIPTION_MODEL_KEY @"OS_PUSH_SUBSCRIPTION_MODEL_KEY" #define OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY @"OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY" #define OS_SUBSCRIPTION_MODEL_STORE_KEY @"OS_SUBSCRIPTION_MODEL_STORE_KEY" +#define OS_MODEL_STORE_LISTENER_POSTFIX @"_LISTENER" // Deltas #define OS_ADD_ALIAS_DELTA @"OS_ADD_ALIAS_DELTA" diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSEventProducer.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSEventProducer.swift index fd84d34c2..9f206d159 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSEventProducer.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSEventProducer.swift @@ -29,24 +29,27 @@ import Foundation import OneSignalCore public class OSEventProducer: NSObject { - // Not an array as there is at most 1 subsriber per OSEventProducer anyway - var subscriber: THandler? + private var subscribers: [String: THandler] = [:] + private let lock = NSLock() - public func subscribe(_ handler: THandler) { - // TODO: UM do we want to synchronize on subscribers - subscriber = handler // TODO: UM style, implicit or explicit self? + public func subscribe(_ handler: THandler, key: String) { + lock.withLock { + subscribers[key] = handler + } } - public func unsubscribe(_ handler: THandler) { + public func unsubscribe(_ handler: THandler, key: String) { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSEventProducer.unsubscribe() called with handler: \(handler)") - // TODO: UM do we want to synchronize on subscribers - subscriber = nil + lock.withLock { + subscribers.removeValue(forKey: key) + } } public func fire(callback: (THandler) -> Void) { - // dump(subscribers) -> uncomment for more verbose log during testing - if let subscriber = subscriber { - callback(subscriber) + lock.withLock { + for subscriber in subscribers.values { + callback(subscriber) + } } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStore.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStore.swift index 64a2f832c..c25691ffa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStore.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStore.swift @@ -47,7 +47,7 @@ open class OSModelStore: NSObject { // listen for changes to the models for model in self.models.values { - model.changeNotifier.subscribe(self) + model.changeNotifier.subscribe(self, key: storeKey) } } @@ -96,7 +96,7 @@ open class OSModelStore: NSObject { OneSignalUserDefaults.initShared().saveCodeableData(forKey: self.storeKey, withValue: self.models) // listen for changes to this model - model.changeNotifier.subscribe(self) + model.changeNotifier.subscribe(self, key: storeKey) guard !hydrating else { return @@ -121,7 +121,7 @@ open class OSModelStore: NSObject { OneSignalUserDefaults.initShared().saveCodeableData(forKey: self.storeKey, withValue: self.models) // no longer listen for changes to this model - model.changeNotifier.unsubscribe(self) + model.changeNotifier.unsubscribe(self, key: storeKey) self.changeSubscription.fire { modelStoreListener in modelStoreListener.onRemoved(model) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift index 540dcca40..cb5bb2ed2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift @@ -44,11 +44,13 @@ public protocol OSModelStoreListener: OSModelStoreChangedHandler { extension OSModelStoreListener { public func start() { - store.changeSubscription.subscribe(self) + let key = store.storeKey + OS_MODEL_STORE_LISTENER_POSTFIX + store.changeSubscription.subscribe(self, key: key) } public func close() { - store.changeSubscription.unsubscribe(self) + let key = store.storeKey + OS_MODEL_STORE_LISTENER_POSTFIX + store.changeSubscription.unsubscribe(self, key: key) } public func onAdded(_ model: OSModel) { From d81c4602d3405317f0f66c9207de89c9d5c2eff6 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 20 Aug 2024 10:44:16 -0700 Subject: [PATCH 10/56] Add JWT Token to Identity Model * Identity Model Store Listener will ignore it so no Deltas will be enqueued --- .../OneSignalUser/Source/OSIdentityModel.swift | 18 ++++++++++++++++-- .../Source/OSIdentityModelStoreListener.swift | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift index 6e70b5057..c8d3c62c0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift @@ -42,8 +42,20 @@ class OSIdentityModel: OSModel { var aliases: [String: String] = [:] private let aliasesLock = NSRecursiveLock() - // TODO: We need to make this token secure - public var jwtBearerToken: String? + // MARK: - JWT + + public var jwtBearerToken: String? { + didSet { + guard jwtBearerToken != oldValue else { + return + } + self.set(property: OS_JWT_BEARER_TOKEN, newValue: jwtBearerToken) + } + } + + func isJwtValid() -> Bool { + return jwtBearerToken != nil && jwtBearerToken != "" && jwtBearerToken != OS_JWT_TOKEN_INVALID + } // MARK: - Initialization @@ -57,6 +69,7 @@ class OSIdentityModel: OSModel { aliasesLock.withLock { super.encode(with: coder) coder.encode(aliases, forKey: "aliases") + coder.encode(jwtBearerToken, forKey: OS_JWT_BEARER_TOKEN) } } @@ -66,6 +79,7 @@ class OSIdentityModel: OSModel { // log error return nil } + self.jwtBearerToken = coder.decodeObject(forKey: OS_JWT_BEARER_TOKEN) as? String self.aliases = aliases } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift index 4edabb321..517946abe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift @@ -48,8 +48,8 @@ class OSIdentityModelStoreListener: OSModelStoreListener { Determines if this update is adding aliases or removing aliases. */ func getUpdateModelDelta(_ args: OSModelChangedArgs) -> OSDelta? { - // TODO: Let users call addAliases with "" IDs? If so, this will change... guard + args.property == "aliases", // avoids JWT token updates let aliasesDict = args.newValue as? [String: String], let (_, id) = aliasesDict.first else { From 2774c3483baf7e5dfce192be6dc7c7ff3c582ec9 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 20 Aug 2024 10:52:33 -0700 Subject: [PATCH 11/56] Identity Model Repo listens for JWT changes * As a refresher, the Identity Model Repo holds all Identity Models present during an app run, which can include past users that have pending updates, while the Identity Model Store contains only the current user's Identity Model. * Let the OSIdentityModelRepo be the listener for changes to JWT token updates, and it needs to pass on that information to the User Manager who manages the JWT Config and fires other listeners of token changes. --- .../Source/OneSignalCommonDefines.h | 1 + .../Source/OSIdentityModelRepo.swift | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index c3ef4056b..7a9d55e1e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -327,6 +327,7 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY @"OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY" #define OS_SUBSCRIPTION_MODEL_STORE_KEY @"OS_SUBSCRIPTION_MODEL_STORE_KEY" #define OS_MODEL_STORE_LISTENER_POSTFIX @"_LISTENER" +#define OS_IDENTITY_MODEL_REPO @"OS_IDENTITY_MODEL_REPO" // Deltas #define OS_ADD_ALIAS_DELTA @"OS_ADD_ALIAS_DELTA" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index 164f9d63a..d8e5bf1aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -26,6 +26,7 @@ */ import Foundation +import OneSignalCore import OneSignalOSCore /** @@ -45,6 +46,8 @@ class OSIdentityModelRepo { func add(model: OSIdentityModel) { lock.withLock { models[model.modelId] = model + // listen for changes to model's JWT Token + model.changeNotifier.subscribe(self, key: OS_IDENTITY_MODEL_REPO) } } @@ -54,6 +57,55 @@ class OSIdentityModelRepo { } } + func get(externalId: String) -> OSIdentityModel? { + lock.withLock { + for model in models.values { + if model.externalId == externalId { + return model + } + } + return nil + } + } + + /** + There may be multiple Identity Models with the same external ID, so update them all. + This can be optimized in the future to re-use an Identity Model if multiple logins are made for the same user. + */ + func updateJwtToken(externalId: String, token: String) { + var found = false + lock.withLock { + for model in models.values { + if model.externalId == externalId { + model.jwtBearerToken = token + found = true + } + } + } + if !found { + OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_ERROR, message: "Update User JWT called for external ID \(externalId) that does not exist") + } + } +} + +extension OSIdentityModelRepo: OSModelChangedHandler { + /** + Listen for updates to the JWT Token and notify the User Manager of this change. + */ + public func onModelUpdated(args: OSModelChangedArgs, hydrating: Bool) { + guard + args.property == OS_JWT_BEARER_TOKEN, + let model = args.model as? OSIdentityModel, + let externalId = model.externalId, + let token = args.newValue as? String + else { + return + } + print("❌ OSIdentityModelRepo onModelUpdated for \(externalId): \(token)") + OneSignalUserManagerImpl.sharedInstance.jwtConfig.onJwtTokenChanged(externalId: externalId, to: token) + } +} + extension OSIdentityModelRepo: OSLoggable { func logSelf() { print( From e7059deeff8a34b581bc3ac4312b3944825c647b Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 20 Aug 2024 13:09:18 -0700 Subject: [PATCH 12/56] Update Executors to consider JWT Updates to: * OSPropertyOperationExecutor * OSIdentityOperationExecutor * OSUserExecutor Updates to OSSubscriptionOperationExecutor is still to be done. --- .../Source/OneSignalCommonDefines.h | 3 + .../OSIdentityOperationExecutor.swift | 186 +++++++++++++----- .../OSPropertyOperationExecutor.swift | 128 +++++++++--- .../OSSubscriptionOperationExecutor.swift | 26 ++- .../Source/Executors/OSUserExecutor.swift | 139 ++++++++++++- .../Source/OneSignalUserManagerImpl.swift | 8 +- 6 files changed, 396 insertions(+), 94 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 7a9d55e1e..f5c107876 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -343,15 +343,18 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_OPERATION_REPO_DELTA_QUEUE_KEY @"OS_OPERATION_REPO_DELTA_QUEUE_KEY" // User Executor +#define OS_USER_EXECUTOR @"OS_USER_EXECUTOR" #define OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY @"OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY" #define OS_USER_EXECUTOR_TRANSFER_SUBSCRIPTION_REQUEST_QUEUE_KEY @"OS_USER_EXECUTOR_TRANSFER_SUBSCRIPTION_REQUEST_QUEUE_KEY" // Identity Executor +#define OS_IDENTITY_EXECUTOR @"OS_IDENTITY_EXECUTOR" #define OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY" #define OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY" #define OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY" // Property Executor +#define OS_PROPERTIES_EXECUTOR @"OS_PROPERTIES_EXECUTOR" #define OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY" #define OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 12087d9cb..609cc486b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -35,12 +35,16 @@ class OSIdentityOperationExecutor: OSOperationExecutor { var addRequestQueue: [OSRequestAddAliases] = [] var removeRequestQueue: [OSRequestRemoveAlias] = [] let newRecordsState: OSNewRecordsState + let jwtConfig: OSUserJwtConfig // The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global()) - init(newRecordsState: OSNewRecordsState) { + init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) { self.newRecordsState = newRecordsState + self.jwtConfig = jwtConfig + self.jwtConfig.subscribe(self, key: OS_IDENTITY_EXECUTOR) + print("❌ OSIdentityOperationExecutor init(\(jwtConfig.isRequired))") // Read unfinished deltas and requests from cache, if any... uncacheDeltas() uncacheAddAliasRequests() @@ -48,69 +52,99 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { - // Hook each uncached Delta to the model in the store - for (index, delta) in deltaQueue.enumerated().reversed() { - if let modelInStore = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.model.modelId) { - // The model exists in the repo, set it to be the Delta's model - delta.model = modelInStore - } else { - // The model does not exist, drop this Delta - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(delta)") - deltaQueue.remove(at: index) - } - } - self.deltaQueue = deltaQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) - } else { + guard var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor error encountered reading from cache for \(OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY)") + return + } + + // Hook each uncached Delta to the model in the store + for (index, delta) in deltaQueue.enumerated().reversed() { + if jwtConfig.isRequired == true, + (delta.model as? OSIdentityModel)?.externalId == nil + { + // remove if jwt is on but the model does not have external ID + deltaQueue.remove(at: index) + continue + } + + if let modelInStore = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.model.modelId) { + // The model exists in the repo, set it to be the Delta's model + delta.model = modelInStore + } else { + // The model does not exist, drop this Delta + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(delta)") + deltaQueue.remove(at: index) + } } + + self.deltaQueue = deltaQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) } private func uncacheAddAliasRequests() { - if var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] { - // Hook each uncached Request to the model in the store - for (index, request) in addRequestQueue.enumerated().reversed() { - if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { - // 1. The model exists in the repo, so set it to be the Request's models - request.identityModel = identityModel - } else if request.prepareForExecution(newRecordsState: newRecordsState) { - // 2. The request can be sent, add the model to the repo - OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) - } else { - // 3. The model do not exist AND this request cannot be sent, drop this Request - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") - addRequestQueue.remove(at: index) - } - } - self.addRequestQueue = addRequestQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) - } else { + guard var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor error encountered reading from cache for \(OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY)") + return + } + + // Hook each uncached Request to the model in the store + for (index, request) in addRequestQueue.enumerated().reversed() { + if jwtConfig.isRequired == true, + request.identityModel.externalId == nil + { + // remove if jwt is on but the model does not have external ID + addRequestQueue.remove(at: index) + continue + } + + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // 1. The model exists in the repo, so set it to be the Request's models + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // 2. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // 3. The model do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") + addRequestQueue.remove(at: index) + } } + + self.addRequestQueue = addRequestQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) } private func uncacheRemoveAliasRequests() { - if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] { - // Hook each uncached Request to the model in the store - for (index, request) in removeRequestQueue.enumerated().reversed() { - if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { - // 1. The model exists in the repo, so set it to be the Request's model - request.identityModel = identityModel - } else if request.prepareForExecution(newRecordsState: newRecordsState) { - // 2. The request can be sent, add the model to the repo - OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) - } else { - // 3. The model does not exist AND this request cannot be sent, drop this Request - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") - removeRequestQueue.remove(at: index) - } - } - self.removeRequestQueue = removeRequestQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) - } else { + guard var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor error encountered reading from cache for \(OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY)") + return + } + + // Hook each uncached Request to the model in the store + for (index, request) in removeRequestQueue.enumerated().reversed() { + if jwtConfig.isRequired == true, + request.identityModel.externalId == nil + { + // remove if jwt is on but the model does not have external ID + removeRequestQueue.remove(at: index) + continue + } + + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // 1. The model exists in the repo, so set it to be the Request's models + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // 2. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // 3. The model do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") + removeRequestQueue.remove(at: index) + } } + + self.removeRequestQueue = removeRequestQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) } func enqueueDelta(_ delta: OSDelta) { @@ -139,6 +173,11 @@ class OSIdentityOperationExecutor: OSOperationExecutor { continue } + // If JWT is on but the external ID does not exist, drop this Delta + if self.jwtConfig.isRequired == true, model.externalId == nil { + print("❌ \(delta) is Invalid with JWT, being dropped") + } + switch delta.name { case OS_ADD_ALIAS_DELTA: let request = OSRequestAddAliases(aliases: aliases, identityModel: model) @@ -294,6 +333,49 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } +extension OSIdentityOperationExecutor: OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + print("❌ OSIdentityOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") + // If auth changed from false or unknown to true, process requests + if to == true { + removeInvalidDeltasAndRequests() + } + } + + func onJwtUpdated(externalId: String, to: String?) { + print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + } + + private func removeInvalidDeltasAndRequests() { + self.dispatchQueue.async { + print("❌ OSIdentityOperationExecutor.removeInvalidDeltasAndRequests called") + for (index, delta) in self.deltaQueue.enumerated().reversed() { + if (delta.model as? OSIdentityModel)?.externalId == nil { + print(" \(delta) is Invalid, being removed") + self.deltaQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + + for (index, request) in self.addRequestQueue.enumerated().reversed() { + if request.identityModel.externalId == nil { + print(" \(request) is Invalid, being removed") + self.addRequestQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + + for (index, request) in self.removeRequestQueue.enumerated().reversed() { + if request.identityModel.externalId == nil { + print(" \(request) is Invalid, being removed") + self.removeRequestQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + } + } +} + extension OSIdentityOperationExecutor: OSLoggable { func logSelf() { print( diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 08670b04f..bea96ca0e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -65,12 +65,17 @@ class OSPropertyOperationExecutor: OSOperationExecutor { var deltaQueue: [OSDelta] = [] var updateRequestQueue: [OSRequestUpdateProperties] = [] let newRecordsState: OSNewRecordsState + let jwtConfig: OSUserJwtConfig // The property executor dispatch queue, serial. This synchronizes access to `deltaQueue` and `updateRequestQueue`. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSPropertyOperationExecutor", target: .global()) - init(newRecordsState: OSNewRecordsState) { + init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) { self.newRecordsState = newRecordsState + self.jwtConfig = jwtConfig + self.jwtConfig.subscribe(self, key: OS_PROPERTIES_EXECUTOR) + print("❌ OSPropertyOperationExecutor init(\(String(describing: jwtConfig.isRequired)))") + // Read unfinished deltas and requests from cache, if any... // Note that we should only have deltas for the current user as old ones are flushed.. uncacheDeltas() @@ -78,12 +83,21 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { + print("❌ OSPropertyOperationExecutor uncacheDeltas called") + if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { for (index, delta) in deltaQueue.enumerated().reversed() { - if OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) == nil { + guard let model = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { // The identity model does not exist, drop this Delta OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.init dropped: \(delta)") deltaQueue.remove(at: index) + continue + } + + // If JWT is on but the external ID does not exist, drop this Delta + if jwtConfig.isRequired == true, model.externalId == nil { + print("❌ removing \(delta)") + deltaQueue.remove(at: index) } } self.deltaQueue = deltaQueue @@ -91,29 +105,41 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor error encountered reading from cache for \(OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY)") } + print("❌ OSPropertyOperationExecutor uncacheDeltas done, \(self.deltaQueue)") } private func uncacheUpdateRequests() { - if var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] { - // Hook each uncached Request to the model in the store - for (index, request) in updateRequestQueue.enumerated().reversed() { - if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { - // 1. The identity model exist in the repo, set it to be the Request's model - request.identityModel = identityModel - } else if request.prepareForExecution(newRecordsState: newRecordsState) { - // 2. The request can be sent, add the model to the repo - OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) - } else { - // 3. The identitymodel do not exist AND this request cannot be sent, drop this Request - OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.init dropped: \(request)") - updateRequestQueue.remove(at: index) - } - } - self.updateRequestQueue = updateRequestQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) - } else { + print("❌ OSPropertyOperationExecutor uncacheUpdateRequests called") + + guard var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor error encountered reading from cache for \(OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY)") + return + } + + // Hook each uncached Request to the model in the store + for (index, request) in updateRequestQueue.enumerated().reversed() { + if jwtConfig.isRequired == true, + request.identityModel.externalId == nil + { + // remove if jwt is on but the model does not have external ID + updateRequestQueue.remove(at: index) + continue + } + + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // 1. The identity model exist in the repo, set it to be the Request's model + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // 2. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // 3. The identitymodel do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.init dropped: \(request)") + updateRequestQueue.remove(at: index) + } } + self.updateRequestQueue = updateRequestQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) } func enqueueDelta(_ delta: OSDelta) { @@ -129,9 +155,17 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } - /// The `deltaQueue` should only contain updates for one user. - /// Even when login -> addTag -> login -> addTag are called in immediate succession. + /** + The `deltaQueue` should typically only contain updates for one user + Even when login -> addTag -> login -> addTag are called in immediate succession. + However, when Identity Verification requirements are unknown, we keep deltas in the queue even when users switch. + */ func processDeltaQueue(inBackground: Bool) { + guard jwtConfig.isRequired != nil else { + print("❌ OSPropertyOperationExecutor processDeltaQueue returning early due to requiresAuth: \(String(describing: jwtConfig.isRequired))") + return + } + self.dispatchQueue.async { if self.deltaQueue.isEmpty { // Delta queue is empty but there may be pending requests @@ -140,7 +174,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSPropertyOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)") - // Holds mapping of identity model ID to the updates for it; there should only be one user + // Holds mapping of identity model ID to the updates for it var combinedProperties: [String: OSCombinedProperties] = [:] // 1. Combined deltas into a single OSCombinedProperties for every user @@ -150,14 +184,16 @@ class OSPropertyOperationExecutor: OSOperationExecutor { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)") continue } + + // If JWT is on but the external ID does not exist, drop this Delta + if self.jwtConfig.isRequired == true, identityModel.externalId == nil { + print("❌ \(delta) is Invalid with JWT, being dropped") + } + let combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] combinedProperties[identityModel.modelId] = self.combineProperties(existing: combinedSoFar, delta: delta) } - if combinedProperties.count > 1 { - OneSignalLog.onesignalLog(.LL_WARN, message: "OSPropertyOperationExecutor.combinedProperties contains \(combinedProperties.count) users") - } - // 2. Turn each OSCombinedProperties' data into a Request for (modelId, properties) in combinedProperties { guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(modelId) @@ -289,6 +325,44 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } +extension OSPropertyOperationExecutor: OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + print("❌ OSPropertyOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") + // If auth changed from false or unknown to true, process requests + if to == true { + removeInvalidDeltasAndRequests() + } + } + + func onJwtUpdated(externalId: String, to: String?) { + print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + } + + private func removeInvalidDeltasAndRequests() { + self.dispatchQueue.async { + print("❌ OSPropertyOperationExecutor.removeInvalidDeltasAndRequests called") + + for (index, delta) in self.deltaQueue.enumerated().reversed() { + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId), + identityModel.externalId == nil + { + print(" \(delta) is Invalid, being removed") + self.deltaQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + + for (index, request) in self.updateRequestQueue.enumerated().reversed() { + if request.identityModel.externalId == nil { + print(" \(request) is Invalid, being removed") + self.updateRequestQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + } + } +} + extension OSPropertyOperationExecutor: OSLoggable { func logSelf() { print( diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 68148cc6c..02d17554c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -37,12 +37,15 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { var updateRequestQueue: [OSRequestUpdateSubscription] = [] var subscriptionModels: [String: OSSubscriptionModel] = [:] let newRecordsState: OSNewRecordsState + let jwtConfig: OSUserJwtConfig // The Subscription executor dispatch queue, serial. This synchronizes access to the delta and request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSSubscriptionOperationExecutor", target: .global()) - init(newRecordsState: OSNewRecordsState) { + // TODO: JWT 🔐 Subscription Executor updates are still WIP + init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) { self.newRecordsState = newRecordsState + self.jwtConfig = jwtConfig // Read unfinished deltas and requests from cache, if any... uncacheDeltas() uncacheCreateSubscriptionRequests() @@ -195,15 +198,22 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { switch delta.name { case OS_ADD_SUBSCRIPTION_DELTA: // Only create the request if the identity model exists - if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) { - let request = OSRequestCreateSubscription( - subscriptionModel: subModel, - identityModel: identityModel - ) - self.addRequestQueue.append(request) - } else { + guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.processDeltaQueue dropped \(delta)") + continue } + + // If JWT is on but the external ID does not exist, drop this Delta + if self.jwtConfig.isRequired == true, identityModel.externalId == nil { + print("❌ \(delta) is Invalid with JWT, being dropped") + } + + let request = OSRequestCreateSubscription( + subscriptionModel: subModel, + identityModel: identityModel + ) + self.addRequestQueue.append(request) + case OS_REMOVE_SUBSCRIPTION_DELTA: let request = OSRequestDeleteSubscription( subscriptionModel: subModel diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index f22e41a95..f5a26b182 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -36,14 +36,20 @@ import OneSignalOSCore class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] private let newRecordsState: OSNewRecordsState + let jwtConfig: OSUserJwtConfig + /// Delay by the "cool down" period plus a buffer of a set amount of milliseconds private let flushDelayMilliseconds = Int(OP_REPO_POST_CREATE_DELAY_SECONDS * 1_000 + 200) // TODO: This could come from a config, plist, method, remote params /// The User executor dispatch queue, serial. This synchronizes access to the request queues. private let dispatchQueue = DispatchQueue(label: "OneSignal.OSUserExecutor", target: .global()) - init(newRecordsState: OSNewRecordsState) { + init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) { self.newRecordsState = newRecordsState + self.jwtConfig = jwtConfig + self.jwtConfig.subscribe(self, key: OS_USER_EXECUTOR) + print("❌ OSUserExecutor init requiresAuth: \(jwtConfig.isRequired)") + uncacheUserRequests() migrateTransferSubscriptionRequests() executePendingRequests() @@ -52,12 +58,19 @@ class OSUserExecutor { /// Read in requests from the cache, do not read in FetchUser requests as this is not needed. private func uncacheUserRequests() { var userRequestQueue: [OSUserRequest] = [] - + print(" OSUserExecutor uncacheUserRequests called") // Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any... if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] { + print(" OSUserExecutor uncacheUserRequests cachedQueue is \(cachedRequestQueue)") + // Hook each uncached Request to the right model reference for request in cachedRequestQueue { if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription { + // Remove this request if JWT is enabled + guard jwtConfig.isRequired != true else { + print(" uncacheUserRequests dropping request \(req)") + continue + } if let identityModel = getIdentityModel(req.identityModel.modelId) { // 1. The model exist in the repo, set it to be the Request's model // It is the current user or the model has already been processed @@ -69,6 +82,15 @@ class OSUserExecutor { userRequestQueue.append(req) } else if request.isKind(of: OSRequestCreateUser.self), let req = request as? OSRequestCreateUser { + + if jwtConfig.isRequired == true, + req.identityModel.externalId == nil + { + // Remove this request if there is no EUID + print(" uncacheUserRequests dropping request \(req)") + continue + } + if let identityModel = getIdentityModel(req.identityModel.modelId) { // 1. The model exist in the repo, set it to be the Request's model req.identityModel = identityModel @@ -80,6 +102,13 @@ class OSUserExecutor { } else if request.isKind(of: OSRequestIdentifyUser.self), let req = request as? OSRequestIdentifyUser { + // If JWT is enabled, we migrate this request into a Create User request + guard jwtConfig.isRequired != true else { + print(" uncacheUserRequests converting \(req) to createUser") + convertIdentifyUserToCreateUser(req) + continue + } + if let identityModelToIdentify = getIdentityModel(req.identityModelToIdentify.modelId), let identityModelToUpdate = getIdentityModel(req.identityModelToUpdate.modelId) { // 1. Both models exist in the repo, set it to be the Request's models @@ -107,6 +136,7 @@ class OSUserExecutor { } self.userRequestQueue = userRequestQueue OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + print(" OSUserExecutor uncacheUserRequests done, now has queue: \(self.userRequestQueue)") } /** @@ -125,6 +155,14 @@ class OSUserExecutor { } } + private func convertIdentifyUserToCreateUser(_ request: OSRequestIdentifyUser) { + if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.aliasId) { + self.createUser(OneSignalUserManagerImpl.sharedInstance.user) + } else { + self.createUser(aliasLabel: request.aliasLabel, aliasId: request.aliasId, identityModel: request.identityModelToUpdate) + } + } + private func getIdentityModel(_ modelId: String) -> OSIdentityModel? { return OneSignalUserManagerImpl.sharedInstance.getIdentityModel(modelId) } @@ -147,10 +185,34 @@ class OSUserExecutor { } } + /** + When Identity Verification is on, only `OSRequestCreateUser` and `OSRequestFetchUser` can be executed. + Other requests should already be removed or translated into an executable type by the time this method runs. + */ + private func executePendingRequestsWithAuth() { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequestsWithAuth called with queue \(self.userRequestQueue)") + + for request in self.userRequestQueue { + if request.isKind(of: OSRequestCreateUser.self), let createUserRequest = request as? OSRequestCreateUser { + self.executeCreateUserRequest(createUserRequest) + } else if request.isKind(of: OSRequestFetchUser.self), let fetchUserRequest = request as? OSRequestFetchUser { + self.executeFetchUserRequest(fetchUserRequest) + } else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") + self.removeFromQueue(request) + } + } + } + /** Requests are flushed after a delay when they need to wait for the "cool down" period to access a user or subscription after its creation. */ func executePendingRequests(withDelay: Bool = false) { + guard jwtConfig.isRequired != nil else { + print("❌ OSUserExecutor.executePendingRequests returning early due to unknown Identity Verification status.") + return + } + if withDelay { self.dispatchQueue.asyncAfter(deadline: .now() + .milliseconds(flushDelayMilliseconds)) { [weak self] in self?._executePendingRequests() @@ -163,7 +225,20 @@ class OSUserExecutor { } private func _executePendingRequests() { - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequests called with queue \(self.userRequestQueue)") + guard let requiresAuth = jwtConfig.isRequired else { + return + } + + if requiresAuth { + executePendingRequestsWithAuth() + } else { + executePendingRequestsWithoutAuth() + } + } + + private func executePendingRequestsWithoutAuth() { + // same as executePendingRequests currently + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.executePendingRequestsWithoutAuth called with queue \(self.userRequestQueue)") for request in self.userRequestQueue { // Return as soon as we reach an un-executable request @@ -188,6 +263,7 @@ class OSUserExecutor { return } else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") + self.removeFromQueue(request) } } } @@ -601,6 +677,63 @@ extension OSUserExecutor { } } +extension OSUserExecutor: OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + print("❌ OSUserExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") + // If auth changed from false or unknown to true, process requests + if to == true { + removeInvalidRequests() + } + self.executePendingRequests() + } + + func onJwtUpdated(externalId: String, to: String?) { + print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + } + + private func removeInvalidRequests() { + self.dispatchQueue.async { + print("❌ OSUserExecutor.removeInvalidRequests called") + + for request in self.userRequestQueue { + guard self.isRequestValidWithAuth(request) else { + print(" \(request) is Invalid, being removed") + self.userRequestQueue.removeAll(where: { $0 == request}) + continue + } + + if request.isKind(of: OSRequestIdentifyUser.self), let req = request as? OSRequestIdentifyUser { + print(" \(request) is IdentifyUser, being converted") + self.userRequestQueue.removeAll(where: { $0 == request}) + self.convertIdentifyUserToCreateUser(req) + } + } + + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + print(" OSUserExecutor.removeInvalidRequests done, \(self.userRequestQueue)") + } + } + + /// Returns if the Request is valid when Identity Verification is on + private func isRequestValidWithAuth(_ request: OSUserRequest) -> Bool { + if request.isKind(of: OSRequestFetchIdentityBySubscription.self) { + return false + } + if request.isKind(of: OSRequestCreateUser.self), + let createUserRequest = request as? OSRequestCreateUser, + createUserRequest.identityModel.externalId == nil + { + return false + } + if request.isKind(of: OSRequestFetchUser.self), + let fetchUserRequest = request as? OSRequestFetchUser, + fetchUserRequest.identityModel.externalId == nil { + return false + } + return true + } +} + extension OSUserExecutor: OSLoggable { func logSelf() { print( diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 5a2736a11..69e2e62aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -225,13 +225,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { // Setup the executors // The OSUserExecutor has to run first, before other executors - self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState) + self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) OSOperationRepo.sharedInstance.start() // Cannot initialize these executors in `init` as they reference the sharedInstance - let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState) - let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState) - let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState) + let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + let identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + let subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) self.propertyExecutor = propertyExecutor self.identityExecutor = identityExecutor self.subscriptionExecutor = subscriptionExecutor From 5c98665ce67100e062b3938666432a6fda6d942c Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 21 Aug 2024 17:41:04 -0700 Subject: [PATCH 13/56] Operation Repo - refactor and update for JWT * Refactor from a static shared instance to a instance managed by User Manager * Operation Repo does not flush while Identity Verification is unknown * Currently it is not going to process Deltas based on JWT. Executors will. --- .../Source/OneSignalCommonDefines.h | 1 + .../Source/OSModelStoreListener.swift | 10 +-- .../Source/OSOperationRepo.swift | 67 ++++++++++++++----- .../Source/Executors/OSUserExecutor.swift | 4 +- .../Source/OSIdentityModelStoreListener.swift | 4 +- .../OSPropertiesModelStoreListener.swift | 4 +- .../OSSubscriptionModelStoreListener.swift | 4 +- .../Source/OneSignalUserManagerImpl.swift | 24 ++++--- 8 files changed, 81 insertions(+), 37 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index f5c107876..30658efd4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -340,6 +340,7 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_UPDATE_SUBSCRIPTION_DELTA @"OS_UPDATE_SUBSCRIPTION_DELTA" // Operation Repo +#define OS_OPERATION_REPO @"OS_OPERATION_REPO" #define OS_OPERATION_REPO_DELTA_QUEUE_KEY @"OS_OPERATION_REPO_DELTA_QUEUE_KEY" // User Executor diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift index cb5bb2ed2..5602d1dc1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSModelStoreListener.swift @@ -31,9 +31,11 @@ import OneSignalCore public protocol OSModelStoreListener: OSModelStoreChangedHandler { associatedtype TModel: OSModel + var operationRepo: OSOperationRepo { get } + var store: OSModelStore { get } - init(store: OSModelStore) + init(store: OSModelStore, operationRepo: OSOperationRepo) func getAddModelDelta(_ model: TModel) -> OSDelta? @@ -59,13 +61,13 @@ extension OSModelStoreListener { return } if let delta = getAddModelDelta(addedModel) { - OSOperationRepo.sharedInstance.enqueueDelta(delta) + operationRepo.enqueueDelta(delta) } } public func onUpdated(_ args: OSModelChangedArgs) { if let delta = getUpdateModelDelta(args) { - OSOperationRepo.sharedInstance.enqueueDelta(delta) + operationRepo.enqueueDelta(delta) } } @@ -76,7 +78,7 @@ extension OSModelStoreListener { return } if let delta = getRemoveModelDelta(removedModel) { - OSOperationRepo.sharedInstance.enqueueDelta(delta) + operationRepo.enqueueDelta(delta) } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index c4a40be30..ae298c9a8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -32,8 +32,7 @@ import OneSignalCore The OSOperationRepo is a static singleton. OSDeltas are enqueued when model store observers observe changes to their models, and sorted to their appropriate executors. */ -public class OSOperationRepo: NSObject { - public static let sharedInstance = OSOperationRepo() +public class OSOperationRepo { private var hasCalledStart = false // The Operation Repo dispatch queue, serial. This synchronizes access to `deltaQueue` and flushing behavior. @@ -47,16 +46,37 @@ public class OSOperationRepo: NSObject { // TODO: This could come from a config, plist, method, remote params var pollIntervalMilliseconds = Int(POLL_INTERVAL_MS) public var paused = false + let jwtConfig: OSUserJwtConfig /** - Initilize this Operation Repo. Read from the cache. Executors may not be available by this time. - If everything starts up on initialize(), order can matter, ideally not but it can. - Likely call init on this from oneSignal but exeuctors can come from diff modules. + Sets the jwt config and uncaches + */ + public init(jwtConfig: OSUserJwtConfig) { + self.jwtConfig = jwtConfig + self.jwtConfig.subscribe(self, key: OS_OPERATION_REPO) + print("❌ OSOperationRepo init(\(String(describing: jwtConfig.isRequired))) called") + + // Read the Deltas from cache, if any... + guard let deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSOperationRepo is unable to uncache the OSDelta queue.") + return + } + self.deltaQueue = deltaQueue + } + + /** + Start this Operation Repo. */ public func start() { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return } + + guard jwtConfig.isRequired != nil else { + print("❌ OSOperationRepo.start() returning early due to unknown Identity Verification status.") + return + } + guard !hasCalledStart else { return } @@ -68,13 +88,6 @@ public class OSOperationRepo: NSObject { selector: #selector(self.addFlushDeltaQueueToDispatchQueue), name: Notification.Name(OS_ON_USER_WILL_CHANGE), object: nil) - // Read the Deltas from cache, if any... - if let deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { - self.deltaQueue = deltaQueue - OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSOperationRepo.start() with deltaQueue: \(deltaQueue)") - } else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSOperationRepo.start() is unable to uncache the OSDelta queue.") - } pollFlushQueue() } @@ -87,13 +100,12 @@ public class OSOperationRepo: NSObject { } /** - Add and start an executor. + Add an executor. */ public func addExecutor(_ executor: OSOperationExecutor) { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return } - start() executors.append(executor) for delta in executor.supportedDeltas { deltasToExecutorMap[delta] = executor @@ -111,7 +123,6 @@ public class OSOperationRepo: NSObject { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return } - start() self.dispatchQueue.async { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSOperationRepo enqueueDelta: \(delta)") self.deltaQueue.append(delta) @@ -140,8 +151,6 @@ public class OSOperationRepo: NSObject { OSBackgroundTaskManager.beginBackgroundTask(OPERATION_REPO_BACKGROUND_TASK) } - self.start() - if !self.deltaQueue.isEmpty { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSOperationRepo flushDeltaQueue in background: \(inBackground) with queue: \(self.deltaQueue)") } @@ -174,6 +183,30 @@ public class OSOperationRepo: NSObject { } } +extension OSOperationRepo: OSUserJwtConfigListener { + public func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + print("❌ OSOperationRepo onRequiresUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") + // If auth changed from false or unknown to true, process deltas + if to == true { + removeInvalidDeltas() + } + start() + } + + public func onJwtUpdated(externalId: String, to: String?) { + print("❌ OSOperationRepo onJwtUpdated for \(externalId) to \(String(describing: to))") + } + + /** + TODO: The operation repo cannot easily remove invalid Deltas that do not have an External ID. + Deltas have an Identity Model ID only and would need to access the User module to find the corresponding Identity Model. + Executors will handle this. + */ + func removeInvalidDeltas() { + print("❌ OSOperationRepo removeInvalidDeltas TODO!") + } +} + extension OSOperationRepo: OSLoggable { public func logSelf() { print("💛 Operation Repo: deltaQueue: \(self.deltaQueue )") diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index f5a26b182..32b9ec6aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -335,7 +335,7 @@ extension OSUserExecutor { self.executePendingRequests() } } - OSOperationRepo.sharedInstance.paused = false + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = false } onFailure: { error in OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor create user request failed with error: \(error.debugDescription)") if let nsError = error as? NSError { @@ -344,7 +344,7 @@ extension OSUserExecutor { // A failed create user request would leave the SDK in a bad state // Don't remove the request from cache and pause the operation repo // We will retry this request on a new session - OSOperationRepo.sharedInstance.paused = true + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true request.sentToClient = false } } else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift index 517946abe..d41b839fd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift @@ -30,9 +30,11 @@ import OneSignalCore import OneSignalOSCore class OSIdentityModelStoreListener: OSModelStoreListener { + let operationRepo: OSOperationRepo var store: OSModelStore - required init(store: OSModelStore) { + required init(store: OSModelStore, operationRepo: OSOperationRepo) { + self.operationRepo = operationRepo self.store = store } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift index 611228801..cec233e43 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift @@ -30,9 +30,11 @@ import OneSignalCore import OneSignalOSCore class OSPropertiesModelStoreListener: OSModelStoreListener { + let operationRepo: OSOperationRepo var store: OSModelStore - required init(store: OSModelStore) { + required init(store: OSModelStore, operationRepo: OSOperationRepo) { + self.operationRepo = operationRepo self.store = store } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift index 6ffe8c9ac..a7965a130 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift @@ -30,9 +30,11 @@ import OneSignalCore import OneSignalOSCore class OSSubscriptionModelStoreListener: OSModelStoreListener { + let operationRepo: OSOperationRepo var store: OSModelStore - required init(store: OSModelStore) { + required init(store: OSModelStore, operationRepo: OSOperationRepo) { + self.operationRepo = operationRepo self.store = store } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 69e2e62aa..82670ba84 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -173,6 +173,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { let pushSubscriptionModelStore = OSModelStore(changeSubscription: OSEventProducer(), storeKey: OS_PUSH_SUBSCRIPTION_MODEL_STORE_KEY) // These must be initialized in init() + let operationRepo: OSOperationRepo let identityModelStoreListener: OSIdentityModelStoreListener let propertiesModelStoreListener: OSPropertiesModelStoreListener let subscriptionModelStoreListener: OSSubscriptionModelStoreListener @@ -186,10 +187,11 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { private init(jwtConfig: OSUserJwtConfig) { self.jwtConfig = jwtConfig - self.identityModelStoreListener = OSIdentityModelStoreListener(store: identityModelStore) - self.propertiesModelStoreListener = OSPropertiesModelStoreListener(store: propertiesModelStore) - self.subscriptionModelStoreListener = OSSubscriptionModelStoreListener(store: subscriptionModelStore) - self.pushSubscriptionModelStoreListener = OSSubscriptionModelStoreListener(store: pushSubscriptionModelStore) + self.operationRepo = OSOperationRepo(jwtConfig: jwtConfig) + self.identityModelStoreListener = OSIdentityModelStoreListener(store: identityModelStore, operationRepo: operationRepo) + self.propertiesModelStoreListener = OSPropertiesModelStoreListener(store: propertiesModelStore, operationRepo: operationRepo) + self.subscriptionModelStoreListener = OSSubscriptionModelStoreListener(store: subscriptionModelStore, operationRepo: operationRepo) + self.pushSubscriptionModelStoreListener = OSSubscriptionModelStoreListener(store: pushSubscriptionModelStore, operationRepo: operationRepo) self.pushSubscriptionImpl = OSPushSubscriptionImpl(pushSubscriptionModelStore: pushSubscriptionModelStore) } @@ -226,7 +228,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { // Setup the executors // The OSUserExecutor has to run first, before other executors self.userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) - OSOperationRepo.sharedInstance.start() + operationRepo.start() // Cannot initialize these executors in `init` as they reference the sharedInstance let propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) @@ -235,9 +237,9 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { self.propertyExecutor = propertyExecutor self.identityExecutor = identityExecutor self.subscriptionExecutor = subscriptionExecutor - OSOperationRepo.sharedInstance.addExecutor(identityExecutor) - OSOperationRepo.sharedInstance.addExecutor(propertyExecutor) - OSOperationRepo.sharedInstance.addExecutor(subscriptionExecutor) + operationRepo.addExecutor(identityExecutor) + operationRepo.addExecutor(propertyExecutor) + operationRepo.addExecutor(subscriptionExecutor) // Path 2. There is a legacy player to migrate if let legacyPlayerId = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_LEGACY_PLAYER_ID, defaultValue: nil) { @@ -561,7 +563,7 @@ extension OneSignalUserManagerImpl { start() userExecutor!.executePendingRequests() - OSOperationRepo.sharedInstance.paused = false + operationRepo.paused = false updatePropertiesDeltas(property: .session_count, value: 1) // Fetch the user's data if there is a onesignal_id @@ -594,7 +596,7 @@ extension OneSignalUserManagerImpl { property: property.rawValue, value: value ) - OSOperationRepo.sharedInstance.enqueueDelta(delta) + operationRepo.enqueueDelta(delta) } /// Time processors forward the session time to this method. @@ -612,7 +614,7 @@ extension OneSignalUserManagerImpl { */ @objc public func runBackgroundTasks() { - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue(inBackground: true) + operationRepo.addFlushDeltaQueueToDispatchQueue(inBackground: true) } } From e019d3d93d7fab7100c81d1095130d8dbb9ee478 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 22 Aug 2024 10:16:49 -0700 Subject: [PATCH 14/56] User Manger - updates to login --- .../Source/OneSignalUserManagerImpl.swift | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 82670ba84..8106580ed 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -308,6 +308,10 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { if let user = _user { guard user.identityModel.externalId != externalId || externalId == nil else { OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignalUserManager.createNewUser: not creating new user due to logging into the same user.)") + if externalId != nil, token != nil { + // save the jwtToken, it can be updated + user.identityModel.jwtBearerToken = token + } return user } } @@ -386,17 +390,19 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignalUserManager internal _login called with externalId: \(externalId ?? "nil")") - // If have token, validate token. Account for this being a requirement. - // Logging into an identified user from an anonymous user + // Logging into an identified user from an anonymous user, if JWT is not ON if let externalId = externalId, let user = _user, - user.isAnonymous { + user.isAnonymous, + jwtConfig.isRequired != true + { user.identityModel.jwtBearerToken = token identifyUser(externalId: externalId, currentUser: user) return self.user } - // Logging into anon -> anon, identified -> anon, identified -> identified, or nil -> any user + // JWT Off: Logging into anon -> anon, identified -> anon, identified -> identified, or nil -> any user + // JWT On: All return createNewUser(externalId: externalId, token: token) } From a0baad8545ad35454cb5e2c2c142154ad9263bbe Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 23 Aug 2024 07:47:36 -0700 Subject: [PATCH 15/56] TODOs --- .../Source/Requests/OSRequestSetStartToken.swift | 1 + .../OneSignalUser/Source/Executors/OSUserExecutor.swift | 1 + 2 files changed, 2 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetStartToken.swift b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetStartToken.swift index 81469d3f4..23b833538 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetStartToken.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalLiveActivities/Source/Requests/OSRequestSetStartToken.swift @@ -28,6 +28,7 @@ import OneSignalCore import OneSignalUser +// TODO: JWT 🔐 This request needs the alias class OSRequestSetStartToken: OneSignalRequest, OSLiveActivityRequest, OSLiveActivityStartTokenRequest { override var description: String { return "(OSRequestSetStartToken) key:\(key) requestSuccessful:\(requestSuccessful) token:\(token)" } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 32b9ec6aa..9513121aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -519,6 +519,7 @@ extension OSUserExecutor { if let response = response { // Clear local data in preparation for hydration + // TODO: JWT 🔐 the following line feels wrong... maybe the user's changed by now OneSignalUserManagerImpl.sharedInstance.clearUserData() self.parseFetchUserResponse(response: response, identityModel: request.identityModel, originalPushToken: OneSignalUserManagerImpl.sharedInstance.pushSubscriptionImpl.token) From ed888cd9103091a6e4bc91e8b77b111ecb952973 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 26 Aug 2024 09:21:50 -0700 Subject: [PATCH 16/56] [tests] fix tests to build and pass, after changes * Set most things to not require auth / JWT off. --- .../OneSignalOSCoreMocks/OSCoreMocks.swift | 6 +- .../OneSignalUserMocks.swift | 2 +- .../Executors/UserExecutorTests.swift | 15 ++++- .../OneSignalUserObjcTests.m | 2 + .../OneSignalUserTests.swift | 5 +- .../SwitchUserIntegrationTests.swift | 15 ++++- .../UserConcurrencyTests.swift | 60 +++++++++++++------ 7 files changed, 79 insertions(+), 26 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift index 6a144eef7..c460c0e24 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCoreMocks/OSCoreMocks.swift @@ -31,9 +31,7 @@ import OneSignalCore @objc public class OSCoreMocks: NSObject { - public static func resetOperationRepo() { - OSOperationRepo.sharedInstance.reset() - } + // TODO: Add mocks here } extension OSOperationRepo { @@ -41,7 +39,7 @@ extension OSOperationRepo { The Operation Repo needs to reset between tests until we dependency inject the Operation Repo, to prevent state from carrying over between tests. */ - func reset() { + public func reset() { deltaQueue.removeAll() executors.removeAll() deltasToExecutorMap.removeAll() diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift index 3230ac5dc..92981061d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalUserMocks.swift @@ -36,7 +36,6 @@ public class OneSignalUserMocks: NSObject { // TODO: create mocked server responses to user requests @objc public static func reset() { - OSCoreMocks.resetOperationRepo() OneSignalUserManagerImpl.sharedInstance.reset() } } @@ -55,6 +54,7 @@ extension OneSignalUserManagerImpl { */ func reset() { identityModelRepo.reset() + operationRepo.reset() // Model store listeners unsubscribe to their models // User Manager start() will subscribe them diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index 31dd74e29..25b203fa0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -37,11 +37,18 @@ import OneSignalUserMocks private class Mocks { let client = MockOneSignalClient() let newRecordsState = MockNewRecordsState() + let jwtConfig = OSUserJwtConfig() let userExecutor: OSUserExecutor init() { OneSignalCoreImpl.setSharedClient(client) - userExecutor = OSUserExecutor(newRecordsState: newRecordsState) + userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + } + + func setAuthRequired(_ required: Bool) { + // Set User Manager's JWT to off, or it blocks requests in prepareForExecution + OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = required + jwtConfig.isRequired = required } func createUserInstance(externalId: String) -> OSUserInternal { @@ -75,6 +82,7 @@ final class UserExecutorTests: XCTestCase { func testCreateUser_withPushSubscription_addsToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID, subscriptionId: "push-sub-id") /* When */ @@ -89,6 +97,7 @@ final class UserExecutorTests: XCTestCase { func testCreateUser_withoutPushSubscription_doesNot_addToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) /* When */ @@ -110,6 +119,7 @@ final class UserExecutorTests: XCTestCase { func testIdentifyUser_successfully_forcesAddToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userA_EUID, conflicted: false) /* When */ @@ -135,6 +145,7 @@ final class UserExecutorTests: XCTestCase { func testIdentifyUserSuccessful_butUserHasChangedSince_doesNotAddToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) MockUserRequests.setDefaultIdentifyUserResponses(with: mocks.client, externalId: userA_EUID, conflicted: false) /* When */ @@ -156,6 +167,7 @@ final class UserExecutorTests: XCTestCase { func testIdentifyUser_withConflict_addsToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) let user = mocks.setUserManagerInternalUser(externalId: userB_EUID) let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) @@ -177,6 +189,7 @@ final class UserExecutorTests: XCTestCase { func testIdentifyUserWithConflict_butUserHasChangedSince_doesNot_addToNewRecords() { /* Setup */ let mocks = Mocks() + mocks.setAuthRequired(false) let user = mocks.setUserManagerInternalUser(externalId: "new-eid") let anonIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m index b367edbea..44193aaf5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserObjcTests.m @@ -61,6 +61,8 @@ - (void)testSendPurchases { }; [arrayOfPurchases addObject:purchase2]; + // Set JWT to off, before accessing the User Manager + [OneSignalUserManagerImpl.sharedInstance setRequiresUserAuth:false]; [OneSignalUserManagerImpl.sharedInstance sendPurchases:arrayOfPurchases]; // Run background threads diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift index 3a4bf1745..cefc8971b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/OneSignalUserTests.swift @@ -78,8 +78,11 @@ final class OneSignalUserTests: XCTestCase { MockUserRequests.setDefaultCreateAnonUserResponses(with: client) OneSignalCoreImpl.setSharedClient(client) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + // Increase flush interval to allow all the updates to batch - OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + OneSignalUserManagerImpl.sharedInstance.operationRepo.pollIntervalMilliseconds = 300 // Wait to let any pending flushes in the Operation Repo to run OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.1) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift index 95859d323..b50d3a576 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/SwitchUserIntegrationTests.swift @@ -40,6 +40,9 @@ final class SwitchUserIntegrationTests: XCTestCase { OneSignalCoreImpl.setSharedClient(client) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + /* When */ // 1. Login to user A and add tag @@ -108,6 +111,9 @@ final class SwitchUserIntegrationTests: XCTestCase { // Returns mocked user data to test hydration MockUserRequests.setDefaultFetchUserResponseForHydration(with: client, externalId: userA_EUID) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + /* When */ // 1. Anonymous user @@ -215,6 +221,9 @@ final class SwitchUserIntegrationTests: XCTestCase { MockUserRequests.setAddAliasesResponse(with: client, aliases: ["alias_b": "id_b"]) MockUserRequests.setAddEmailResponse(with: client, email: "email_b@example.com") + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + /* When */ // 1. Anonymous user starts @@ -318,8 +327,12 @@ final class SwitchUserIntegrationTests: XCTestCase { let client = MockOneSignalClient() OneSignalCoreImpl.setSharedClient(client) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + // Increase flush interval to allow all the updates to batch - OSOperationRepo.sharedInstance.pollIntervalMilliseconds = 300 + OneSignalUserManagerImpl.sharedInstance.operationRepo.pollIntervalMilliseconds = 300 + // Wait to let any pending flushes in the Operation Repo to run OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.3) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift index bcf076a74..4169325cc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -57,6 +57,9 @@ final class UserConcurrencyTests: XCTestCase { /* Setup */ OneSignalCoreImpl.setSharedClient(MockOneSignalClient()) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + /* When */ // 1. Enqueue 10 Deltas to the Operation Repo @@ -68,7 +71,7 @@ final class UserConcurrencyTests: XCTestCase { for _ in 1...4 { DispatchQueue.global().async { print("🧪 flushDeltaQueue on thread \(Thread.current)") - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + OneSignalUserManagerImpl.sharedInstance.operationRepo.addFlushDeltaQueueToDispatchQueue() } } @@ -82,6 +85,7 @@ final class UserConcurrencyTests: XCTestCase { This test reproduces a crash in the Subscription Executor. It is possible for two threads to modify and cache queues concurrently. */ + // TODO: revisit this test once subscriptions are addressed for JWT func testSubscriptionExecutorConcurrency() throws { /* Setup */ @@ -92,18 +96,20 @@ final class UserConcurrencyTests: XCTestCase { ) OneSignalCoreImpl.setSharedClient(client) - let executor = OSSubscriptionOperationExecutor(newRecordsState: OSNewRecordsState()) - OSOperationRepo.sharedInstance.addExecutor(executor) + let jwtConfig = OSUserJwtConfig() + let executor = OSSubscriptionOperationExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: jwtConfig) + let operationRepo = OSOperationRepo(jwtConfig: jwtConfig) + operationRepo.addExecutor(executor) /* When */ DispatchQueue.concurrentPerform(iterations: 50) { _ in // 1. Enqueue Remove Subscription Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) + operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) + operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + operationRepo.addFlushDeltaQueueToDispatchQueue() // 3. Simulate updating the executor's request queue from a network response executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) @@ -117,6 +123,8 @@ final class UserConcurrencyTests: XCTestCase { // Previously caused crash: signal SIGABRT - malloc: double free for ptr // Assert that every request SDK makes has a response set, and is handled XCTAssertTrue(client.allRequestsHandled) + // Ensure the requests are actually made, future proofing + XCTAssertEqual(client.executedRequests.count, 200) } /** @@ -131,18 +139,22 @@ final class UserConcurrencyTests: XCTestCase { OneSignalCoreImpl.setSharedClient(client) MockUserRequests.setAddAliasesResponse(with: client, aliases: aliases) - let executor = OSIdentityOperationExecutor(newRecordsState: OSNewRecordsState()) - OSOperationRepo.sharedInstance.addExecutor(executor) + // Set User Manager's JWT to off, or it blocks requests + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + + let executor = OSIdentityOperationExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig) + let operationRepo = OSOperationRepo(jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig) + operationRepo.addExecutor(executor) /* When */ DispatchQueue.concurrentPerform(iterations: 50) { _ in // 1. Enqueue Add Alias Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) + operationRepo.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) + operationRepo.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: UUID().uuidString, model: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()), property: "aliases", value: aliases)) // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + operationRepo.addFlushDeltaQueueToDispatchQueue() // 3. Simulate updating the executor's request queue from a network response executor.executeAddAliasesRequest(OSRequestAddAliases(aliases: aliases, identityModel: OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer())), inBackground: false) @@ -156,6 +168,8 @@ final class UserConcurrencyTests: XCTestCase { // Previously caused crash: signal SIGABRT - malloc: double free for ptr // Assert that every request SDK makes has a response set, and is handled XCTAssertTrue(client.allRequestsHandled) + // Ensure the requests are actually made, future proofing + XCTAssertGreaterThanOrEqual(client.executedRequests.count, 150) } /** @@ -169,21 +183,24 @@ final class UserConcurrencyTests: XCTestCase { client.fireSuccessForAllRequests = true OneSignalCoreImpl.setSharedClient(client) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + let identityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(identityModel) - let executor = OSPropertyOperationExecutor(newRecordsState: OSNewRecordsState()) - OSOperationRepo.sharedInstance.addExecutor(executor) + let executor = OSPropertyOperationExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig) + OneSignalUserManagerImpl.sharedInstance.operationRepo.addExecutor(executor) /* When */ DispatchQueue.concurrentPerform(iterations: 50) { _ in // 1. Enqueue Deltas to the Operation Repo - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) - OSOperationRepo.sharedInstance.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) + OneSignalUserManagerImpl.sharedInstance.operationRepo.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) + OneSignalUserManagerImpl.sharedInstance.operationRepo.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "language", value: UUID().uuidString)) // 2. Flush Operation Repo - OSOperationRepo.sharedInstance.addFlushDeltaQueueToDispatchQueue() + OneSignalUserManagerImpl.sharedInstance.operationRepo.addFlushDeltaQueueToDispatchQueue() // 3. Simulate updating the executor's request queue from a network response executor.executeUpdatePropertiesRequest(OSRequestUpdateProperties(params: ["properties": ["language": UUID().uuidString], "refresh_device_metadata": false], identityModel: identityModel), inBackground: false) @@ -194,6 +211,8 @@ final class UserConcurrencyTests: XCTestCase { /* Then */ // No crash + // Ensure the requests are actually made, future proofing + XCTAssertGreaterThanOrEqual(client.executedRequests.count, 75) } /** @@ -213,13 +232,16 @@ final class UserConcurrencyTests: XCTestCase { let identityModel1 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) let identityModel2 = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) - let userExecutor = OSUserExecutor(newRecordsState: OSNewRecordsState()) + // Set User Manager's JWT to off, or it blocks requests + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + + let userExecutor = OSUserExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig) /* When */ DispatchQueue.concurrentPerform(iterations: 50) { _ in let identifyRequest = OSRequestIdentifyUser(aliasLabel: OS_EXTERNAL_ID, aliasId: UUID().uuidString, identityModelToIdentify: identityModel1, identityModelToUpdate: identityModel2) - let fetchRequest = OSRequestFetchUser(identityModel: identityModel1, aliasLabel: OS_ONESIGNAL_ID, aliasId: UUID().uuidString, onNewSession: false) + let fetchRequest = OSRequestFetchUser(identityModel: identityModel1, onesignalId: UUID().uuidString, onNewSession: false) // Append and execute requests simultaneously userExecutor.appendToQueue(identifyRequest) @@ -233,6 +255,8 @@ final class UserConcurrencyTests: XCTestCase { /* Then */ // No crash + // Ensure the requests are actually made, future proofing + XCTAssertGreaterThanOrEqual(client.executedRequests.count, 75) } /** From 8694a4d3e19f9a8b89544d21b3492369c4bdad47 Mon Sep 17 00:00:00 2001 From: Nan Date: Thu, 29 Aug 2024 11:15:54 -0700 Subject: [PATCH 17/56] consolidate all user-related requirement checks * Multiple requests are making the same user-related requirement checks. * Put this into a helper function. * Since each request sets its own path at execution time with an appropriate app ID and alias, the app ID and alias are two separate checks. --- .../OneSignal.xcodeproj/project.pbxproj | 4 ++ .../Source/Jwt/OSAliasPair.swift | 39 +++++++++++++++++ .../Source/Requests/OSRequestAddAliases.swift | 10 ++--- .../OSRequestCreateSubscription.swift | 10 ++--- .../Source/Requests/OSRequestFetchUser.swift | 11 ++--- .../Requests/OSRequestRemoveAlias.swift | 10 ++--- .../Requests/OSRequestUpdateProperties.swift | 10 ++--- .../Source/Requests/OSUserRequest.swift | 42 +++++++++++++++---- 8 files changed, 93 insertions(+), 43 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 8a71e8d0f..121ed036d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -182,6 +182,7 @@ 3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */; }; 3CF11E402C6E6DE2002856F5 /* MockNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */; }; 3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */; }; + 3CF807352C80E3A6003E5FE1 /* OSAliasPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF807342C80E3A6003E5FE1 /* OSAliasPair.swift */; }; 3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */; }; 3CF862A028A1964F00776CA4 /* OSPropertiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */; }; 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */; }; @@ -1304,6 +1305,7 @@ 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserExecutorTests.swift; sourceTree = ""; }; 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNewRecordsState.swift; sourceTree = ""; }; 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSNewRecordsState.swift; sourceTree = ""; }; + 3CF807342C80E3A6003E5FE1 /* OSAliasPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSAliasPair.swift; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -2082,6 +2084,7 @@ isa = PBXGroup; children = ( 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */, + 3CF807342C80E3A6003E5FE1 /* OSAliasPair.swift */, ); path = Jwt; sourceTree = ""; @@ -4063,6 +4066,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 3CF807352C80E3A6003E5FE1 /* OSAliasPair.swift in Sources */, DEFB3E652BB7346D00E65DAD /* OSLiveActivities.swift in Sources */, 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */, 3C11518B289ADEEB00565C41 /* OSEventProducer.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift new file mode 100644 index 000000000..b5cb9e051 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift @@ -0,0 +1,39 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + An alias label and alias ID pair to represent a user. + */ +@objc public class OSAliasPair: NSObject { + public let label: String + public let id: String + + public init(_ label: String, _ id: String) { + self.label = label + self.id = id + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift index 89a683d3f..9c3d80cd1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestAddAliases.swift @@ -40,18 +40,14 @@ class OSRequestAddAliases: OneSignalRequest, OSUserRequest { /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - let alias = getAlias(identityModel: identityModel) guard - let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let aliasIdToUse = alias.id, - let appId = OneSignalConfigManager.getAppId(), - addJWTHeaderIsValid(identityModel: identityModel) + let alias = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState), + let appId = OneSignalConfigManager.getAppId() else { return false } - self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/identity" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)/identity" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift index 234f1ef14..0bce71257 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateSubscription.swift @@ -45,18 +45,14 @@ class OSRequestCreateSubscription: OneSignalRequest, OSUserRequest { /// Needs the `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - let alias = getAlias(identityModel: identityModel) guard - let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let aliasIdToUse = alias.id, - let appId = OneSignalConfigManager.getAppId(), - addJWTHeaderIsValid(identityModel: identityModel) + let alias = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState), + let appId = OneSignalConfigManager.getAppId() else { return false } - self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/subscriptions" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)/subscriptions" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift index bab458d52..aa1ef1e1d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestFetchUser.swift @@ -48,17 +48,14 @@ class OSRequestFetchUser: OneSignalRequest, OSUserRequest { // TODO: JWT 🔐 Is external ID already handled by this time? Or do we need to check the alias here? /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - let alias = getAlias(identityModel: identityModel) guard - let aliasIdToUse = alias.id, - let appId = OneSignalConfigManager.getAppId(), - newRecordsState.canAccess(onesignalId), - addJWTHeaderIsValid( identityModel: identityModel) + let alias = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState), + let appId = OneSignalConfigManager.getAppId() else { - OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request for \(alias) yet.") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Cannot generate the fetch user request for \(identityModel.aliases) yet.") return false } - self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift index f83800a4a..3608a1082 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestRemoveAlias.swift @@ -40,18 +40,14 @@ class OSRequestRemoveAlias: OneSignalRequest, OSUserRequest { /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - let alias = getAlias(identityModel: identityModel) guard - let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let aliasIdToUse = alias.id, - let appId = OneSignalConfigManager.getAppId(), - addJWTHeaderIsValid(identityModel: identityModel) + let alias = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState), + let appId = OneSignalConfigManager.getAppId() else { return false } - self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)/identity/\(labelToRemove)" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)/identity/\(labelToRemove)" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift index b4f99116e..53e4447cc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift @@ -39,19 +39,15 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { /// Needs `onesignal_id` without JWT on or `external_id` with valid JWT to send this request func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - let alias = getAlias(identityModel: identityModel) guard - let onesignalId = identityModel.onesignalId, - newRecordsState.canAccess(onesignalId), - let aliasIdToUse = alias.id, - let appId = OneSignalConfigManager.getAppId(), - addJWTHeaderIsValid(identityModel: identityModel) + let alias = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState), + let appId = OneSignalConfigManager.getAppId() else { return false } _ = self.addPushSubscriptionIdToAdditionalHeaders() - self.path = "apps/\(appId)/users/by/\(alias.label)/\(aliasIdToUse)" + self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift index 547e52b42..1f08cbaaf 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift @@ -35,16 +35,42 @@ protocol OSUserRequest: OneSignalRequest, NSCoding { internal extension OneSignalRequest { /** - Returns the alias pair to use to send this request for. Defaults to Onesignal Id, unless Identity Verification is on. + Handles a full check of user-related requirements. + - The existence of onesignal ID and the ability to access it. + - The existence of an appropriate alias. + - Checks JWT requirements and sets header. + + - Returns: The alias pair to use to send this request. */ - func getAlias(identityModel: OSIdentityModel) -> (label: String, id: String?) { - var label = OS_ONESIGNAL_ID - var id = identityModel.onesignalId - if OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired == true { - label = OS_EXTERNAL_ID - id = identityModel.externalId + func checkUserRequirementsAndReturnAlias(_ identityModel: OSIdentityModel, _ newRecordsState: OSNewRecordsState) -> OSAliasPair? { + guard + let onesignalId = identityModel.onesignalId, + newRecordsState.canAccess(onesignalId), + let aliasPair = getAlias(identityModel: identityModel, jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig), + addJWTHeaderIsValid(identityModel: identityModel) + else { + return nil + } + + return aliasPair + } + + private func getAlias(identityModel: OSIdentityModel, jwtConfig: OSUserJwtConfig) -> OSAliasPair? { + guard let jwtRequired = jwtConfig.isRequired else { + return nil } - return (label, id) + + if jwtRequired, let externalId = identityModel.externalId + { + // JWT is on and external ID exists + return OSAliasPair(OS_EXTERNAL_ID, externalId) + } else if !jwtRequired, let onesignalId = identityModel.onesignalId { + // JWT is off and onesignal ID exists + return OSAliasPair(OS_ONESIGNAL_ID, onesignalId) + } + + // Missing onesignal ID or external ID, when expected + return nil } /** From c6989b27d7666423aec19d496dae797e1fca259e Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 27 Aug 2024 13:43:24 -0700 Subject: [PATCH 18/56] [nit] remove extraneous logging already done by client * The client already logs the error response from the server * The executors don't need to re-log the same response, just adds clutter. --- .../Source/Executors/OSIdentityOperationExecutor.swift | 1 - .../Source/Executors/OSPropertyOperationExecutor.swift | 1 - .../Source/Executors/OSSubscriptionOperationExecutor.swift | 1 - .../OneSignalUser/Source/Executors/OSUserExecutor.swift | 2 -- 4 files changed, 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 609cc486b..787541525 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -255,7 +255,6 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } } onFailure: { error in - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor add aliases request failed with error: \(error.debugDescription)") self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index bea96ca0e..755a502b6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -292,7 +292,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } } onFailure: { error in - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor update properties request failed with error: \(error.debugDescription)") self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 02d17554c..eef6d7da2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -414,7 +414,6 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } } onFailure: { error in - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor update subscription request failed with error: \(error.debugDescription)") self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 9513121aa..192a42278 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -337,7 +337,6 @@ extension OSUserExecutor { } OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = false } onFailure: { error in - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor create user request failed with error: \(error.debugDescription)") if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType != .retryable { @@ -546,7 +545,6 @@ extension OSUserExecutor { } self.executePendingRequests() } onFailure: { error in - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor executeFetchUserRequest failed with error: \(error.debugDescription)") if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .missing { From ac887b9ada7a007af4162d4f6818b40e924d2ad8 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 28 Aug 2024 09:54:36 -0700 Subject: [PATCH 19/56] [nit] log when GET requests are sent in Client * The client only logged sent requests that contained a body, now also log GETs that do not have a body, such as fetching remote params or IAMs --- .../OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m index 82fa964dd..43ef0038e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/API/OneSignalClient.m @@ -176,8 +176,10 @@ - (double)calculateReattemptDelay:(int)reattemptCount { } - (void)prettyPrintDebugStatementWithRequest:(OneSignalRequest *)request { - if (![NSJSONSerialization isValidJSONObject:request.parameters]) + if (![NSJSONSerialization isValidJSONObject:request.parameters]) { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"HTTP Request (%@) with URL: %@, with headers: %@", NSStringFromClass([request class]), request.urlRequest.URL.absoluteString, request.additionalHeaders]]; return; + } NSError *error; From d19cf0cba3f9d1af0913289a44f86bc48fe25883 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 30 Aug 2024 09:28:21 -0700 Subject: [PATCH 20/56] Expose JWT config to objective c Motivation: * The OSMessagingController will be a `OSUserJwtConfigListener` and observe jwt * Optional bool is not compatible with objective c --- .../Source/Jwt/OSUserJwtConfig.swift | 26 +++++++++---------- .../Source/OSOperationRepo.swift | 4 +-- .../OSIdentityOperationExecutor.swift | 4 +-- .../OSPropertyOperationExecutor.swift | 4 +-- .../Source/Executors/OSUserExecutor.swift | 4 +-- .../Source/OneSignalUserManagerImpl.swift | 8 +++++- 6 files changed, 27 insertions(+), 23 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift index 83740cd87..9da450fe1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift @@ -28,13 +28,11 @@ import Foundation import OneSignalCore -/** - Use an enum to avoid working with optional Bool, which is unsightly to cache and uncache. - */ -enum OSRequiresUserAuth: String { - case on - case off - case unknown +@objc +public enum OSRequiresUserAuth: Int { + case on = 1 + case off = -1 + case unknown = 0 // TODO: JWT 🔐 consider additional reasons such as detecting this by dev calling loginWithJWT / onViaRemoteParams func isRequired() -> Bool? { @@ -52,8 +50,8 @@ enum OSRequiresUserAuth: String { /** Internal listener. */ -public protocol OSUserJwtConfigListener { - func onRequiresUserAuthChanged(from: Bool?, to: Bool?) +@objc public protocol OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) func onJwtUpdated(externalId: String, to: String?) } @@ -69,10 +67,10 @@ public class OSUserJwtConfig { print("❌ OSUserJwtConfig.requiresUserAuth: changing from \(oldValue) to \(requiresUserAuth), firing \(changeNotifier)") // Persist new value - OneSignalUserDefaults.initShared().saveString(forKey: OSUD_USE_IDENTITY_VERIFICATION, withValue: requiresUserAuth.rawValue) + OneSignalUserDefaults.initShared().saveInteger(forKey: OSUD_USE_IDENTITY_VERIFICATION, withValue: requiresUserAuth.rawValue) self.changeNotifier.fire { listener in - listener.onRequiresUserAuthChanged(from: oldValue.isRequired(), to: requiresUserAuth.isRequired()) + listener.onRequiresUserAuthChanged(from: oldValue, to: requiresUserAuth) } } } @@ -94,11 +92,11 @@ public class OSUserJwtConfig { } public init() { - let rawValue = OneSignalUserDefaults.initShared().getSavedString(forKey: OSUD_USE_IDENTITY_VERIFICATION, defaultValue: OSRequiresUserAuth.unknown.rawValue) + let rawValue = OneSignalUserDefaults.initShared().getSavedInteger(forKey: OSUD_USE_IDENTITY_VERIFICATION, defaultValue: OSRequiresUserAuth.unknown.rawValue) - print("❌ OSUserJwtConfig init(): \(OSRequiresUserAuth(rawValue: rawValue!)))") + print("❌ OSUserJwtConfig init(): \(String(describing: OSRequiresUserAuth(rawValue: rawValue))))") - requiresUserAuth = OSRequiresUserAuth(rawValue: rawValue ?? OSRequiresUserAuth.unknown.rawValue) ?? OSRequiresUserAuth.unknown + requiresUserAuth = OSRequiresUserAuth(rawValue: rawValue) ?? OSRequiresUserAuth.unknown } public func subscribe(_ listener: OSUserJwtConfigListener, key: String) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index ae298c9a8..9b3ad9abe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -184,10 +184,10 @@ public class OSOperationRepo { } extension OSOperationRepo: OSUserJwtConfigListener { - public func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + public func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { print("❌ OSOperationRepo onRequiresUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process deltas - if to == true { + if to == .on { removeInvalidDeltas() } start() diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 787541525..b3f4580c4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -333,10 +333,10 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } extension OSIdentityOperationExecutor: OSUserJwtConfigListener { - func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { print("❌ OSIdentityOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests - if to == true { + if to == .on { removeInvalidDeltasAndRequests() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 755a502b6..866eca4f2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -325,10 +325,10 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } extension OSPropertyOperationExecutor: OSUserJwtConfigListener { - func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { print("❌ OSPropertyOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests - if to == true { + if to == .on { removeInvalidDeltasAndRequests() } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 192a42278..5ecec8944 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -677,10 +677,10 @@ extension OSUserExecutor { } extension OSUserExecutor: OSUserJwtConfigListener { - func onRequiresUserAuthChanged(from: Bool?, to: Bool?) { + func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { print("❌ OSUserExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests - if to == true { + if to == .on { removeInvalidRequests() } self.executePendingRequests() diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 8106580ed..e974e0905 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -627,9 +627,15 @@ extension OneSignalUserManagerImpl { // MARK: - JWT extension OneSignalUserManagerImpl { - @objc public func setRequiresUserAuth(_ required: Bool) { + @objc + public func setRequiresUserAuth(_ required: Bool) { jwtConfig.isRequired = required } + + @objc + public func subscribeToJwtConfig(_ listener: OSUserJwtConfigListener, key: String) { + jwtConfig.subscribe(listener, key: key) + } } extension OneSignalUserManagerImpl: OSUser { From 40e74ee101943142e05ca0a0179e2b5e2660d601 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 30 Aug 2024 10:08:37 -0700 Subject: [PATCH 21/56] [nit] Rename jwt token parameter for clarity --- .../OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift | 8 ++++---- .../OneSignalOSCore/Source/OSOperationRepo.swift | 4 ++-- .../Source/Executors/OSIdentityOperationExecutor.swift | 4 ++-- .../Source/Executors/OSPropertyOperationExecutor.swift | 4 ++-- .../OneSignalUser/Source/Executors/OSUserExecutor.swift | 4 ++-- .../OneSignalUser/Source/OSIdentityModelRepo.swift | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift index 9da450fe1..1a86ae435 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift @@ -52,7 +52,7 @@ public enum OSRequiresUserAuth: Int { */ @objc public protocol OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) - func onJwtUpdated(externalId: String, to: String?) + func onJwtUpdated(externalId: String, token: String?) } public class OSUserJwtConfig { @@ -103,10 +103,10 @@ public class OSUserJwtConfig { self.changeNotifier.subscribe(listener, key: key) } - public func onJwtTokenChanged(externalId: String, to: String?) { - print("❌ OSUserJwtConfig.onJwtTokenChanged \(externalId): \(to)") + public func onJwtTokenChanged(externalId: String, token: String?) { + print("❌ OSUserJwtConfig.onJwtTokenChanged \(externalId): \(token)") changeNotifier.fire { listener in - listener.onJwtUpdated(externalId: externalId, to: to) + listener.onJwtUpdated(externalId: externalId, token: token) } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 9b3ad9abe..963945e75 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -193,8 +193,8 @@ extension OSOperationRepo: OSUserJwtConfigListener { start() } - public func onJwtUpdated(externalId: String, to: String?) { - print("❌ OSOperationRepo onJwtUpdated for \(externalId) to \(String(describing: to))") + public func onJwtUpdated(externalId: String, token: String?) { + print("❌ OSOperationRepo onJwtUpdated for \(externalId) to \(String(describing: token))") } /** diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index b3f4580c4..51e22a637 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -341,8 +341,8 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { } } - func onJwtUpdated(externalId: String, to: String?) { - print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + func onJwtUpdated(externalId: String, token: String?) { + print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } private func removeInvalidDeltasAndRequests() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 866eca4f2..6c67ca698 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -333,8 +333,8 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { } } - func onJwtUpdated(externalId: String, to: String?) { - print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + func onJwtUpdated(externalId: String, token: String?) { + print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } private func removeInvalidDeltasAndRequests() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 5ecec8944..7bb555f15 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -686,8 +686,8 @@ extension OSUserExecutor: OSUserJwtConfigListener { self.executePendingRequests() } - func onJwtUpdated(externalId: String, to: String?) { - print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: to))") + func onJwtUpdated(externalId: String, token: String?) { + print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } private func removeInvalidRequests() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index d8e5bf1aa..2c8fd293b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -102,7 +102,7 @@ extension OSIdentityModelRepo: OSModelChangedHandler { return } print("❌ OSIdentityModelRepo onModelUpdated for \(externalId): \(token)") - OneSignalUserManagerImpl.sharedInstance.jwtConfig.onJwtTokenChanged(externalId: externalId, to: token) + OneSignalUserManagerImpl.sharedInstance.jwtConfig.onJwtTokenChanged(externalId: externalId, token: token) } } From 90a08f782d3002b40afb9eee621d9c6ede0a792f Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 30 Aug 2024 09:27:16 -0700 Subject: [PATCH 22/56] Move shared user helpers to utility class --- .../OneSignal.xcodeproj/project.pbxproj | 4 ++ .../Source/OneSignalUserManagerImpl.swift | 48 +++++++++++++ .../Source/Requests/OSRequestCreateUser.swift | 2 +- .../Requests/OSRequestUpdateProperties.swift | 2 +- .../OSRequestUpdateSubscription.swift | 2 +- .../Source/Requests/OSUserRequest.swift | 48 +++---------- .../Source/Support/OSUserUtils.swift | 71 +++++++++++++++++++ 7 files changed, 136 insertions(+), 41 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 121ed036d..e85a0da7b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -183,6 +183,7 @@ 3CF11E402C6E6DE2002856F5 /* MockNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */; }; 3CF1A5632C669EA40056B3AA /* OSNewRecordsState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */; }; 3CF807352C80E3A6003E5FE1 /* OSAliasPair.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF807342C80E3A6003E5FE1 /* OSAliasPair.swift */; }; + 3CF807372C80F3B5003E5FE1 /* OSUserUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF807362C80F3B5003E5FE1 /* OSUserUtils.swift */; }; 3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */; }; 3CF862A028A1964F00776CA4 /* OSPropertiesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */; }; 3CF862A228A197D200776CA4 /* OSPropertiesModelStoreListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */; }; @@ -1306,6 +1307,7 @@ 3CF11E3F2C6E6DE2002856F5 /* MockNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNewRecordsState.swift; sourceTree = ""; }; 3CF1A5622C669EA40056B3AA /* OSNewRecordsState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSNewRecordsState.swift; sourceTree = ""; }; 3CF807342C80E3A6003E5FE1 /* OSAliasPair.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSAliasPair.swift; sourceTree = ""; }; + 3CF807362C80F3B5003E5FE1 /* OSUserUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserUtils.swift; sourceTree = ""; }; 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSIdentityModel.swift; sourceTree = ""; }; 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModel.swift; sourceTree = ""; }; 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPropertiesModelStoreListener.swift; sourceTree = ""; }; @@ -2188,6 +2190,7 @@ isa = PBXGroup; children = ( 3CEE90A62BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift */, + 3CF807362C80F3B5003E5FE1 /* OSUserUtils.swift */, ); path = Support; sourceTree = ""; @@ -4295,6 +4298,7 @@ 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, 3C9AD6CB2B228B5200BC1540 /* OSRequestIdentifyUser.swift in Sources */, + 3CF807372C80F3B5003E5FE1 /* OSUserUtils.swift in Sources */, 3C9AD6BC2B2285FB00BC1540 /* OSUserExecutor.swift in Sources */, 3C9AD6C32B22887700BC1540 /* OSRequestCreateUser.swift in Sources */, 3C9AD6D12B228B9200BC1540 /* OSRequestRemoveAlias.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index e974e0905..d5dec443e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -372,6 +372,54 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { return user.identityModel.externalId == externalId } + + @objc + public func getAliasForCurrentUser() -> OSAliasPair? { + guard let identityModel = _user?.identityModel else { + return nil + } + + return OSUserUtils.getAlias( + identityModel: identityModel, + jwtConfig: jwtConfig + ) + } + + /** + Helper method used by other modules in Objective-C such as fetching in app messages. + - Returns: The complete user header including push headers and jwt headers, if valid. + Returns `nil` if this request is not yet valid due to auth or null user instance. + + TODO: Alternative is to refactor and let OSRequestGetInAppMessages implement the OSUserRequest protocol + and have access to the extension methods on OneSignalRequest that handles the header. + */ + @objc + public func getCurrentUserFullHeader() -> [String: String]? { + guard let required = jwtConfig.isRequired else { + return nil + } + + guard let _user = _user else { + return nil + } + + var fullHeader = OSUserUtils.getFullPushHeader() + + if !required { + return fullHeader + } + + // JWT is required + + if _user.identityModel.isJwtValid(), + let token = _user.identityModel.jwtBearerToken + { + fullHeader["Authorization"] = "Bearer \(token)" + return fullHeader + } + return nil + } + /** Clears the existing user's data in preparation for hydration via a fetch user call. */ diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift index 130682567..b568b83e6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestCreateUser.swift @@ -65,7 +65,7 @@ class OSRequestCreateUser: OneSignalRequest, OSUserRequest { return false } - _ = self.addPushSubscriptionIdToAdditionalHeaders() + _ = self.addPushSubscriptionToAdditionalHeaders() self.path = "apps/\(appId)/users" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift index 53e4447cc..572042733 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateProperties.swift @@ -46,7 +46,7 @@ class OSRequestUpdateProperties: OneSignalRequest, OSUserRequest { return false } - _ = self.addPushSubscriptionIdToAdditionalHeaders() + _ = self.addPushSubscriptionToAdditionalHeaders() self.path = "apps/\(appId)/users/by/\(alias.label)/\(alias.id)" return true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift index d13ecbc68..cb687938e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift @@ -42,7 +42,7 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { // Need the subscription_id func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - addPushSubscriptionIdToAdditionalHeaders() + addPushSubscriptionToAdditionalHeaders() if let subscriptionId = subscriptionModel.subscriptionId, newRecordsState.canAccess(subscriptionId), let appId = OneSignalConfigManager.getAppId() diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift index 1f08cbaaf..52bebf57e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSUserRequest.swift @@ -56,21 +56,7 @@ internal extension OneSignalRequest { } private func getAlias(identityModel: OSIdentityModel, jwtConfig: OSUserJwtConfig) -> OSAliasPair? { - guard let jwtRequired = jwtConfig.isRequired else { - return nil - } - - if jwtRequired, let externalId = identityModel.externalId - { - // JWT is on and external ID exists - return OSAliasPair(OS_EXTERNAL_ID, externalId) - } else if !jwtRequired, let onesignalId = identityModel.onesignalId { - // JWT is off and onesignal ID exists - return OSAliasPair(OS_ONESIGNAL_ID, onesignalId) - } - - // Missing onesignal ID or external ID, when expected - return nil + return OSUserUtils.getAlias(identityModel: identityModel, jwtConfig: jwtConfig) } /** @@ -98,28 +84,14 @@ internal extension OneSignalRequest { return canBeSent } - /** Returns if the `OneSignal-Subscription-Id` header was added successfully. */ - func addPushSubscriptionIdToAdditionalHeaders() -> Bool { - _ = addPushToken() - if let pushSubscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId { - var additionalHeaders = self.additionalHeaders ?? [String: String]() - additionalHeaders["OneSignal-Subscription-Id"] = pushSubscriptionId - self.additionalHeaders = additionalHeaders - return true - } else { - return false - } - } - - /** Returns if the `Device-Auth-Push-Token` header was added successfully. */ - private func addPushToken() -> Bool { - if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address { - var additionalHeaders = self.additionalHeaders ?? [String: String]() - additionalHeaders["Device-Auth-Push-Token"] = "Basic \(token)" - self.additionalHeaders = additionalHeaders - return true - } else { - return false - } + /** + The `OneSignal-Subscription-Id` header supports improved `last_active` tracking for subscriptions that were actually active. + The `Device-Auth-Push-Token` header includes the push token if available. + */ + func addPushSubscriptionToAdditionalHeaders() { + let pushHeader = OSUserUtils.getFullPushHeader() + var additionalHeaders = self.additionalHeaders ?? [String: String]() + additionalHeaders = additionalHeaders.merging(pushHeader) { (_, new) in new } + self.additionalHeaders = additionalHeaders } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift new file mode 100644 index 000000000..06d1afac7 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift @@ -0,0 +1,71 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalOSCore + +class OSUserUtils { + /** + Returns the alias pair to use to send a request for. + When Identity Verification is unknown, or IDs are missing, this will be null. + When Identity Verification is disabled, this should be the onesignal ID. + When Identity Verification is enabled, this should be the external ID. + */ + static func getAlias(identityModel: OSIdentityModel, jwtConfig: OSUserJwtConfig) -> OSAliasPair? { + guard let jwtRequired = jwtConfig.isRequired else { + return nil + } + + if jwtRequired, let externalId = identityModel.externalId + { + // JWT is on and external ID exists + return OSAliasPair(OS_EXTERNAL_ID, externalId) + } else if !jwtRequired, let onesignalId = identityModel.onesignalId { + // JWT is off and onesignal ID exists + return OSAliasPair(OS_ONESIGNAL_ID, onesignalId) + } + + // Missing onesignal ID or external ID, when expected + return nil + } + + /** + The `OneSignal-Subscription-Id` header supports improved `last_active` tracking for subscriptions that were actually active. + The `Device-Auth-Push-Token` header includes the push token if available. + */ + static func getFullPushHeader() -> [String: String] { + var headers = [String: String]() + + if let pushSubscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId { + headers["OneSignal-Subscription-Id"] = pushSubscriptionId + } + if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address { + headers["Device-Auth-Push-Token"] = "Basic \(token)" + } + return headers + } +} From c9b0a94a5d9acca8e4425cadd8d45f2d382d3f0c Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 30 Aug 2024 09:14:16 -0700 Subject: [PATCH 23/56] Modify getting IAM to use alias and jwt * All methods that trigger fetching IAM from server will no longer pass the push subscription ID. * The method to fetch IAM will itself handle the requirements. * To get IAMs from the server, the following requirements are necessary: - A subscription ID - An appropriate alias (depending on Identity Verification enabled) for the subscription - A valid JWT token for the user if Identity Verification is enabled * OSMessagingController will observe user and jwt --- .../Controller/OSMessagingController.h | 5 +- .../Controller/OSMessagingController.m | 86 +++++++++++++++++-- .../OSInAppMessagingDefines.h | 2 + .../OneSignalInAppMessages.h | 2 +- .../OneSignalInAppMessages.m | 4 +- .../Requests/OSInAppMessagingRequests.h | 2 +- .../Requests/OSInAppMessagingRequests.m | 8 +- .../Source/Jwt/OSAliasPair.swift | 4 +- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 19 ++-- 9 files changed, 107 insertions(+), 25 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h index d37e3577e..089a7be59 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.h @@ -29,6 +29,7 @@ #import "OSInAppMessageInternal.h" #import "OSInAppMessageViewController.h" #import "OSTriggerController.h" +#import #import NS_ASSUME_NONNULL_BEGIN @@ -39,7 +40,7 @@ NS_ASSUME_NONNULL_BEGIN @end -@interface OSMessagingController : NSObject +@interface OSMessagingController : NSObject @property (class, readonly) BOOL isInAppMessagingPaused; @@ -52,7 +53,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)removeInstance; - (void)presentInAppMessage:(OSInAppMessageInternal *)message; - (void)updateInAppMessagesFromCache; -- (void)getInAppMessagesFromServer:(NSString * _Nullable)subscriptionId; +- (void)getInAppMessagesFromServer; - (void)messageViewImpressionRequest:(OSInAppMessageInternal *)message; - (void)messageViewPageImpressionRequest:(OSInAppMessageInternal *)message withPageId:(NSString *)pageId; diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index 47bc312ff..629a51b13 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -145,6 +145,21 @@ @implementation OSMessagingController @dynamic isInAppMessagingPaused; // Maximum time decided to save IAM with redisplay on cache - current value: six months in seconds static long OS_IAM_MAX_CACHE_TIME = 6 * 30 * 24 * 60 * 60; + +/** + If an attempt to get IAMs from the server returns an Unauthorized response, + the controller should re-attempt once the JWT token is updated. + */ +static BOOL shouldRetryGetInAppMessagesOnJwtUpdated = false; + +/** + If an attempt to get IAMs from the server is blocked by incomplete alias information, + the controller should re-attempt once the user state changes. + An example of when this can happen occurs when users are switching with Identity Verification turned off - + the SDK has a push subscription ID but no onesignal ID for the current user. + */ +static BOOL shouldRetryGetInAppMessagesOnUserChange = false; + static OSMessagingController *sharedInstance = nil; static dispatch_once_t once; + (OSMessagingController *)sharedInstance { @@ -167,6 +182,8 @@ + (void)removeInstance { + (void)start { OSMessagingController *shared = OSMessagingController.sharedInstance; [OneSignalUserManagerImpl.sharedInstance.pushSubscriptionImpl addObserver:shared]; + [OneSignalUserManagerImpl.sharedInstance addObserver:shared]; + [OneSignalUserManagerImpl.sharedInstance subscribeToJwtConfig:shared key:OS_MESSAGING_CONTROLLER]; } static BOOL _isInAppMessagingPaused = false; @@ -236,15 +253,48 @@ - (void)updateInAppMessagesFromCache { [self evaluateMessages]; } -- (void)getInAppMessagesFromServer:(NSString *)subscriptionId { - [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer"]; - +/** + To get IAMs from the server, the following requirements are necessary: + - A subscription ID + - An appropriate alias (depending on Identity Verification enabled) for the subscription + - A valid JWT token for the user if Identity Verification is enabled + + This current implementation is not completely correct, as it will always use the current subscription ID and the current user. + The SDK would need to consider if the current user owns the subscription on the server. + */ +- (void)getInAppMessagesFromServer { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer attempted"]; + + NSString *subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId; if (!subscriptionId) { + // When the subscription observer fires, it will drive a re-fetch + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer blocked by null subscriptionId"]; + [self updateInAppMessagesFromCache]; + return; + } + + OSAliasPair *alias = [OneSignalUserManagerImpl.sharedInstance getAliasForCurrentUser]; + if (!alias) { + // When the user observer fires, it will drive a re-fetch + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer blocked by null alias"]; + shouldRetryGetInAppMessagesOnUserChange = true; + [self updateInAppMessagesFromCache]; + return; + } + + NSDictionary *header = [OneSignalUserManagerImpl.sharedInstance getCurrentUserFullHeader]; + if (!header) { + // When the JWT updated listener fires, it will drive a re-fetch + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer blocked by missing header"]; + shouldRetryGetInAppMessagesOnJwtUpdated = true; [self updateInAppMessagesFromCache]; return; } - OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId]; + OSRequestGetInAppMessages *request = [OSRequestGetInAppMessages withSubscriptionId:subscriptionId + withAliasLabel:alias.label + withAliasId:alias.id + withHeader:header]; [OneSignalCoreImpl.sharedClient executeRequest:request onSuccess:^(NSDictionary *result) { dispatch_async(dispatch_get_main_queue(), ^{ [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"getInAppMessagesFromServer success"]; @@ -270,6 +320,10 @@ - (void)getInAppMessagesFromServer:(NSString *)subscriptionId { }); } onFailure:^(NSError *error) { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"getInAppMessagesFromServer failure: %@", error.localizedDescription]]; + OSResponseStatusType responseType = [OSNetworkingUtils getResponseStatusType:error.code]; + if (responseType == OSResponseStatusUnauthorized) { + shouldRetryGetInAppMessagesOnJwtUpdated = true; + } [self updateInAppMessagesFromCache]; }]; } @@ -1087,7 +1141,29 @@ - (void)onPushSubscriptionDidChangeWithState:(OSPushSubscriptionChangedState * _ // Pull new IAMs when the subscription id changes to a new valid subscription id [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"OSMessagingController onPushSubscriptionDidChange: changed to new valid subscription id"]; - [self getInAppMessagesFromServer:state.current.id]; + [self getInAppMessagesFromServer]; +} + +#pragma mark OSUserStateObserver Methods +- (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state { + NSLog(@"❌ OSMessagingController onUserStateDidChangeWithState: %@", [state jsonRepresentation]); + if (state.current.onesignalId && shouldRetryGetInAppMessagesOnUserChange) { + shouldRetryGetInAppMessagesOnUserChange = false; + [self getInAppMessagesFromServer]; + } +} + +#pragma mark OSUserJwtConfigListener Methods +- (void)onRequiresUserAuthChangedFrom:(enum OSRequiresUserAuth)from to:(enum OSRequiresUserAuth)to { + // This callback is unused, the controller will fetch when subscription ID changes +} + +- (void)onJwtUpdatedWithExternalId:(NSString *)externalId token:(NSString *)token { + NSLog(@"❌ OSMessagingController onJwtUpdatedWithExternalId: %@ token: %@", externalId, token); + if (![token isEqual: OS_JWT_TOKEN_INVALID] && shouldRetryGetInAppMessagesOnJwtUpdated) { + shouldRetryGetInAppMessagesOnJwtUpdated = false; + [self getInAppMessagesFromServer]; + } } - (void)dealloc { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OSInAppMessagingDefines.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OSInAppMessagingDefines.h index 9bda4a68b..8ee3db3b9 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OSInAppMessagingDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OSInAppMessagingDefines.h @@ -28,6 +28,8 @@ #ifndef OSInAppMessagingDefines_h #define OSInAppMessagingDefines_h +// OSMessagingController name +#define OS_MESSAGING_CONTROLLER @"OSMessagingController" // IAM display position enums typedef NS_ENUM(NSUInteger, OSInAppMessageDisplayPosition) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.h index 11d0faa67..fda5f4def 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.h @@ -32,7 +32,7 @@ + (Class_Nonnull)InAppMessages; + (void)start; -+ (void)getInAppMessagesFromServer:(NSString * _Nullable)subscriptionId; ++ (void)getInAppMessagesFromServer; + (void)onApplicationDidBecomeActive; + (void)migrate; @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m index f9540813c..27fef03df 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/OneSignalInAppMessages.m @@ -40,8 +40,8 @@ + (void)start { [OSMessagingController start]; } -+ (void)getInAppMessagesFromServer:(NSString * _Nullable)subscriptionId { - [OSMessagingController.sharedInstance getInAppMessagesFromServer:subscriptionId]; ++ (void)getInAppMessagesFromServer { + [OSMessagingController.sharedInstance getInAppMessagesFromServer]; } + (void)onApplicationDidBecomeActive { diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h index c37b4bbf1..2b1f2a381 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.h @@ -29,7 +29,7 @@ #import "OSInAppMessageClickResult.h" @interface OSRequestGetInAppMessages : OneSignalRequest -+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId; ++ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId withAliasLabel:(NSString * _Nonnull)aliasLabel withAliasId:(NSString * _Nonnull)aliasId withHeader:(NSDictionary * _Nonnull)header; @end @interface OSRequestInAppMessageViewed : OneSignalRequest diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m index 6f837a243..f0b2defe6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Requests/OSInAppMessagingRequests.m @@ -28,11 +28,15 @@ of this software and associated documentation files (the "Software"), to deal #import "OSInAppMessagingRequests.h" @implementation OSRequestGetInAppMessages -+ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId { ++ (instancetype _Nonnull)withSubscriptionId:(NSString * _Nonnull)subscriptionId + withAliasLabel:(NSString * _Nonnull)aliasLabel + withAliasId:(NSString * _Nonnull)aliasId + withHeader:(NSDictionary * _Nonnull)header { let request = [OSRequestGetInAppMessages new]; request.method = GET; NSString *appId = [OneSignalConfigManager getAppId]; - request.path = [NSString stringWithFormat:@"apps/%@/subscriptions/%@/iams", appId, subscriptionId]; + request.additionalHeaders = header; + request.path = [NSString stringWithFormat:@"apps/%@/users/by/%@/%@/subscriptions/%@/iams", appId, aliasLabel, aliasId, subscriptionId]; return request; } @end diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift index b5cb9e051..c0f9b161b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSAliasPair.swift @@ -29,8 +29,8 @@ An alias label and alias ID pair to represent a user. */ @objc public class OSAliasPair: NSObject { - public let label: String - public let id: String + @objc public let label: String + @objc public let id: String public init(_ label: String, _ id: String) { self.label = label diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 5b49733bf..db2745a86 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -416,16 +416,8 @@ + (void)startNewSessionInternal { // TODO: Figure out if Create User also sets session_count automatically on backend [OneSignalUserManagerImpl.sharedInstance startNewSession]; - - // This is almost always going to be nil the first time. - // The OSMessagingController is an OSPushSubscriptionObserver so that we pull IAMs once we have the sub id - NSString *subscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId; - if (subscriptionId) { - let oneSignalInAppMessages = NSClassFromString(ONE_SIGNAL_IN_APP_MESSAGES_CLASS_NAME); - if (oneSignalInAppMessages != nil && [oneSignalInAppMessages respondsToSelector:@selector(getInAppMessagesFromServer:)]) { - [oneSignalInAppMessages performSelector:@selector(getInAppMessagesFromServer:) withObject:subscriptionId]; - } - } + + [self fetchInAppMessages]; // The below means there are NO IAMs until on_session returns // because they can be ended, paused, or deleted from the server, or your segment has changed and you're no longer eligible @@ -440,6 +432,13 @@ + (void)startNewSessionInternal { // [OSMessagingController.sharedInstance updateInAppMessagesFromCache]; // go to controller } ++ (void)fetchInAppMessages { + let oneSignalInAppMessages = NSClassFromString(ONE_SIGNAL_IN_APP_MESSAGES_CLASS_NAME); + if (oneSignalInAppMessages != nil && [oneSignalInAppMessages respondsToSelector:@selector(getInAppMessagesFromServer)]) { + [oneSignalInAppMessages performSelector:@selector(getInAppMessagesFromServer)]; + } +} + + (void)startInAppMessages { let oneSignalInAppMessages = NSClassFromString(ONE_SIGNAL_IN_APP_MESSAGES_CLASS_NAME); if (oneSignalInAppMessages != nil && [oneSignalInAppMessages respondsToSelector:@selector(start)]) { From e7eeba5b2d6d66aaf8c448c3657386a81c022c80 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Tue, 3 Sep 2024 13:01:19 -0700 Subject: [PATCH 24/56] create simple way to fire invalid Jwt callback --- .../OneSignalDevApp/OneSignalDevApp/AppDelegate.m | 15 +++++++++++++-- .../Source/Executors/OSUserExecutor.swift | 6 ++++++ .../Source/OneSignalUserManagerImpl.swift | 5 +++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index dfa6df4c0..fba0a3dc9 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -46,6 +46,10 @@ @implementation AppDelegate OneSignalNotificationCenterDelegate *_notificationDelegate; +// ECM Should we ship these typedefs in OneSignalFramework.h to make them available to Objective C customers? +typedef void (^JwtCompletionBlock)(NSString*); +typedef void (^JwtExpiredBlock)(NSString *, JwtCompletionBlock); + - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // [FIRApp configure]; @@ -72,6 +76,13 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [OneSignal.User addObserver:self]; [OneSignal.Notifications addPermissionObserver:self]; [OneSignal.InAppMessages addClickListener:self]; + + JwtExpiredBlock expiredBlock = ^(NSString *externalId, JwtCompletionBlock completion){ + NSLog(@"JWT expired for external id: %@", externalId); + completion(@"test"); + }; + + [OneSignal.User onJwtExpiredWithExpiredHandler:expiredBlock]; NSLog(@"UNUserNotificationCenter.delegate: %@", UNUserNotificationCenter.currentNotificationCenter.delegate); @@ -86,8 +97,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -#define ONESIGNAL_APP_ID_DEFAULT @"STAGING_APP_HERE" -#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"YOUR_APP_ID_HERE" +#define ONESIGNAL_APP_ID_DEFAULT @"0139bd6f-451f-438c-8886-4e0f0fe3a085" +#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"0139bd6f-451f-438c-8886-4e0f0fe3a085" + (NSString*)getOneSignalAppId { NSString* userDefinedAppId = [[NSUserDefaults standardUserDefaults] objectForKey:ONESIGNAL_APP_ID_KEY_FOR_TESTING]; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 7bb555f15..99848d5aa 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -339,6 +339,9 @@ extension OSUserExecutor { } onFailure: { error in if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) + if responseType == .unauthorized { + OneSignalUserManagerImpl.sharedInstance.invalidJwtConfigResponse(error: nsError) + } if responseType != .retryable { // A failed create user request would leave the SDK in a bad state // Don't remove the request from cache and pause the operation repo @@ -687,6 +690,9 @@ extension OSUserExecutor: OSUserJwtConfigListener { } func onJwtUpdated(externalId: String, token: String?) { + /* + Handle pending 401 requests again + */ print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index d5dec443e..7ec17e5b2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -684,6 +684,11 @@ extension OneSignalUserManagerImpl { public func subscribeToJwtConfig(_ listener: OSUserJwtConfigListener, key: String) { jwtConfig.subscribe(listener, key: key) } + + @objc + public func invalidJwtConfigResponse(error: NSError) { + fireJwtExpired() + } } extension OneSignalUserManagerImpl: OSUser { From c0ad6c3e739513737f2a4b5944c49e8108fac167 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Tue, 3 Sep 2024 16:56:29 -0700 Subject: [PATCH 25/56] Adding OSJwtInvalidatedEvent and updating the JWT callback This uses the callback in the UserExecutor only --- .../OneSignal.xcodeproj/project.pbxproj | 4 ++ .../Controller/OSMessagingController.m | 1 + .../Source/Executors/OSUserExecutor.swift | 18 +++++-- .../Source/OSJwtInvalidatedEvent.swift | 39 ++++++++++++++ .../Source/OneSignalUserManagerImpl.swift | 53 ++++++++++++------- 5 files changed, 91 insertions(+), 24 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index e85a0da7b..860a37882 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -348,6 +348,7 @@ DE16C14524D3724700670EFA /* OneSignalLifecycleObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */; }; DE16C14724D3727200670EFA /* OneSignalLifecycleObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = DE16C14624D3727200670EFA /* OneSignalLifecycleObserver.h */; }; DE16C17024D3989A00670EFA /* OneSignalLifecycleObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */; }; + DE1DD0602C87D87B00787071 /* OSJwtInvalidatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */; }; DE20425E24E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; DE20425F24E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; DE20426024E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; @@ -1510,6 +1511,7 @@ CACBAAAB218A662B000ACAA5 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalLifecycleObserver.m; sourceTree = ""; }; DE16C14624D3727200670EFA /* OneSignalLifecycleObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLifecycleObserver.h; sourceTree = ""; }; + DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSJwtInvalidatedEvent.swift; sourceTree = ""; }; DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIApplication+OneSignal.h"; sourceTree = ""; }; DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = ""; }; DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = ""; }; @@ -2492,6 +2494,7 @@ 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */, 3CE795F828DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift */, 3C5117162B15C31E00563465 /* OSUserState.swift */, + DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */, ); path = Source; sourceTree = ""; @@ -4286,6 +4289,7 @@ files = ( 3CE795F928DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift in Sources */, DE69E1AC282ED87A0090BB3D /* OneSignalUserManagerImpl.swift in Sources */, + DE1DD0602C87D87B00787071 /* OSJwtInvalidatedEvent.swift in Sources */, 3C9AD6CF2B228B7800BC1540 /* OSRequestAddAliases.swift in Sources */, 3C9AD6D32B228BB000BC1540 /* OSRequestUpdateProperties.swift in Sources */, 3C9AD6CD2B228B6300BC1540 /* OSRequestFetchUser.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index 629a51b13..e51b7f698 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -250,6 +250,7 @@ - (void)initializeTriggerController { - (void)updateInAppMessagesFromCache { self.messages = [OneSignalUserDefaults.initStandard getSavedCodeableDataForKey:OS_IAM_MESSAGES_ARRAY defaultValue:[NSArray new]]; + // ECM THIS NEEDS TO RUN ON THE MAIN THREAD [self evaluateMessages]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 99848d5aa..0031d58fc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -340,12 +340,18 @@ extension OSUserExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .unauthorized { - OneSignalUserManagerImpl.sharedInstance.invalidJwtConfigResponse(error: nsError) - } - if responseType != .retryable { + guard let externalId = request.identityModel.externalId else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor no externalId for unauthorized request.") + return + } + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: nsError) + request.sentToClient = false + } else if responseType != .retryable { // A failed create user request would leave the SDK in a bad state // Don't remove the request from cache and pause the operation repo // We will retry this request on a new session + + // We can't do this anymore for 401s OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true request.sentToClient = false } @@ -691,7 +697,11 @@ extension OSUserExecutor: OSUserJwtConfigListener { func onJwtUpdated(externalId: String, token: String?) { /* - Handle pending 401 requests again + ECM + Do we actually even need this callback? + Requests that are invalidated do not pass prepare for execution + Once they are valid they will pass prepare for execution. + We could use this callback to optimize sending requests immediately */ print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift new file mode 100644 index 000000000..0a633f7e0 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift @@ -0,0 +1,39 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + + +import Foundation + +@objc public class OSJwtInvalidatedEvent: NSObject { + @objc public let externalId: String + @objc public let message: String + + init(externalId: String, message: String) { + self.externalId = externalId + self.message = message + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 7ec17e5b2..fe5c00a54 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -77,9 +77,8 @@ import OneSignalNotifications // Language func setLanguage(_ language: String) // JWT Token Expire - typealias OSJwtCompletionBlock = (_ newJwtToken: String) -> Void - typealias OSJwtExpiredHandler = (_ externalId: String, _ completion: OSJwtCompletionBlock) -> Void - func onJwtExpired(expiredHandler: @escaping OSJwtExpiredHandler) + typealias OSJwtInvalidatedHandler = (_ event: OSJwtInvalidatedEvent) -> Void + func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) } /** @@ -125,7 +124,7 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var hasCalledStart = false - private var jwtExpiredHandler: OSJwtExpiredHandler? + private var jwtInvalidatedHandler: OSJwtInvalidatedHandler? let jwtConfig: OSUserJwtConfig var user: OSUserInternal { @@ -591,18 +590,6 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { identityModelRepo.updateJwtToken(externalId: externalId, token: token) } - - private func fireJwtExpired() { - guard let externalId = user.identityModel.externalId, let jwtExpiredHandler = self.jwtExpiredHandler else { - return - } - jwtExpiredHandler(externalId) { [self] (newToken) -> Void in - guard user.identityModel.externalId == externalId else { - return - } - user.identityModel.jwtBearerToken = newToken - } - } } // MARK: - Sessions @@ -686,14 +673,40 @@ extension OneSignalUserManagerImpl { } @objc - public func invalidJwtConfigResponse(error: NSError) { - fireJwtExpired() + public func invalidateJwtForExternalId(externalId: String, error: NSError) { + guard let identityModel = identityModelRepo.get(externalId: externalId) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "Unable to find identity model for externalId: \(externalId)") + return + } + identityModel.jwtBearerToken = nil + + let message = getMessageFromJwtError(error) + fireJwtExpired(externalId: externalId, message: message) + } + + + private func fireJwtExpired(externalId: String, message: String) { + guard let jwtInvalidatedHandler = self.jwtInvalidatedHandler else { + return + } + let invalidatedEvent = OSJwtInvalidatedEvent(externalId: externalId, message: message) + + jwtInvalidatedHandler(invalidatedEvent) + } + + private func getMessageFromJwtError(_ error: NSError) -> String { + if let returnedObject = error.userInfo["returned"] as? Dictionary { + if let errors = returnedObject["errors"] as? Array> { + return errors[0]["title"] as? String ?? error.localizedDescription + } + } + return error.localizedDescription } } extension OneSignalUserManagerImpl: OSUser { - public func onJwtExpired(expiredHandler: @escaping OSJwtExpiredHandler) { - jwtExpiredHandler = expiredHandler + public func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) { + jwtInvalidatedHandler = invalidatedHandler } public var User: OSUser { From f926109fe78607c93fa31d0b3df3f488d46a458c Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Tue, 3 Sep 2024 17:07:05 -0700 Subject: [PATCH 26/56] refactor in UserExecutor to handle 401s --- .../Source/Executors/OSUserExecutor.swift | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 0031d58fc..9eaccd6f1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -344,7 +344,7 @@ extension OSUserExecutor { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor no externalId for unauthorized request.") return } - OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError) request.sentToClient = false } else if responseType != .retryable { // A failed create user request would leave the SDK in a bad state @@ -360,6 +360,12 @@ extension OSUserExecutor { } } } + + func handleUnauthorizedError(externalId: String, error: NSError) { + if (jwtConfig.isRequired ?? false) { + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) + } + } func fetchIdentityBySubscription(_ user: OSUserInternal) { let request = OSRequestFetchIdentityBySubscription(identityModel: user.identityModel, pushSubscriptionModel: user.pushSubscriptionModel) @@ -566,6 +572,11 @@ extension OSUserExecutor { // The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil OneSignalUserManagerImpl.sharedInstance._logout() + } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + if let externalId = request.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false } else if responseType != .retryable { // If the error is not retryable, remove from cache and queue self.removeFromQueue(request) From 02acc2af5a6cc5b38b48fd7fa2e758f2919058dc Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 4 Sep 2024 12:56:23 -0700 Subject: [PATCH 27/56] Unit tests for UserExecutor with identity verification on --- .../OneSignalUserMocks/MockUserDefines.swift | 2 + .../OneSignalUserMocks/MockUserRequests.swift | 18 +++ .../Executors/UserExecutorTests.swift | 127 ++++++++++++++++++ 3 files changed, 147 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index 86ccd5823..935ac46e8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -5,3 +5,5 @@ public let userB_OSID = "test_user_b_onesignal_id" public let userB_EUID = "test_user_b_external_id" public let testPushSubId = "test_push_subscription_id" + +public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 77560bd28..cf1f71fb1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -59,6 +59,14 @@ public class MockUserRequests: NSObject { "properties": properties ] } + + public static func testUnauthorizedailureError() -> NSError { + let userInfo = ["returned": [ + "errors": [["title":"token has invalid claims: token is expired", "code":"auth-0"]], + "httpStatusCode": 401, + ]] + return NSError(domain: "not-important", code: 401, userInfo: userInfo) + } } // MARK: - Set Up Default Client Responses @@ -97,6 +105,16 @@ extension MockUserRequests { response: userResponse ) } + + public static func setUnauthorizedCreateUserFailureResponses(with client: MockOneSignalClient, externalId: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"", error: error) + } + + public static func setUnauthorizedFetchUserFailureResponses(with client: MockOneSignalClient, onesignalId: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"", error: error) + } public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) { var osid: String diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index 25b203fa0..ab8ce240b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -207,4 +207,131 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) XCTAssertTrue(mocks.newRecordsState.records.isEmpty) } + + func testCreateUser_IdentityVerificationRequired_butNoToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: "") + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) + + /* When */ + mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should not execute this request since identity verification is required, but no token was set + XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertEqual(mocks.newRecordsState.records.count, 0) + } + + func testCreateUser_IdentityVerificationRequired_withToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: "") + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + newIdentityModel.jwtBearerToken = userA_JwtToken + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) + + /* When */ + mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + } + + func testCreateUser_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + newIdentityModel.jwtBearerToken = userA_JwtToken + MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } + + func testFetchUser_IdentityVerificationRequired_butNoToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: "") + let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + + /* When */ + mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should not execute this request since identity verification is required, but no token was set + XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + } + + func testFetchUser_IdentityVerificationRequired_withToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: "") + let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + newIdentityModel.jwtBearerToken = userA_JwtToken + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + + /* When */ + mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should not execute this request since identity verification is required, but no token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + } + + func testFetchUser_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + newIdentityModel.jwtBearerToken = userA_JwtToken + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } } From 2530e2fadd2dbc52cba002eb843b06e779b04f1a Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 4 Sep 2024 14:15:36 -0700 Subject: [PATCH 28/56] Fire callback for 401 error in property executor Also adds tests for the property executor and refactors some testing code that can be shared with other executor tests --- .../OneSignal.xcodeproj/project.pbxproj | 8 + .../OSPropertyOperationExecutor.swift | 12 ++ .../OneSignalUserMocks/MockUserRequests.swift | 13 ++ .../OneSignalExecutorMocks.swift | 68 ++++++++ .../Executors/PropertyExecutorTests.swift | 145 ++++++++++++++++++ .../Executors/UserExecutorTests.swift | 34 +--- 6 files changed, 251 insertions(+), 29 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 860a37882..4b644f52c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -354,6 +354,8 @@ DE20426024E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; DE2D8F452947D85800844084 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D17F927026BA3002D3A5D /* OneSignalExtension.framework */; }; DE2D8F4A2947D86200844084 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; }; + DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; }; + DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; }; DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1514,6 +1516,8 @@ DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSJwtInvalidatedEvent.swift; sourceTree = ""; }; DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIApplication+OneSignal.h"; sourceTree = ""; }; DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = ""; }; + DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = ""; }; + DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = ""; }; DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = ""; }; DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = ""; }; DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = ""; }; @@ -2171,6 +2175,7 @@ 3C87066F2BDE0957000D8CD2 /* MockUserRequests.swift */, 3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */, 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */, + DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */, ); path = OneSignalUserMocks; sourceTree = ""; @@ -2201,6 +2206,7 @@ isa = PBXGroup; children = ( 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */, + DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */, ); path = Executors; sourceTree = ""; @@ -4130,6 +4136,7 @@ 3C8706702BDE0957000D8CD2 /* MockUserRequests.swift in Sources */, 3C8706722BDEE076000D8CD2 /* MockUserDefines.swift in Sources */, 3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */, + DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4138,6 +4145,7 @@ buildActionMask = 2147483647; files = ( 3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */, + DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */, 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 6c67ca698..338f0047f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -182,6 +182,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor.processDeltaQueue dropped: \(delta)") + // ECM Remove the delta here. Need an iterator to do it in place continue } @@ -266,6 +267,12 @@ class OSPropertyOperationExecutor: OSOperationExecutor { executeUpdatePropertiesRequest(request, inBackground: inBackground) } } + + func handleUnauthorizedError(externalId: String, error: NSError) { + if (jwtConfig.isRequired ?? false) { + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) + } + } func executeUpdatePropertiesRequest(_ request: OSRequestUpdateProperties, inBackground: Bool) { guard !request.sentToClient else { @@ -310,6 +317,11 @@ class OSPropertyOperationExecutor: OSOperationExecutor { // The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil OneSignalUserManagerImpl.sharedInstance._logout() + } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + if let externalId = request.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue self.updateRequestQueue.removeAll(where: { $0 == request}) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index cf1f71fb1..d2bd416fe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -115,6 +115,19 @@ extension MockUserRequests { let error = testUnauthorizedailureError() client.setMockFailureResponseForRequest(request:"", error: error) } + + public static func setUnauthorizedUpdatePropertiesFailureResponses(with client: MockOneSignalClient, tags: [String: String]) { + let error = testUnauthorizedailureError() + + let params: NSDictionary = [ + "properties": [ + "tags": tags + ], + "refresh_device_metadata": false + ] + + client.setMockFailureResponseForRequest(request:"", error: error) + } public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) { var osid: String diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift new file mode 100644 index 000000000..7965fd0f4 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift @@ -0,0 +1,68 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +@testable import OneSignalUser + +@objc +open class OneSignalExecutorMocks: NSObject { + public let client = MockOneSignalClient() + public let newRecordsState = MockNewRecordsState() + public let jwtConfig = OSUserJwtConfig() + + override public init() { + super.init() + OneSignalCoreImpl.setSharedClient(client) + } + + @objc + open func setAuthRequired(_ required: Bool) { + // Set User Manager's JWT to off, or it blocks requests in prepareForExecution + OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = required + jwtConfig.isRequired = required + } + + open func createUserInstance(externalId: String) -> OSUserInternal { + let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: externalId], changeNotifier: OSEventProducer()) + let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) + let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()) + return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel) + } + + open func setUserManagerInternalUser(externalId: String, onesignalId: String? = nil) -> OSUserInternal { + let user = OneSignalUserManagerImpl.sharedInstance.setNewInternalUser( + externalId: externalId, + pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())) + if let onesignalId = onesignalId { + user.identityModel.addAliases([OS_ONESIGNAL_ID: onesignalId]) + } + return user + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift new file mode 100644 index 000000000..fcce17b7a --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -0,0 +1,145 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalCore +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +import OneSignalUserMocks +@testable import OneSignalUser + +private class Mocks: OneSignalExecutorMocks { + var propertyExecutor: OSPropertyOperationExecutor! + + override init() { + super.init() + propertyExecutor = OSPropertyOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + } +} + +final class PropertyExecutorTests: XCTestCase { + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + // App ID is set because requests have guards against null App ID + OneSignalConfigManager.setAppId("test-app-id") + // Temp. logging to help debug during testing + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { } + + func testUpdateTagsSendsWhenProcessed() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(false) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let tags = ["testUserA" : "true"] + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + /* When */ + mocks.propertyExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + } + + func testUpdateTags_IdentityVerificationRequired_butNoToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let tags = ["testUserA" : "true"] + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + /* When */ + mocks.propertyExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + } + + func testUpdateTags_IdentityVerificationRequired_withToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let tags = ["testUserA" : "true"] + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + /* When */ + mocks.propertyExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + } + + func testCreateUser_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + + + + let tags = ["testUserA" : "true"] + MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.propertyExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index ab8ce240b..f76b55d20 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -33,36 +33,12 @@ import OneSignalOSCoreMocks import OneSignalUserMocks @testable import OneSignalUser -/// This class has helpers that can be used in other tests and can be extracted out, as they are used -private class Mocks { - let client = MockOneSignalClient() - let newRecordsState = MockNewRecordsState() - let jwtConfig = OSUserJwtConfig() - let userExecutor: OSUserExecutor - - init() { - OneSignalCoreImpl.setSharedClient(client) - userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) - } - - func setAuthRequired(_ required: Bool) { - // Set User Manager's JWT to off, or it blocks requests in prepareForExecution - OneSignalUserManagerImpl.sharedInstance.jwtConfig.isRequired = required - jwtConfig.isRequired = required - } +private class Mocks: OneSignalExecutorMocks { + var userExecutor: OSUserExecutor! - func createUserInstance(externalId: String) -> OSUserInternal { - let identityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: externalId], changeNotifier: OSEventProducer()) - let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) - let pushModel = OSSubscriptionModel(type: .push, address: "", subscriptionId: nil, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()) - return OSUserInternalImpl(identityModel: identityModel, propertiesModel: propertiesModel, pushSubscriptionModel: pushModel) - } - - func setUserManagerInternalUser(externalId: String) -> OSUserInternal { - return OneSignalUserManagerImpl.sharedInstance.setNewInternalUser( - externalId: externalId, - pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer()) - ) + override init() { + super.init() + userExecutor = OSUserExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) } } From c0156d77ee22df64bb716b194b742a35fb6f5330 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 4 Sep 2024 15:08:05 -0700 Subject: [PATCH 29/56] fire Jwt callback from subscription executor Includes tests Currently delete and update requests don't have an identity model attached. This may need to be changed for JWT --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../OSSubscriptionOperationExecutor.swift | 27 ++- .../OneSignalUserMocks/MockUserDefines.swift | 5 +- .../OneSignalUserMocks/MockUserRequests.swift | 15 ++ .../Executors/PropertyExecutorTests.swift | 2 +- .../SubscriptionsExecutorTests.swift | 196 ++++++++++++++++++ 6 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 4b644f52c..9af96750d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -356,6 +356,7 @@ DE2D8F4A2947D86200844084 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D188027037F43002D3A5D /* OneSignalOutcomes.framework */; }; DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; }; DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; }; + DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */; }; DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1518,6 +1519,7 @@ DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = ""; }; DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = ""; }; DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = ""; }; + DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsExecutorTests.swift; sourceTree = ""; }; DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = ""; }; DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = ""; }; DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = ""; }; @@ -2207,6 +2209,7 @@ children = ( 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */, DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */, + DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */, ); path = Executors; sourceTree = ""; @@ -4149,6 +4152,7 @@ 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, + DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */, 3CDE664C2BFC2A56006DA114 /* OneSignalUserObjcTests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index eef6d7da2..5268ca395 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -278,6 +278,12 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } } + + func handleUnauthorizedError(externalId: String, error: NSError) { + if (jwtConfig.isRequired ?? false) { + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) + } + } func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) { guard !request.sentToClient else { @@ -331,6 +337,11 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { // The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil OneSignalUserManagerImpl.sharedInstance._logout() + } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + if let externalId = request.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue self.addRequestQueue.removeAll(where: { $0 == request}) @@ -375,7 +386,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) - if responseType != .retryable { + if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + // ECM The delete subscription request doesn't have an identity model? + if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false + } else if responseType != .retryable { // Fail, no retry, remove from cache and queue // If this request returns a missing status, that is ok as this is a delete request self.removeRequestQueue.removeAll(where: { $0 == request}) @@ -417,7 +434,13 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) - if responseType != .retryable { + if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + // ECM The update subscription request doesn't have an identity model? + if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false + } else if responseType != .retryable { // Fail, no retry, remove from cache and queue self.updateRequestQueue.removeAll(where: { $0 == request}) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index 935ac46e8..61793b1c0 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -5,5 +5,8 @@ public let userB_OSID = "test_user_b_onesignal_id" public let userB_EUID = "test_user_b_external_id" public let testPushSubId = "test_push_subscription_id" - +public let testEmailSubId = "test_email_subscription_id" +public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1fadf3a04be69" public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" + +public let userA_email = "userA@onesignal.com" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index d2bd416fe..4d155294a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -128,6 +128,21 @@ extension MockUserRequests { client.setMockFailureResponseForRequest(request:"", error: error) } + + public static func setUnauthorizedAddEmailFailureResponse(with client: MockOneSignalClient, email: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"", error: error) + } + + public static func setUnauthorizedRemoveEmailFailureResponse(with client: MockOneSignalClient, email: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"", error: error) + } + + public static func setUnauthorizedUpdateSubscriptionFailureResponse(with client: MockOneSignalClient, token: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error) + } public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) { var osid: String diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index fcce17b7a..6820dee43 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -113,7 +113,7 @@ final class PropertyExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) } - func testCreateUser_IdentityVerificationRequired_withInvalidToken() { + func testUpdateProperty_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift new file mode 100644 index 000000000..0a81c2d8f --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -0,0 +1,196 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalCore +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +import OneSignalUserMocks +@testable import OneSignalUser + +private class Mocks: OneSignalExecutorMocks { + var subscriptionExecutor: OSSubscriptionOperationExecutor! + + override init() { + super.init() + subscriptionExecutor = OSSubscriptionOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + } +} + +final class SubscriptionExecutorTests: XCTestCase { + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + // App ID is set because requests have guards against null App ID + OneSignalConfigManager.setAppId("test-app-id") + // Temp. logging to help debug during testing + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { } + + func testAddEmailSendsWhenProcessed() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(false) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let email = userA_email + MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + } + + func testAddEmail_IdentityVerificationRequired_butNoToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let email = userA_email + MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + } + + func testAddEmail_IdentityVerificationRequired_withToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let email = userA_email + MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + } + + func testAddEmail_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let email = userA_email + MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } + + func testDeleteEmail_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let email = userA_email + MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } + + func testUpdateSubscription_IdentityVerificationRequired_withInvalidToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let token = testPushToken + MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value:token)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } +} From d5514e4cff08252085317d6b21c2e92730a5759d Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 4 Sep 2024 15:45:48 -0700 Subject: [PATCH 30/56] fire Jwt callback for add and remove aliases 401 Includes tests --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../OSIdentityOperationExecutor.swift | 19 +- .../OneSignalUserMocks/MockUserDefines.swift | 4 + .../OneSignalUserMocks/MockUserRequests.swift | 10 + .../Executors/IdentityExecutorTests.swift | 171 ++++++++++++++++++ 5 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 9af96750d..2053fd1fc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -357,6 +357,7 @@ DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */; }; DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */; }; DE3568F02C89067400AF447C /* SubscriptionsExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */; }; + DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */; }; DE3784842888CFF900453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784852888D00300453A8E /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; }; DE3784862888D00B00453A8E /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE69E19B282ED8060090BB3D /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -1520,6 +1521,7 @@ DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = ""; }; DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneSignalExecutorMocks.swift; sourceTree = ""; }; DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionsExecutorTests.swift; sourceTree = ""; }; + DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IdentityExecutorTests.swift; sourceTree = ""; }; DE3CD2FE270FA9F200A5BECD /* OSOutcomes.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSOutcomes.m; sourceTree = ""; }; DE51DDE3294262AB0073D5C4 /* OSRemoteParamController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OSRemoteParamController.m; sourceTree = ""; }; DE51DDE4294262AB0073D5C4 /* OSRemoteParamController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OSRemoteParamController.h; sourceTree = ""; }; @@ -2210,6 +2212,7 @@ 3CF11E3C2C6D6155002856F5 /* UserExecutorTests.swift */, DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */, DE3568EF2C89067400AF447C /* SubscriptionsExecutorTests.swift */, + DE3568F12C8911EA00AF447C /* IdentityExecutorTests.swift */, ); path = Executors; sourceTree = ""; @@ -4149,6 +4152,7 @@ files = ( 3CF11E3D2C6D6155002856F5 /* UserExecutorTests.swift in Sources */, DE3568EA2C88F56600AF447C /* PropertyExecutorTests.swift in Sources */, + DE3568F22C8911EA00AF447C /* IdentityExecutorTests.swift in Sources */, 3C67F77A2BEB2B710085A0F0 /* SwitchUserIntegrationTests.swift in Sources */, 3CC063EE2B6D7FE8002BB07F /* OneSignalUserTests.swift in Sources */, 3CC890352C5BF9A7002CB4CC /* UserConcurrencyTests.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 51e22a637..bd9a5c86d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -227,6 +227,12 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } } + + func handleUnauthorizedError(externalId: String, error: NSError) { + if (jwtConfig.isRequired ?? false) { + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) + } + } func executeAddAliasesRequest(_ request: OSRequestAddAliases, inBackground: Bool) { guard !request.sentToClient else { @@ -273,6 +279,11 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil OneSignalUserManagerImpl.sharedInstance._logout() + } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + if let externalId = request.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue self.addRequestQueue.removeAll(where: { $0 == request}) @@ -317,7 +328,13 @@ class OSIdentityOperationExecutor: OSOperationExecutor { self.dispatchQueue.async { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) - if responseType != .retryable { + if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { + if let externalId = request.identityModel.externalId { + self.handleUnauthorizedError(externalId: externalId, error: nsError) + } + request.sentToClient = false + } + else if responseType != .retryable { // Fail, no retry, remove from cache and queue // A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted self.removeRequestQueue.removeAll(where: { $0 == request}) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index 61793b1c0..ae8225a40 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -10,3 +10,7 @@ public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1 public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" public let userA_email = "userA@onesignal.com" + +public let userA_AliasLabel = "testAliasLabel" +public let userA_Aliases = [userA_AliasLabel : "userAValue"] + diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 4d155294a..2084249ab 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -143,6 +143,16 @@ extension MockUserRequests { let error = testUnauthorizedailureError() client.setMockFailureResponseForRequest(request:"OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error) } + + public static func setUnauthorizedAddAliasFailureResponse(with client: MockOneSignalClient, aliases: [String: String]) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"", error: error) + } + + public static func setUnauthorizedRemoveAliasFailureResponse(with client: MockOneSignalClient, aliasLabel: String) { + let error = testUnauthorizedailureError() + client.setMockFailureResponseForRequest(request:"OSRequestRemoveAlias with aliasLabel: \(aliasLabel)", error: error) + } public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) { var osid: String diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift new file mode 100644 index 000000000..bab751323 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -0,0 +1,171 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import XCTest +import OneSignalCore +import OneSignalOSCore +import OneSignalCoreMocks +import OneSignalOSCoreMocks +import OneSignalUserMocks +@testable import OneSignalUser + +private class Mocks: OneSignalExecutorMocks { + var identityExecutor: OSIdentityOperationExecutor! + + override init() { + super.init() + identityExecutor = OSIdentityOperationExecutor(newRecordsState: newRecordsState, jwtConfig: jwtConfig) + } +} + +final class IdentityExecutorTests: XCTestCase { + + override func setUpWithError() throws { + OneSignalCoreMocks.clearUserDefaults() + OneSignalUserMocks.reset() + // App ID is set because requests have guards against null App ID + OneSignalConfigManager.setAppId("test-app-id") + // Temp. logging to help debug during testing + OneSignalLog.setLogLevel(.LL_VERBOSE) + } + + override func tearDownWithError() throws { } + + func testAddAliasSendsWhenProcessed() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(false) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let aliases = userA_Aliases + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + /* When */ + mocks.identityExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + } + + func testAddAlias_IdentityVerificationRequired_butNoToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + let aliases = userA_Aliases + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + /* When */ + mocks.identityExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + } + + func testAddAlias_IdentityVerificationRequired_withToken() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let aliases = userA_Aliases + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + /* When */ + mocks.identityExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + } + + func testAddAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let aliases = userA_Aliases + MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.identityExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } + + func testRemoveAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_JwtToken + let aliases = userA_Aliases + MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + XCTAssertTrue(event.message == "token has invalid claims: token is expired") + invalidatedCallbackWasCalled = true + } + + /* When */ + mocks.identityExecutor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + } + + +} From 5afab13779486677c73506716692cdfd2ba764bf Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 11:05:44 -0700 Subject: [PATCH 31/56] Remove message for invalid handler --- iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m | 10 ++++------ .../OneSignalUser/Source/OSJwtInvalidatedEvent.swift | 6 ++---- .../Source/OneSignalUserManagerImpl.swift | 9 ++++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index fba0a3dc9..80a8deca5 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -76,13 +76,11 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [OneSignal.User addObserver:self]; [OneSignal.Notifications addPermissionObserver:self]; [OneSignal.InAppMessages addClickListener:self]; + - JwtExpiredBlock expiredBlock = ^(NSString *externalId, JwtCompletionBlock completion){ - NSLog(@"JWT expired for external id: %@", externalId); - completion(@"test"); - }; - - [OneSignal.User onJwtExpiredWithExpiredHandler:expiredBlock]; + [OneSignal.User onJwtInvalidatedWithInvalidatedHandler:^(OSJwtInvalidatedEvent * _Nonnull invalidatedEvent) { + NSLog(@"JWT INVALIDATED CALLBACK FOR: %@", invalidatedEvent.externalId); + }]; NSLog(@"UNUserNotificationCenter.delegate: %@", UNUserNotificationCenter.currentNotificationCenter.delegate); diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift index 0a633f7e0..bbc4528a4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift @@ -30,10 +30,8 @@ import Foundation @objc public class OSJwtInvalidatedEvent: NSObject { @objc public let externalId: String - @objc public let message: String - - init(externalId: String, message: String) { + + init(externalId: String) { self.externalId = externalId - self.message = message } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index fe5c00a54..9e30f6e1a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -674,22 +674,21 @@ extension OneSignalUserManagerImpl { @objc public func invalidateJwtForExternalId(externalId: String, error: NSError) { - guard let identityModel = identityModelRepo.get(externalId: externalId) else { + guard jwtConfig.isRequired == true, let identityModel = identityModelRepo.get(externalId: externalId) else { OneSignalLog.onesignalLog(.LL_ERROR, message: "Unable to find identity model for externalId: \(externalId)") return } identityModel.jwtBearerToken = nil - let message = getMessageFromJwtError(error) - fireJwtExpired(externalId: externalId, message: message) + fireJwtExpired(externalId: externalId) } - private func fireJwtExpired(externalId: String, message: String) { + private func fireJwtExpired(externalId: String) { guard let jwtInvalidatedHandler = self.jwtInvalidatedHandler else { return } - let invalidatedEvent = OSJwtInvalidatedEvent(externalId: externalId, message: message) + let invalidatedEvent = OSJwtInvalidatedEvent(externalId: externalId) jwtInvalidatedHandler(invalidatedEvent) } From 1d0b97f1daea5d5805b82919d8f16ac39cafa6fe Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 11:17:19 -0700 Subject: [PATCH 32/56] Fire JWT callback from IAMs This will always fire it for 401 even if JWT is not required, so validate the JWT config in the user manager before firing the callback --- .../Controller/OSMessagingController.m | 9 ++++++++- .../OneSignalUser/Source/OneSignalUserManagerImpl.swift | 5 ++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index e51b7f698..8154b458e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -251,7 +251,9 @@ - (void)initializeTriggerController { - (void)updateInAppMessagesFromCache { self.messages = [OneSignalUserDefaults.initStandard getSavedCodeableDataForKey:OS_IAM_MESSAGES_ARRAY defaultValue:[NSArray new]]; // ECM THIS NEEDS TO RUN ON THE MAIN THREAD - [self evaluateMessages]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self evaluateMessages]; + }); } /** @@ -324,11 +326,16 @@ - (void)getInAppMessagesFromServer { OSResponseStatusType responseType = [OSNetworkingUtils getResponseStatusType:error.code]; if (responseType == OSResponseStatusUnauthorized) { shouldRetryGetInAppMessagesOnJwtUpdated = true; + [self handleUnauthroizedError:error externalId:alias.id]; } [self updateInAppMessagesFromCache]; }]; } +- (void)handleUnauthroizedError:(NSError*)error externalId:(NSString *)externalId { + [OneSignalUserManagerImpl.sharedInstance invalidateJwtForExternalIdWithExternalId:externalId error:error]; +} + - (void)updateInAppMessagesFromServer:(NSArray *)newMessages { [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"updateInAppMessagesFromServer"]; self.messages = newMessages; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 9e30f6e1a..71902ce67 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -674,7 +674,10 @@ extension OneSignalUserManagerImpl { @objc public func invalidateJwtForExternalId(externalId: String, error: NSError) { - guard jwtConfig.isRequired == true, let identityModel = identityModelRepo.get(externalId: externalId) else { + guard jwtConfig.isRequired == true else { + return + } + guard let identityModel = identityModelRepo.get(externalId: externalId) else { OneSignalLog.onesignalLog(.LL_ERROR, message: "Unable to find identity model for externalId: \(externalId)") return } From f3cb3d8e3d2e05af7fbba6db9b3eb1180e209fbc Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 13:43:38 -0700 Subject: [PATCH 33/56] Store UserExecutor requests that need auth This PR adds a pendingAuthRequests dictionary that stores the requests that are waiting for an updated JWT keyed on externalId. When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again. Also update tests to account for the callback object change and add tests for the new case --- .../Source/Executors/OSUserExecutor.swift | 54 ++++++-- .../OneSignalUserMocks/MockUserDefines.swift | 5 +- .../Executors/IdentityExecutorTests.swift | 8 +- .../Executors/PropertyExecutorTests.swift | 5 +- .../SubscriptionsExecutorTests.swift | 11 +- .../Executors/UserExecutorTests.swift | 126 +++++++++++++++++- 6 files changed, 176 insertions(+), 33 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 9eaccd6f1..ef6dfec1a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -35,6 +35,7 @@ import OneSignalOSCore */ class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] + var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() private let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -289,6 +290,22 @@ extension OSUserExecutor { appendToQueue(request) executePendingRequests() } + + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { + self.dispatchQueue.async { + self.userRequestQueue.removeAll(where: { $0 == request}) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + } + } func executeCreateUserRequest(_ request: OSRequestCreateUser) { guard !request.sentToClient else { @@ -301,6 +318,11 @@ extension OSUserExecutor { request.pushSubscriptionModel = pushSubscriptionModel request.updatePushSubscriptionModel(pushSubscriptionModel) } + + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } guard request.prepareForExecution(newRecordsState: newRecordsState) else { @@ -344,7 +366,7 @@ extension OSUserExecutor { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor no externalId for unauthorized request.") return } - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) request.sentToClient = false } else if responseType != .retryable { // A failed create user request would leave the SDK in a bad state @@ -361,8 +383,9 @@ extension OSUserExecutor { } } - func handleUnauthorizedError(externalId: String, error: NSError) { + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { if (jwtConfig.isRequired ?? false) { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } @@ -376,6 +399,7 @@ extension OSUserExecutor { /** 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. + ECM can this ever succeed with identity verification on? */ func executeFetchIdentityBySubscriptionRequest(_ request: OSRequestFetchIdentityBySubscription) { guard !request.sentToClient else { @@ -485,7 +509,7 @@ extension OSUserExecutor { // This will hydrate the OneSignal ID for any pending requests self.createUser(aliasLabel: request.aliasLabel, aliasId: request.aliasId, identityModel: request.identityModelToUpdate) } - } else if responseType == .invalid || responseType == .unauthorized { + } else if responseType == .invalid || responseType == .unauthorized { //Identify User should never be called with identity verification on // Failed, no retry self.removeFromQueue(request) self.executePendingRequests() @@ -574,7 +598,7 @@ extension OSUserExecutor { OneSignalUserManagerImpl.sharedInstance._logout() } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -707,15 +731,23 @@ extension OSUserExecutor: OSUserJwtConfigListener { } func onJwtUpdated(externalId: String, token: String?) { - /* - ECM - Do we actually even need this callback? - Requests that are invalidated do not pass prepare for execution - Once they are valid they will pass prepare for execution. - We could use this callback to optimize sending requests immediately - */ + reQueuePendingRequestsForExternalId(externalId: externalId) print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } + + private func reQueuePendingRequestsForExternalId(externalId: String) { + self.dispatchQueue.async { + guard let requests = self.pendingAuthRequests[externalId] else { + return + } + for request in requests { + self.userRequestQueue.append(request) + } + self.pendingAuthRequests[externalId] = nil + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + self.executePendingRequests(withDelay: true) + } + } private func removeInvalidRequests() { self.dispatchQueue.async { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index ae8225a40..1f4cc6249 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -7,7 +7,10 @@ public let userB_EUID = "test_user_b_external_id" public let testPushSubId = "test_push_subscription_id" public let testEmailSubId = "test_email_subscription_id" public let testPushToken = "2b7347630b72265c83b1c1d2227f563ce6169d5aaf274b06f1a1fadf3a04be69" -public let userA_JwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" +public let userA_InvalidJwtToken = "byJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" + +public let userA_ValidJwtToken = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" +public let userB_ValidJwtToken = "fyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiIwMTM5YmQ2Zi00NTFmLTQzOGMtODg4Ni00ZTBmMGZlM2EwODUiLCJleHAiOjE3MjUzOTY3NTksImlkZW50aXR5Ijp7ImV4dGVybmFsX2lkIjoiZWxsaW90MTE0MCJ9LCJzdWJzY3JpcHRpb25zIjpbeyJ0eXBlIjoiRW1haWwiLCJ0b2tlbiI6InRlc3RAZG9tYWluLmNvbSJ9LHsidHlwZSI6IlNNUyIsInRva2VuIjoiKzEyMzQ1Njc4In1dfQ.wmtt8mH7wYpxmUDyx_l8ktfF4Eg-6y_4iOSsIEl3AxuQ5pEriCIRj-3P-NmSPO3jsSAGPeBRZQ-rRS5j-LbN1w" public let userA_email = "userA@onesignal.com" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index bab751323..e50af82b4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -100,7 +100,7 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) @@ -120,14 +120,13 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -147,14 +146,13 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index 6820dee43..f41b49d29 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -100,7 +100,7 @@ final class PropertyExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let tags = ["testUserA" : "true"] MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) @@ -120,7 +120,7 @@ final class PropertyExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken @@ -130,7 +130,6 @@ final class PropertyExecutorTests: XCTestCase { var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index 0a81c2d8f..32c753314 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -100,7 +100,7 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) @@ -120,14 +120,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -147,14 +146,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -174,14 +172,13 @@ final class SubscriptionExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_JwtToken + user.identityModel.jwtBearerToken = userA_InvalidJwtToken let token = testPushToken MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value:token)) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index f76b55d20..6407cdcd1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -42,6 +42,7 @@ private class Mocks: OneSignalExecutorMocks { } } + final class UserExecutorTests: XCTestCase { override func setUpWithError() throws { @@ -210,7 +211,7 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) /* When */ @@ -229,12 +230,11 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -273,7 +273,7 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) /* When */ @@ -292,12 +292,11 @@ final class UserExecutorTests: XCTestCase { let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - newIdentityModel.jwtBearerToken = userA_JwtToken + newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) var invalidatedCallbackWasCalled = false OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in - XCTAssertTrue(event.message == "token has invalid claims: token is expired") invalidatedCallbackWasCalled = true } @@ -310,4 +309,119 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } + + func testUserRequests_Retry_OnTokenUpdate() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! + + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + } + + /* When */ + executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 2) + } + + func testUserRequests_RetryAllRequests_OnTokenUpdate() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! + + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: userAIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 4) + } + + func testUserRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! + + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) + userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + let userBIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userB_OSID, OS_EXTERNAL_ID: userB_EUID], changeNotifier: OSEventProducer()) + userBIdentityModel.jwtBearerToken = userA_InvalidJwtToken + + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) + MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userB_EUID) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userB_EUID, identityModel: userBIdentityModel) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) + MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userB_EUID) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 3) + } } From aca978ef1c804014164396996b62958714cf9b76 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 15:38:46 -0700 Subject: [PATCH 34/56] store property update requests that need auth When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again. fixup property operations --- .../OSPropertyOperationExecutor.swift | 45 ++++++++++- .../Executors/PropertyExecutorTests.swift | 78 +++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 338f0047f..f5b7d19b4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -63,6 +63,7 @@ private struct OSCombinedProperties { class OSPropertyOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA] var deltaQueue: [OSDelta] = [] + var pendingAuthRequests: [String: [OSRequestUpdateProperties]] = [String:[OSRequestUpdateProperties]]() var updateRequestQueue: [OSRequestUpdateProperties] = [] let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -268,19 +269,44 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } } - func handleUnauthorizedError(externalId: String, error: NSError) { + func handleUnauthorizedError(externalId: String, error: NSError, request: OSRequestUpdateProperties) { if (jwtConfig.isRequired ?? false) { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } + + func pendRequestUntilAuthUpdated(_ request: OSRequestUpdateProperties, externalId: String?) { + self.dispatchQueue.async { + self.updateRequestQueue.removeAll(where: { $0 == request}) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + } + } func executeUpdatePropertiesRequest(_ request: OSRequestUpdateProperties, inBackground: Bool) { guard !request.sentToClient else { return } + + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } + + print("ECM executing properties request: %@", request.identityModel.externalId) request.sentToClient = true let backgroundTaskIdentifier = PROPERTIES_EXECUTOR_BACKGROUND_TASK + UUID().uuidString @@ -319,7 +345,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { OneSignalUserManagerImpl.sharedInstance._logout() } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -347,6 +373,21 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { func onJwtUpdated(externalId: String, token: String?) { print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") + reQueuePendingRequestsForExternalId(externalId: externalId) + } + + private func reQueuePendingRequestsForExternalId(externalId: String) { + self.dispatchQueue.async { + guard let requests = self.pendingAuthRequests[externalId] else { + return + } + for request in requests { + self.updateRequestQueue.append(request) + } + self.pendingAuthRequests[externalId] = nil + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + self.processRequestQueue(inBackground: false) + } } private func removeInvalidDeltasAndRequests() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index f41b49d29..b266359f4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -141,4 +141,82 @@ final class PropertyExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } + + func testUpdateRequests_Retry_OnTokenUpdate() { + + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_InvalidJwtToken + + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor! + + let tags = ["testUserA" : "true"] + MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 2) + } + + func testUpdateRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken + + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + userB.identityModel.jwtBearerToken = userA_InvalidJwtToken + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor! + + let tags = ["testUserA" : "true"] + MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) + + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userA.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userB.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + let updateRequests = mocks.client.executedRequests.filter { request in + request.isKind(of: OSRequestUpdateProperties.self) + } + XCTAssertEqual(updateRequests.count, 3) + } } From 1ae0deb40131d8e9394ef36e16303c8956dd09cb Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 16:10:47 -0700 Subject: [PATCH 35/56] store alias update requests that need auth When a requests fails with a 401 due to JWT or fails when preparing for execution we remove the request from the request queue and add it to the pending dictionary. Once we get the onJWTUpdated callback for that externalId we requeue the pending requests and try again. --- .../OSIdentityOperationExecutor.swift | 53 +++++++- .../Executors/IdentityExecutorTests.swift | 121 ++++++++++++++++++ 2 files changed, 171 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index bd9a5c86d..78d119495 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -34,6 +34,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // To simplify uncaching, we maintain separate request queues for each type var addRequestQueue: [OSRequestAddAliases] = [] var removeRequestQueue: [OSRequestRemoveAlias] = [] + var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -228,16 +229,38 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } - func handleUnauthorizedError(externalId: String, error: NSError) { + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { if (jwtConfig.isRequired ?? false) { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } + + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { + self.dispatchQueue.async { + self.addRequestQueue.removeAll(where: { $0 == request}) + self.removeRequestQueue.removeAll(where: { $0 == request}) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + } + } func executeAddAliasesRequest(_ request: OSRequestAddAliases, inBackground: Bool) { guard !request.sentToClient else { return } + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -281,7 +304,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { OneSignalUserManagerImpl.sharedInstance._logout() } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -301,6 +324,10 @@ class OSIdentityOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -330,7 +357,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } @@ -360,6 +387,26 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { func onJwtUpdated(externalId: String, token: String?) { print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") + reQueuePendingRequestsForExternalId(externalId: externalId) + } + + private func reQueuePendingRequestsForExternalId(externalId: String) { + self.dispatchQueue.async { + guard let requests = self.pendingAuthRequests[externalId] else { + return + } + for request in requests { + if let addRequest = request as? OSRequestAddAliases { + self.addRequestQueue.append(addRequest) + } else if let removeRequest = request as? OSRequestRemoveAlias { + self.removeRequestQueue.append(removeRequest) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + self.pendingAuthRequests[externalId] = nil + self.processRequestQueue(inBackground: false) + } } private func removeInvalidDeltasAndRequests() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index e50af82b4..545df66c5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -165,5 +165,126 @@ final class IdentityExecutorTests: XCTestCase { XCTAssertTrue(invalidatedCallbackWasCalled) } + func testAddAliasRequests_Retry_OnTokenUpdate() { + + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_InvalidJwtToken + + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! + + let aliases = userA_Aliases + MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases) + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 2) + } + + func testAddAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken + + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + userB.identityModel.jwtBearerToken = userA_InvalidJwtToken + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! + + let aliases = userA_Aliases + MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases) + + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases)) + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + let addAliasRequests = mocks.client.executedRequests.filter { request in + request.isKind(of: OSRequestAddAliases.self) + } + // It is 4 because setting user B's OneSignal ID counts as an add alias request + XCTAssertEqual(addAliasRequests.count, 4) + } + func testRemoveAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + + mocks.setAuthRequired(true) + + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken + + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + userB.identityModel.jwtBearerToken = userA_InvalidJwtToken + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! + + let aliases = userA_Aliases + MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) + + executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases)) + executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + let removeAliasRequests = mocks.client.executedRequests.filter { request in + request.isKind(of: OSRequestRemoveAlias.self) + } + + XCTAssertEqual(removeAliasRequests.count, 3) + } } From 3542d3407c91e05c561c5499206c23583f0b12c2 Mon Sep 17 00:00:00 2001 From: Elliot Mawby Date: Wed, 11 Sep 2024 16:32:10 -0700 Subject: [PATCH 36/56] Adding work in progress Subscription executor auth pends adds handling for pending unauthorized subscription executor requests. Doesn't yet handle prepare for execution properly No unit tests yet --- .../OSSubscriptionOperationExecutor.swift | 77 ++++++++++++++++++- 1 file changed, 73 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 5268ca395..4fc5c5308 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -35,6 +35,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { var addRequestQueue: [OSRequestCreateSubscription] = [] var removeRequestQueue: [OSRequestDeleteSubscription] = [] var updateRequestQueue: [OSRequestUpdateSubscription] = [] + var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() var subscriptionModels: [String: OSSubscriptionModel] = [:] let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -279,16 +280,40 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } - func handleUnauthorizedError(externalId: String, error: NSError) { + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { if (jwtConfig.isRequired ?? false) { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } + + + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { + self.dispatchQueue.async { + self.addRequestQueue.removeAll(where: { $0 == request}) + self.removeRequestQueue.removeAll(where: { $0 == request}) + self.updateRequestQueue.removeAll(where: { $0 == request}) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + } + } func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) { guard !request.sentToClient else { return } + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + return + } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -339,7 +364,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OneSignalUserManagerImpl.sharedInstance._logout() } else if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { if let externalId = request.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -359,6 +384,11 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } + // ECM TODO +// guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { +// pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) +// return +// } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -389,7 +419,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { // ECM The delete subscription request doesn't have an identity model? if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -410,6 +440,11 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } + // ECM TODO +// guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { +// pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) +// return +// } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -437,7 +472,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { // ECM The update subscription request doesn't have an identity model? if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError) + self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false } else if responseType != .retryable { @@ -454,6 +489,40 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } +extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { + func onRequiresUserAuthChanged(from: OneSignalOSCore.OSRequiresUserAuth, to: OneSignalOSCore.OSRequiresUserAuth) { + print("❌ OSSubscriptionOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") + // ECM TODO If auth changed from false or unknown to true, process requests + } + + func onJwtUpdated(externalId: String, token: String?) { + print("❌ OSSubscriptionOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") + reQueuePendingRequestsForExternalId(externalId: externalId) + } + + private func reQueuePendingRequestsForExternalId(externalId: String) { + self.dispatchQueue.async { + guard let requests = self.pendingAuthRequests[externalId] else { + return + } + for request in requests { + if let addRequest = request as? OSRequestCreateSubscription { + self.addRequestQueue.append(addRequest) + } else if let removeRequest = request as? OSRequestDeleteSubscription { + self.removeRequestQueue.append(removeRequest) + } else if let updateRequest = request as? OSRequestUpdateSubscription { + self.updateRequestQueue.append(updateRequest) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + self.pendingAuthRequests[externalId] = nil + self.processRequestQueue(inBackground: false) + } + } +} + extension OSSubscriptionOperationExecutor: OSLoggable { func logSelf() { print( From d90f0d20566e893620fb638ec6dffcd301a079db Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 23 Sep 2024 08:54:18 -0700 Subject: [PATCH 37/56] [nit] run swiftlint Run swiftlint and make a log more helpful --- .../OSIdentityOperationExecutor.swift | 19 ++- .../OSPropertyOperationExecutor.swift | 20 +-- .../OSSubscriptionOperationExecutor.swift | 17 +-- .../Source/Executors/OSUserExecutor.swift | 20 +-- .../Source/OSIdentityModelRepo.swift | 10 +- .../Source/OSJwtInvalidatedEvent.swift | 1 - .../Source/OneSignalUserManagerImpl.swift | 13 +- .../OneSignalUserMocks/MockUserDefines.swift | 3 +- .../OneSignalUserMocks/MockUserRequests.swift | 42 +++--- .../Executors/IdentityExecutorTests.swift | 114 +++++++------- .../Executors/PropertyExecutorTests.swift | 96 ++++++------ .../SubscriptionsExecutorTests.swift | 60 ++++---- .../Executors/UserExecutorTests.swift | 139 +++++++++--------- 13 files changed, 272 insertions(+), 282 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 78d119495..9d1442c8f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -34,7 +34,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // To simplify uncaching, we maintain separate request queues for each type var addRequestQueue: [OSRequestAddAliases] = [] var removeRequestQueue: [OSRequestRemoveAlias] = [] - var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() + var pendingAuthRequests: [String: [OSUserRequest]] = [String: [OSUserRequest]]() let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -228,14 +228,14 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } } - + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { - if (jwtConfig.isRequired ?? false) { - self.pendRequestUntilAuthUpdated(request, externalId: externalId) + if jwtConfig.isRequired ?? false { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } - + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { self.addRequestQueue.removeAll(where: { $0 == request}) @@ -258,7 +258,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { return } guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { - pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) return } guard request.prepareForExecution(newRecordsState: newRecordsState) else { @@ -325,7 +325,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { return } guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { - pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) return } guard request.prepareForExecution(newRecordsState: newRecordsState) else { @@ -360,8 +360,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false - } - else if responseType != .retryable { + } else if responseType != .retryable { // Fail, no retry, remove from cache and queue // A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted self.removeRequestQueue.removeAll(where: { $0 == request}) @@ -389,7 +388,7 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } - + private func reQueuePendingRequestsForExternalId(externalId: String) { self.dispatchQueue.async { guard let requests = self.pendingAuthRequests[externalId] else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index f5b7d19b4..3d7a4da14 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -63,7 +63,7 @@ private struct OSCombinedProperties { class OSPropertyOperationExecutor: OSOperationExecutor { var supportedDeltas: [String] = [OS_UPDATE_PROPERTIES_DELTA] var deltaQueue: [OSDelta] = [] - var pendingAuthRequests: [String: [OSRequestUpdateProperties]] = [String:[OSRequestUpdateProperties]]() + var pendingAuthRequests: [String: [OSRequestUpdateProperties]] = [String: [OSRequestUpdateProperties]]() var updateRequestQueue: [OSRequestUpdateProperties] = [] let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -268,14 +268,14 @@ class OSPropertyOperationExecutor: OSOperationExecutor { executeUpdatePropertiesRequest(request, inBackground: inBackground) } } - + func handleUnauthorizedError(externalId: String, error: NSError, request: OSRequestUpdateProperties) { - if (jwtConfig.isRequired ?? false) { - self.pendRequestUntilAuthUpdated(request, externalId: externalId) + if jwtConfig.isRequired ?? false { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } - + func pendRequestUntilAuthUpdated(_ request: OSRequestUpdateProperties, externalId: String?) { self.dispatchQueue.async { self.updateRequestQueue.removeAll(where: { $0 == request}) @@ -296,16 +296,16 @@ class OSPropertyOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { - pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) return } - + guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } - + print("ECM executing properties request: %@", request.identityModel.externalId) request.sentToClient = true @@ -375,7 +375,7 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } - + private func reQueuePendingRequestsForExternalId(externalId: String) { self.dispatchQueue.async { guard let requests = self.pendingAuthRequests[externalId] else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 4fc5c5308..d1c43e903 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -35,7 +35,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { var addRequestQueue: [OSRequestCreateSubscription] = [] var removeRequestQueue: [OSRequestDeleteSubscription] = [] var updateRequestQueue: [OSRequestUpdateSubscription] = [] - var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() + var pendingAuthRequests: [String: [OSUserRequest]] = [String: [OSUserRequest]]() var subscriptionModels: [String: OSSubscriptionModel] = [:] let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -279,15 +279,14 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } } - + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { - if (jwtConfig.isRequired ?? false) { - self.pendRequestUntilAuthUpdated(request, externalId: externalId) + if jwtConfig.isRequired ?? false { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } - - + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { self.addRequestQueue.removeAll(where: { $0 == request}) @@ -311,7 +310,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { return } guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { - pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) return } guard request.prepareForExecution(newRecordsState: newRecordsState) else { @@ -494,12 +493,12 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { print("❌ OSSubscriptionOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // ECM TODO If auth changed from false or unknown to true, process requests } - + func onJwtUpdated(externalId: String, token: String?) { print("❌ OSSubscriptionOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } - + private func reQueuePendingRequestsForExternalId(externalId: String) { self.dispatchQueue.async { guard let requests = self.pendingAuthRequests[externalId] else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index ef6dfec1a..e487dfdca 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -35,7 +35,7 @@ import OneSignalOSCore */ class OSUserExecutor { var userRequestQueue: [OSUserRequest] = [] - var pendingAuthRequests: [String: [OSUserRequest]] = [String:[OSUserRequest]]() + var pendingAuthRequests: [String: [OSUserRequest]] = [String: [OSUserRequest]]() private let newRecordsState: OSNewRecordsState let jwtConfig: OSUserJwtConfig @@ -290,7 +290,7 @@ extension OSUserExecutor { appendToQueue(request) executePendingRequests() } - + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { self.userRequestQueue.removeAll(where: { $0 == request}) @@ -318,9 +318,9 @@ extension OSUserExecutor { request.pushSubscriptionModel = pushSubscriptionModel request.updatePushSubscriptionModel(pushSubscriptionModel) } - + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { - pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) return } @@ -372,7 +372,7 @@ extension OSUserExecutor { // A failed create user request would leave the SDK in a bad state // Don't remove the request from cache and pause the operation repo // We will retry this request on a new session - + // We can't do this anymore for 401s OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true request.sentToClient = false @@ -382,10 +382,10 @@ extension OSUserExecutor { } } } - + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { - if (jwtConfig.isRequired ?? false) { - self.pendRequestUntilAuthUpdated(request, externalId: externalId) + if jwtConfig.isRequired ?? false { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) } } @@ -509,7 +509,7 @@ extension OSUserExecutor { // This will hydrate the OneSignal ID for any pending requests self.createUser(aliasLabel: request.aliasLabel, aliasId: request.aliasId, identityModel: request.identityModelToUpdate) } - } else if responseType == .invalid || responseType == .unauthorized { //Identify User should never be called with identity verification on + } else if responseType == .invalid || responseType == .unauthorized { // Identify User should never be called with identity verification on // Failed, no retry self.removeFromQueue(request) self.executePendingRequests() @@ -734,7 +734,7 @@ extension OSUserExecutor: OSUserJwtConfigListener { reQueuePendingRequestsForExternalId(externalId: externalId) print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } - + private func reQueuePendingRequestsForExternalId(externalId: String) { self.dispatchQueue.async { guard let requests = self.pendingAuthRequests[externalId] else { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index 2c8fd293b..230961f57 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -108,11 +108,9 @@ extension OSIdentityModelRepo: OSModelChangedHandler { extension OSIdentityModelRepo: OSLoggable { func logSelf() { - print( - """ - 💛 OSIdentityModelRepo has the following models: - models: \(self.models) - """ - ) + print("OSIdentityModelRepo has the following models: ") + for model in models.values { + print(" modelID: \(model.modelId), alises: \(model.aliases) token: \(model.jwtBearerToken ?? "nil")") + } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift index bbc4528a4..f6fd98245 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift @@ -25,7 +25,6 @@ THE SOFTWARE. */ - import Foundation @objc public class OSJwtInvalidatedEvent: NSObject { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 71902ce67..a624b0b55 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -671,7 +671,7 @@ extension OneSignalUserManagerImpl { public func subscribeToJwtConfig(_ listener: OSUserJwtConfigListener, key: String) { jwtConfig.subscribe(listener, key: key) } - + @objc public func invalidateJwtForExternalId(externalId: String, error: NSError) { guard jwtConfig.isRequired == true else { @@ -685,20 +685,19 @@ extension OneSignalUserManagerImpl { fireJwtExpired(externalId: externalId) } - - + private func fireJwtExpired(externalId: String) { guard let jwtInvalidatedHandler = self.jwtInvalidatedHandler else { return } let invalidatedEvent = OSJwtInvalidatedEvent(externalId: externalId) - + jwtInvalidatedHandler(invalidatedEvent) } - + private func getMessageFromJwtError(_ error: NSError) -> String { - if let returnedObject = error.userInfo["returned"] as? Dictionary { - if let errors = returnedObject["errors"] as? Array> { + if let returnedObject = error.userInfo["returned"] as? [String: AnyObject] { + if let errors = returnedObject["errors"] as? [[String: AnyObject]] { return errors[0]["title"] as? String ?? error.localizedDescription } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift index 1f4cc6249..3ed267d3e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserDefines.swift @@ -15,5 +15,4 @@ public let userB_ValidJwtToken = "fyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiO public let userA_email = "userA@onesignal.com" public let userA_AliasLabel = "testAliasLabel" -public let userA_Aliases = [userA_AliasLabel : "userAValue"] - +public let userA_Aliases = [userA_AliasLabel: "userAValue"] diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift index 2084249ab..b9776bee1 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserRequests.swift @@ -59,11 +59,11 @@ public class MockUserRequests: NSObject { "properties": properties ] } - + public static func testUnauthorizedailureError() -> NSError { let userInfo = ["returned": [ - "errors": [["title":"token has invalid claims: token is expired", "code":"auth-0"]], - "httpStatusCode": 401, + "errors": [["title": "token has invalid claims: token is expired", "code": "auth-0"]], + "httpStatusCode": 401 ]] return NSError(domain: "not-important", code: 401, userInfo: userInfo) } @@ -105,53 +105,53 @@ extension MockUserRequests { response: userResponse ) } - + public static func setUnauthorizedCreateUserFailureResponses(with client: MockOneSignalClient, externalId: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"", error: error) + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedFetchUserFailureResponses(with client: MockOneSignalClient, onesignalId: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"", error: error) + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedUpdatePropertiesFailureResponses(with client: MockOneSignalClient, tags: [String: String]) { let error = testUnauthorizedailureError() - + let params: NSDictionary = [ "properties": [ "tags": tags ], "refresh_device_metadata": false ] - - client.setMockFailureResponseForRequest(request:"", error: error) + + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedAddEmailFailureResponse(with client: MockOneSignalClient, email: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"", error: error) + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedRemoveEmailFailureResponse(with client: MockOneSignalClient, email: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"", error: error) + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedUpdateSubscriptionFailureResponse(with client: MockOneSignalClient, token: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error) + client.setMockFailureResponseForRequest(request: "OSRequestUpdateSubscription with subscriptionObject: [\"token\": \"\(token)\"]", error: error) } - + public static func setUnauthorizedAddAliasFailureResponse(with client: MockOneSignalClient, aliases: [String: String]) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"", error: error) + client.setMockFailureResponseForRequest(request: "", error: error) } - + public static func setUnauthorizedRemoveAliasFailureResponse(with client: MockOneSignalClient, aliasLabel: String) { let error = testUnauthorizedailureError() - client.setMockFailureResponseForRequest(request:"OSRequestRemoveAlias with aliasLabel: \(aliasLabel)", error: error) + client.setMockFailureResponseForRequest(request: "OSRequestRemoveAlias with aliasLabel: \(aliasLabel)", error: error) } public static func setDefaultIdentifyUserResponses(with client: MockOneSignalClient, externalId: String, conflicted: Bool = false) { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index 545df66c5..3c986de11 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -54,185 +54,185 @@ final class IdentityExecutorTests: XCTestCase { } override func tearDownWithError() throws { } - + func testAddAliasSendsWhenProcessed() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(false) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) let aliases = userA_Aliases MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) - mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) } - + func testAddAlias_IdentityVerificationRequired_butNoToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) let aliases = userA_Aliases MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) - mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) } - + func testAddAlias_IdentityVerificationRequired_withToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) - mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) } - + func testAddAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases) - mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) - + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testRemoveAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let aliases = userA_Aliases MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) - mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) - + mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testAddAliasRequests_Retry_OnTokenUpdate() { - + /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken - + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! - + let aliases = userA_Aliases MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases) - executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value:aliases)) + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } - + /* When */ executor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) XCTAssertTrue(invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } - + func testAddAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) userA.identityModel.jwtBearerToken = userA_InvalidJwtToken - + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) userB.identityModel.jwtBearerToken = userA_InvalidJwtToken // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! - + let aliases = userA_Aliases MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases) - - executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases)) - executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases)) - + + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value: aliases)) + executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value: aliases)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ executor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) @@ -243,32 +243,32 @@ final class IdentityExecutorTests: XCTestCase { // It is 4 because setting user B's OneSignal ID counts as an add alias request XCTAssertEqual(addAliasRequests.count, 4) } - + func testRemoveAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) userA.identityModel.jwtBearerToken = userA_InvalidJwtToken - + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) userB.identityModel.jwtBearerToken = userA_InvalidJwtToken // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.identityExecutor! - + let aliases = userA_Aliases MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) - executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value:aliases)) - executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value:aliases)) - + executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value: aliases)) + executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value: aliases)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ executor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) @@ -276,7 +276,7 @@ final class IdentityExecutorTests: XCTestCase { OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index b266359f4..0961c876c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -54,162 +54,160 @@ final class PropertyExecutorTests: XCTestCase { } override func tearDownWithError() throws { } - + func testUpdateTagsSendsWhenProcessed() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(false) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - let tags = ["testUserA" : "true"] + let tags = ["testUserA": "true"] MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) - mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) /* When */ mocks.propertyExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) } - + func testUpdateTags_IdentityVerificationRequired_butNoToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - let tags = ["testUserA" : "true"] + let tags = ["testUserA": "true"] MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) - mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) /* When */ mocks.propertyExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) } - + func testUpdateTags_IdentityVerificationRequired_withToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken - let tags = ["testUserA" : "true"] + let tags = ["testUserA": "true"] MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) - mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) /* When */ mocks.propertyExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) } - + func testUpdateProperty_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken - - - - let tags = ["testUserA" : "true"] + + let tags = ["testUserA": "true"] MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) - mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ mocks.propertyExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testUpdateRequests_Retry_OnTokenUpdate() { - + /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken - + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor! - - let tags = ["testUserA" : "true"] + + let tags = ["testUserA": "true"] MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) - executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } - + /* When */ executor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) XCTAssertTrue(invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } - + func testUpdateRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) userA.identityModel.jwtBearerToken = userA_InvalidJwtToken - + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) userB.identityModel.jwtBearerToken = userA_InvalidJwtToken // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.propertyExecutor! - - let tags = ["testUserA" : "true"] + + let tags = ["testUserA": "true"] MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) - - executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userA.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) - executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userB.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value:tags)) - + + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userA.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) + executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userB.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ executor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index 32c753314..240b7c752 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -54,138 +54,138 @@ final class SubscriptionExecutorTests: XCTestCase { } override func tearDownWithError() throws { } - + func testAddEmailSendsWhenProcessed() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(false) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) let email = userA_email MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) } - + func testAddEmail_IdentityVerificationRequired_butNoToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) let email = userA_email MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) } - + func testAddEmail_IdentityVerificationRequired_withToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) } - + func testAddEmail_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) - + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testDeleteEmail_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let email = userA_email MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value:email)) - + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testUpdateSubscription_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) user.identityModel.jwtBearerToken = userA_InvalidJwtToken let token = testPushToken MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value:token)) - + mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value: token)) + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self)) XCTAssertTrue(invalidatedCallbackWasCalled) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index 6407cdcd1..f23892c90 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -42,7 +42,6 @@ private class Mocks: OneSignalExecutorMocks { } } - final class UserExecutorTests: XCTestCase { override func setUpWithError() throws { @@ -184,197 +183,197 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) XCTAssertTrue(mocks.newRecordsState.records.isEmpty) } - + func testCreateUser_IdentityVerificationRequired_butNoToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: "") + + _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) - + /* When */ mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should not execute this request since identity verification is required, but no token was set XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) XCTAssertEqual(mocks.newRecordsState.records.count, 0) } - + func testCreateUser_IdentityVerificationRequired_withToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: "") + + _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) - + /* When */ mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) } - + func testCreateUser_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + + _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) - + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testFetchUser_IdentityVerificationRequired_butNoToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: "") + + _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID], changeNotifier: OSEventProducer()) MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) - + /* When */ mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should not execute this request since identity verification is required, but no token was set XCTAssertFalse(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) } - + func testFetchUser_IdentityVerificationRequired_withToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: "") + + _ = mocks.setUserManagerInternalUser(externalId: "") let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) - + /* When */ mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should not execute this request since identity verification is required, but no token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) } - + func testFetchUser_IdentityVerificationRequired_withInvalidToken() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + + _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) let newIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) - + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } - + func testUserRequests_Retry_OnTokenUpdate() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + + _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! - + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken - + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) - + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } - + /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } - + func testUserRequests_RetryAllRequests_OnTokenUpdate() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + + _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! - + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken - + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) - + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: userAIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userA_EUID) - + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) @@ -382,42 +381,42 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 4) } - + func testUserRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { /* Setup */ let mocks = Mocks() - + mocks.setAuthRequired(true) - - let _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + + _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! - + let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken - + let userBIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userB_OSID, OS_EXTERNAL_ID: userB_EUID], changeNotifier: OSEventProducer()) userBIdentityModel.jwtBearerToken = userA_InvalidJwtToken - + MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userB_EUID) - + var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { event in + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in invalidatedCallbackWasCalled = true } - + /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userB_EUID, identityModel: userBIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) MockUserRequests.setDefaultCreateUserResponses(with: mocks.client, externalId: userB_EUID) - + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - + /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) From 01c2069bb925b3304fe1227cf4f224d8e305934d Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 24 Sep 2024 13:03:48 -0700 Subject: [PATCH 38/56] cache the pending auth requests in executors * Uncaching now involves more queues, can be refactored when op repo is refactored * Some executors added a helper to remove requests from the active queue and cache the queue after removal. --- .../Source/OneSignalCommonDefines.h | 4 + .../OSIdentityOperationExecutor.swift | 92 ++++++---- .../OSPropertyOperationExecutor.swift | 21 ++- .../OSSubscriptionOperationExecutor.swift | 173 +++++++++++------- .../Source/Executors/OSUserExecutor.swift | 167 +++++++++-------- 5 files changed, 272 insertions(+), 185 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 30658efd4..2bb00e656 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -347,23 +347,27 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_USER_EXECUTOR @"OS_USER_EXECUTOR" #define OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY @"OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY" #define OS_USER_EXECUTOR_TRANSFER_SUBSCRIPTION_REQUEST_QUEUE_KEY @"OS_USER_EXECUTOR_TRANSFER_SUBSCRIPTION_REQUEST_QUEUE_KEY" +#define OS_USER_EXECUTOR_PENDING_QUEUE_KEY @"OS_USER_EXECUTOR_PENDING_QUEUE_KEY" // Identity Executor #define OS_IDENTITY_EXECUTOR @"OS_IDENTITY_EXECUTOR" #define OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY" #define OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY" #define OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY" +#define OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY @"OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY" // Property Executor #define OS_PROPERTIES_EXECUTOR @"OS_PROPERTIES_EXECUTOR" #define OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY" #define OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY" +#define OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY" // Subscription Executor #define OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY" +#define OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY" // Live Activies Executor #define OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY @"OS_LIVE_ACTIVITIES_EXECUTOR_UPDATE_TOKENS_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 9d1442c8f..090490135 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -48,8 +48,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { print("❌ OSIdentityOperationExecutor init(\(jwtConfig.isRequired))") // Read unfinished deltas and requests from cache, if any... uncacheDeltas() - uncacheAddAliasRequests() - uncacheRemoveAliasRequests() + uncacheRequests() } private func uncacheDeltas() { @@ -82,19 +81,42 @@ class OSIdentityOperationExecutor: OSOperationExecutor { OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) } - private func uncacheAddAliasRequests() { - guard var addRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor error encountered reading from cache for \(OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY)") - return + private func uncacheRequests() { + var addRequestQueue: [OSRequestAddAliases] = [] + var removeRequestQueue: [OSRequestRemoveAlias] = [] + + if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestAddAliases] { + addRequestQueue = cachedAddRequestQueue + } + + if let cachedRemoveRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] { + removeRequestQueue = cachedRemoveRequestQueue + } + + if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSUserRequest]] { + for requests in pendingRequests.values { + for request in requests { + if request.isKind(of: OSRequestAddAliases.self), let req = request as? OSRequestAddAliases { + addRequestQueue.append(req) + } else if request.isKind(of: OSRequestRemoveAlias.self), let req = request as? OSRequestRemoveAlias { + removeRequestQueue.append(req) + } + } + } } + linkAddAliasRequests(requests: &addRequestQueue) + linkRemoveAliasRequests(requests: &removeRequestQueue) + } + + private func linkAddAliasRequests(requests: inout [OSRequestAddAliases]) { // Hook each uncached Request to the model in the store - for (index, request) in addRequestQueue.enumerated().reversed() { + for (index, request) in requests.enumerated().reversed() { if jwtConfig.isRequired == true, request.identityModel.externalId == nil { // remove if jwt is on but the model does not have external ID - addRequestQueue.remove(at: index) + requests.remove(at: index) continue } @@ -107,27 +129,22 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } else { // 3. The model do not exist AND this request cannot be sent, drop this Request OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") - addRequestQueue.remove(at: index) + requests.remove(at: index) } } - self.addRequestQueue = addRequestQueue + self.addRequestQueue = requests OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) } - private func uncacheRemoveAliasRequests() { - guard var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestRemoveAlias] else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor error encountered reading from cache for \(OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY)") - return - } - + private func linkRemoveAliasRequests(requests: inout [OSRequestRemoveAlias]) { // Hook each uncached Request to the model in the store - for (index, request) in removeRequestQueue.enumerated().reversed() { + for (index, request) in requests.enumerated().reversed() { if jwtConfig.isRequired == true, request.identityModel.externalId == nil { // remove if jwt is on but the model does not have external ID - removeRequestQueue.remove(at: index) + requests.remove(at: index) continue } @@ -140,11 +157,11 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } else { // 3. The model do not exist AND this request cannot be sent, drop this Request OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor.init dropped \(request)") - removeRequestQueue.remove(at: index) + requests.remove(at: index) } } - self.removeRequestQueue = removeRequestQueue + self.removeRequestQueue = requests OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) } @@ -161,6 +178,19 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } } + /** + This method does not handle concurrency; it should be called with thread-safe usage. + */ + private func removeFromRequestQueueAndPersist(_ request: OSUserRequest) { + if request.isKind(of: OSRequestAddAliases.self) { + self.addRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + } else if request.isKind(of: OSRequestRemoveAlias.self) { + self.removeRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + } + } + func processDeltaQueue(inBackground: Bool) { self.dispatchQueue.async { if !self.deltaQueue.isEmpty { @@ -238,8 +268,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { - self.addRequestQueue.removeAll(where: { $0 == request}) - self.removeRequestQueue.removeAll(where: { $0 == request}) + self.removeFromRequestQueueAndPersist(request) guard let externalId = externalId else { return } @@ -250,6 +279,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } requests.append(request) self.pendingAuthRequests[externalId] = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) } } @@ -277,8 +307,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // No hydration from response // On success, remove request from cache self.dispatchQueue.async { - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + self.removeFromRequestQueueAndPersist(request) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } @@ -288,9 +317,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .missing { - // Remove from cache and queue - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + self.removeFromRequestQueueAndPersist(request) // Logout if the user in the SDK is the same guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel) else { @@ -309,8 +336,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + self.removeFromRequestQueueAndPersist(request) } } if inBackground { @@ -344,8 +370,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // There is nothing to hydrate // On success, remove request from cache self.dispatchQueue.async { - self.removeRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + self.removeFromRequestQueueAndPersist(request) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } @@ -363,8 +388,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { } else if responseType != .retryable { // Fail, no retry, remove from cache and queue // A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted - self.removeRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + self.removeFromRequestQueueAndPersist(request) } } if inBackground { @@ -404,6 +428,7 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) self.pendingAuthRequests[externalId] = nil + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) self.processRequestQueue(inBackground: false) } } @@ -446,6 +471,7 @@ extension OSIdentityOperationExecutor: OSLoggable { addRequestQueue: \(self.addRequestQueue) removeRequestQueue: \(self.removeRequestQueue) deltaQueue: \(self.deltaQueue) + pendingAuthRequests: \(self.pendingAuthRequests) """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index 3d7a4da14..ff50d921a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -110,13 +110,22 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } private func uncacheUpdateRequests() { - print("❌ OSPropertyOperationExecutor uncacheUpdateRequests called") + var updateRequestQueue: [OSRequestUpdateProperties] = [] - guard var updateRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor error encountered reading from cache for \(OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY)") - return + if let cachedQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestUpdateProperties] { + updateRequestQueue = cachedQueue } + if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSRequestUpdateProperties]] { + print("❌ prop executor uncached pending \(pendingRequests)") + + for requests in pendingRequests.values { + for request in requests { + updateRequestQueue.append(request) + } + } + } + print("❌ prop executor uncached requests \(updateRequestQueue)") // Hook each uncached Request to the model in the store for (index, request) in updateRequestQueue.enumerated().reversed() { if jwtConfig.isRequired == true, @@ -279,6 +288,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { func pendRequestUntilAuthUpdated(_ request: OSRequestUpdateProperties, externalId: String?) { self.dispatchQueue.async { self.updateRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) guard let externalId = externalId else { return } @@ -289,6 +299,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } requests.append(request) self.pendingAuthRequests[externalId] = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) } } @@ -386,6 +397,7 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { } self.pendingAuthRequests[externalId] = nil OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) self.processRequestQueue(inBackground: false) } } @@ -422,6 +434,7 @@ extension OSPropertyOperationExecutor: OSLoggable { 💛 OSPropertyOperationExecutor has the following queues: updateRequestQueue: \(self.updateRequestQueue) deltaQueue: \(self.deltaQueue) + pendingAuthRequests: \(self.pendingAuthRequests) """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index d1c43e903..3f66169b8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -49,9 +49,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { self.jwtConfig = jwtConfig // Read unfinished deltas and requests from cache, if any... uncacheDeltas() - uncacheCreateSubscriptionRequests() - uncacheDeleteSubscriptionRequests() - uncacheUpdateSubscriptionRequests() + uncacheRequests() } private func uncacheDeltas() { @@ -74,65 +72,89 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } - private func uncacheCreateSubscriptionRequests() { - var requestQueue: [OSRequestCreateSubscription] = [] + private func uncacheRequests() { + // Uncache the Create and Delete requests from the queues and pending queue + + var addRequestQueue: [OSRequestCreateSubscription] = [] + var removeRequestQueue: [OSRequestDeleteSubscription] = [] if let cachedAddRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestCreateSubscription] { - // Hook each uncached Request to the model in the store - for request in cachedAddRequestQueue { - // 1. Hook up the subscription model - if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { - // a. The model exist in the store, set it to be the Request's models - request.subscriptionModel = subscriptionModel - } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { - // b. The model exists in the dictionary of seen models - request.subscriptionModel = subscriptionModel - } else { - // c. The model has not been seen yet, add to dict - subscriptionModels[request.subscriptionModel.modelId] = request.subscriptionModel - } - // 2. Hook up the identity model - if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { - // a. The model exist in the repo - request.identityModel = identityModel - } else if request.prepareForExecution(newRecordsState: newRecordsState) { - // b. The request can be sent, add the model to the repo - OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) - } else { - // c. The model do not exist AND this request cannot be sent, drop this Request - OneSignalLog.onesignalLog(.LL_WARN, message: "OSSubscriptionOperationExecutor.init dropped: \(request)") - continue + addRequestQueue = cachedAddRequestQueue + } + if let cachedRemoveRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestDeleteSubscription] { + removeRequestQueue = cachedRemoveRequestQueue + } + + if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSUserRequest]] { + for requests in pendingRequests.values { + for request in requests { + if request.isKind(of: OSRequestCreateSubscription.self), let req = request as? OSRequestCreateSubscription { + addRequestQueue.append(req) + } else if request.isKind(of: OSRequestDeleteSubscription.self), let req = request as? OSRequestDeleteSubscription { + removeRequestQueue.append(req) + } } - requestQueue.append(request) } - self.addRequestQueue = requestQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) - } else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor error encountered reading from cache for \(OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY)") } + + linkCreateSubscriptionRequests(requests: addRequestQueue) + linkDeleteSubscriptionRequests(requests: &removeRequestQueue) + // Update Requests are not added to the pending queues + uncacheUpdateSubscriptionRequests() } - private func uncacheDeleteSubscriptionRequests() { - if var removeRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSRequestDeleteSubscription] { - // Hook each uncached Request to the model in the store - for (index, request) in removeRequestQueue.enumerated().reversed() { - if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { - // 1. The model exists in the store, set it to be the Request's model - request.subscriptionModel = subscriptionModel - } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { - // 2. The model exists in the dict of seen subscription models - request.subscriptionModel = subscriptionModel - } else if !request.prepareForExecution(newRecordsState: newRecordsState) { - // 3. The model does not exist AND this request cannot be sent, drop this Request - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)") - removeRequestQueue.remove(at: index) - } + private func linkCreateSubscriptionRequests(requests: [OSRequestCreateSubscription]) { + var requestQueue: [OSRequestCreateSubscription] = [] + + // Hook each uncached Request to the model in the store + for request in requests { + // 1. Hook up the subscription model + if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { + // a. The model exist in the store, set it to be the Request's models + request.subscriptionModel = subscriptionModel + } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { + // b. The model exists in the dictionary of seen models + request.subscriptionModel = subscriptionModel + } else { + // c. The model has not been seen yet, add to dict + subscriptionModels[request.subscriptionModel.modelId] = request.subscriptionModel } - self.removeRequestQueue = removeRequestQueue - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) - } else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor error encountered reading from cache for \(OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY)") + // 2. Hook up the identity model + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // a. The model exist in the repo + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // b. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // c. The model do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_WARN, message: "OSSubscriptionOperationExecutor.init dropped: \(request)") + continue + } + requestQueue.append(request) } + self.addRequestQueue = requestQueue + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + + } + + private func linkDeleteSubscriptionRequests(requests: inout [OSRequestDeleteSubscription]) { + // Hook each uncached Request to the model in the store + for (index, request) in requests.enumerated().reversed() { + if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { + // 1. The model exists in the store, set it to be the Request's model + request.subscriptionModel = subscriptionModel + } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { + // 2. The model exists in the dict of seen subscription models + request.subscriptionModel = subscriptionModel + } else if !request.prepareForExecution(newRecordsState: newRecordsState) { + // 3. The model does not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)") + requests.remove(at: index) + } + } + self.removeRequestQueue = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) } private func uncacheUpdateSubscriptionRequests() { @@ -184,6 +206,22 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } + /** + This method does not handle concurrency; it should be called with thread-safe usage. + */ + private func removeFromRequestQueueAndPersist(_ request: OSUserRequest) { + if request.isKind(of: OSRequestCreateSubscription.self) { + self.addRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + } else if request.isKind(of: OSRequestDeleteSubscription.self) { + self.removeRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + } else if request.isKind(of: OSRequestUpdateSubscription.self) { + self.updateRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + } + } + func processDeltaQueue(inBackground: Bool) { self.dispatchQueue.async { if !self.deltaQueue.isEmpty { @@ -289,9 +327,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { - self.addRequestQueue.removeAll(where: { $0 == request}) - self.removeRequestQueue.removeAll(where: { $0 == request}) - self.updateRequestQueue.removeAll(where: { $0 == request}) + self.removeFromRequestQueueAndPersist(request) guard let externalId = externalId else { return } @@ -302,6 +338,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } requests.append(request) self.pendingAuthRequests[externalId] = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) } } @@ -327,9 +364,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { OneSignalCoreImpl.sharedClient().execute(request) { result in // On success, remove request from cache (even if not hydrating model), and hydrate model self.dispatchQueue.async { - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) - + self.removeFromRequestQueueAndPersist(request) guard let response = result?["subscription"] as? [String: Any] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "Unabled to parse response to create subscription request") if inBackground { @@ -348,8 +383,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .missing { - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + self.removeFromRequestQueueAndPersist(request) // Logout if the user in the SDK is the same guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel) else { @@ -368,8 +402,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue - self.addRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) + self.removeFromRequestQueueAndPersist(request) } } if inBackground { @@ -404,8 +437,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { // On success, remove request from cache. No model hydration occurs. // For example, if app restarts and we read in operations between sending this off and getting the response self.dispatchQueue.async { - self.removeRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + self.removeFromRequestQueueAndPersist(request) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } @@ -424,8 +456,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } else if responseType != .retryable { // Fail, no retry, remove from cache and queue // If this request returns a missing status, that is ok as this is a delete request - self.removeRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) + self.removeFromRequestQueueAndPersist(request) } } if inBackground { @@ -458,8 +489,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { // On success, remove request from cache. No model hydration occurs. // For example, if app restarts and we read in operations between sending this off and getting the response self.dispatchQueue.async { - self.updateRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + self.removeFromRequestQueueAndPersist(request) if inBackground { OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier) } @@ -476,8 +506,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { request.sentToClient = false } else if responseType != .retryable { // Fail, no retry, remove from cache and queue - self.updateRequestQueue.removeAll(where: { $0 == request}) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + self.removeFromRequestQueueAndPersist(request) } } if inBackground { @@ -517,6 +546,7 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) self.pendingAuthRequests[externalId] = nil + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) self.processRequestQueue(inBackground: false) } } @@ -531,6 +561,7 @@ extension OSSubscriptionOperationExecutor: OSLoggable { removeRequestQueue: \(self.removeRequestQueue) updateRequestQueue: \(self.updateRequestQueue) deltaQueue: \(self.deltaQueue) + pendingAuthRequests: \(self.pendingAuthRequests) """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index e487dfdca..d83ff15cb 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -59,80 +59,89 @@ class OSUserExecutor { /// Read in requests from the cache, do not read in FetchUser requests as this is not needed. private func uncacheUserRequests() { var userRequestQueue: [OSUserRequest] = [] + var cachedRequestQueue: [OSUserRequest] = [] print(" OSUserExecutor uncacheUserRequests called") // Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any... - if let cachedRequestQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] { - print(" OSUserExecutor uncacheUserRequests cachedQueue is \(cachedRequestQueue)") - - // Hook each uncached Request to the right model reference - for request in cachedRequestQueue { - if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription { - // Remove this request if JWT is enabled - guard jwtConfig.isRequired != true else { - print(" uncacheUserRequests dropping request \(req)") - continue - } - if let identityModel = getIdentityModel(req.identityModel.modelId) { - // 1. The model exist in the repo, set it to be the Request's model - // It is the current user or the model has already been processed - req.identityModel = identityModel - } else { - // 2. The model do not exist, use the model on the request, and add to repo. - addIdentityModel(req.identityModel) - } - userRequestQueue.append(req) + if let cache = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] { + cachedRequestQueue = cache + } - } else if request.isKind(of: OSRequestCreateUser.self), let req = request as? OSRequestCreateUser { + if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSUserRequest]] { + for requests in pendingRequests.values { + for request in requests { + cachedRequestQueue.append(request) + } + } + } - if jwtConfig.isRequired == true, - req.identityModel.externalId == nil - { - // Remove this request if there is no EUID - print(" uncacheUserRequests dropping request \(req)") - continue - } + // Hook each uncached Request to the right model reference + for request in cachedRequestQueue { + if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription { + // Remove this request if JWT is enabled + guard jwtConfig.isRequired != true else { + print(" uncacheUserRequests dropping request \(req)") + continue + } + if let identityModel = getIdentityModel(req.identityModel.modelId) { + // 1. The model exist in the repo, set it to be the Request's model + // It is the current user or the model has already been processed + req.identityModel = identityModel + } else { + // 2. The model do not exist, use the model on the request, and add to repo. + addIdentityModel(req.identityModel) + } + userRequestQueue.append(req) - if let identityModel = getIdentityModel(req.identityModel.modelId) { - // 1. The model exist in the repo, set it to be the Request's model - req.identityModel = identityModel - } else { - // 2. The models do not exist, use the model on the request, and add to repo. - addIdentityModel(req.identityModel) - } - userRequestQueue.append(req) + } else if request.isKind(of: OSRequestCreateUser.self), let req = request as? OSRequestCreateUser { - } else if request.isKind(of: OSRequestIdentifyUser.self), let req = request as? OSRequestIdentifyUser { + if jwtConfig.isRequired == true, + req.identityModel.externalId == nil + { + // Remove this request if there is no EUID + print(" uncacheUserRequests dropping request \(req)") + continue + } - // If JWT is enabled, we migrate this request into a Create User request - guard jwtConfig.isRequired != true else { - print(" uncacheUserRequests converting \(req) to createUser") - convertIdentifyUserToCreateUser(req) - continue - } + if let identityModel = getIdentityModel(req.identityModel.modelId) { + // 1. The model exist in the repo, set it to be the Request's model + req.identityModel = identityModel + } else { + // 2. The models do not exist, use the model on the request, and add to repo. + addIdentityModel(req.identityModel) + } + userRequestQueue.append(req) - if let identityModelToIdentify = getIdentityModel(req.identityModelToIdentify.modelId), - let identityModelToUpdate = getIdentityModel(req.identityModelToUpdate.modelId) { - // 1. Both models exist in the repo, set it to be the Request's models - req.identityModelToIdentify = identityModelToIdentify - req.identityModelToUpdate = identityModelToUpdate - } else if let identityModelToIdentify = getIdentityModel(req.identityModelToIdentify.modelId), - getIdentityModel(req.identityModelToUpdate.modelId) == nil { - // 2. A model is in the repo, the other model does not exist - req.identityModelToIdentify = identityModelToIdentify - addIdentityModel(req.identityModelToUpdate) - } else { - // 3. Both models don't exist yet - // Drop the request if the identityModelToIdentify does not already exist AND the request is missing OSID - // Otherwise, this request will forever fail `prepareForExecution` and block pending requests such as recovery calls to `logout` or `login` - guard request.prepareForExecution(newRecordsState: newRecordsState) else { - OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor.start() dropped: \(request)") - continue - } - addIdentityModel(req.identityModelToIdentify) - addIdentityModel(req.identityModelToUpdate) + } else if request.isKind(of: OSRequestIdentifyUser.self), let req = request as? OSRequestIdentifyUser { + + // If JWT is enabled, we migrate this request into a Create User request + guard jwtConfig.isRequired != true else { + print(" uncacheUserRequests converting \(req) to createUser") + convertIdentifyUserToCreateUser(req) + continue + } + + if let identityModelToIdentify = getIdentityModel(req.identityModelToIdentify.modelId), + let identityModelToUpdate = getIdentityModel(req.identityModelToUpdate.modelId) { + // 1. Both models exist in the repo, set it to be the Request's models + req.identityModelToIdentify = identityModelToIdentify + req.identityModelToUpdate = identityModelToUpdate + } else if let identityModelToIdentify = getIdentityModel(req.identityModelToIdentify.modelId), + getIdentityModel(req.identityModelToUpdate.modelId) == nil { + // 2. A model is in the repo, the other model does not exist + req.identityModelToIdentify = identityModelToIdentify + addIdentityModel(req.identityModelToUpdate) + } else { + // 3. Both models don't exist yet + // Drop the request if the identityModelToIdentify does not already exist AND the request is missing OSID + // Otherwise, this request will forever fail `prepareForExecution` and block pending requests such as recovery calls to `logout` or `login` + guard request.prepareForExecution(newRecordsState: newRecordsState) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor.start() dropped: \(request)") + continue } - userRequestQueue.append(req) + addIdentityModel(req.identityModelToIdentify) + addIdentityModel(req.identityModelToUpdate) } + userRequestQueue.append(req) } } self.userRequestQueue = userRequestQueue @@ -179,7 +188,7 @@ class OSUserExecutor { } } - func removeFromQueue(_ request: OSUserRequest) { + func removeFromRequestQueueAndPersist(_ request: OSUserRequest) { self.dispatchQueue.async { self.userRequestQueue.removeAll(where: { $0 == request}) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) @@ -200,7 +209,7 @@ class OSUserExecutor { self.executeFetchUserRequest(fetchUserRequest) } else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) } } } @@ -264,7 +273,7 @@ class OSUserExecutor { return } else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSUserExecutor met incompatible Request type that cannot be executed.") - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) } } } @@ -294,6 +303,7 @@ extension OSUserExecutor { func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { self.dispatchQueue.async { self.userRequestQueue.removeAll(where: { $0 == request}) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) guard let externalId = externalId else { return } @@ -304,6 +314,7 @@ extension OSUserExecutor { } requests.append(request) self.pendingAuthRequests[externalId] = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) } } @@ -333,7 +344,7 @@ extension OSUserExecutor { request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { response in - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) // Create User's response won't send us the user's complete info if this user already exists if let response = response { @@ -415,7 +426,7 @@ extension OSUserExecutor { request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { response in - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) if let identityObject = self.parseIdentityObjectResponse(response), let onesignalId = identityObject[OS_ONESIGNAL_ID] { @@ -437,7 +448,7 @@ extension OSUserExecutor { if responseType != .retryable { // Fail, no retry, remove the subscription_id but keep the same push subscription model OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) } } self.executePendingRequests() @@ -470,7 +481,7 @@ extension OSUserExecutor { request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { _ in - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) guard let onesignalId = request.identityModelToIdentify.onesignalId else { OneSignalLog.onesignalLog(.LL_ERROR, message: "executeIdentifyUserRequest succeeded but is now missing OneSignal ID!") @@ -500,7 +511,7 @@ extension OSUserExecutor { // Returns 409 if any provided (label, id) pair exists on another User, so the SDK will switch to this user. OneSignalLog.onesignalLog(.LL_DEBUG, message: "executeIdentifyUserRequest returned error code user-2. Now handling user-2 error response... switch to this user.") - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) { // Generate a Create User request, if it's still the current user @@ -511,10 +522,10 @@ extension OSUserExecutor { } } else if responseType == .invalid || responseType == .unauthorized { // Identify User should never be called with identity verification on // Failed, no retry - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) self.executePendingRequests() } else if responseType == .missing { - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) self.executePendingRequests() // Logout if the user in the SDK is the same guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModelToUpdate) @@ -553,7 +564,7 @@ extension OSUserExecutor { request.sentToClient = true OneSignalCoreImpl.sharedClient().execute(request) { response in - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) if let response = response { // Clear local data in preparation for hydration @@ -587,7 +598,7 @@ extension OSUserExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .missing { - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) // Logout if the user in the SDK is the same guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel) else { @@ -603,7 +614,7 @@ extension OSUserExecutor { request.sentToClient = false } else if responseType != .retryable { // If the error is not retryable, remove from cache and queue - self.removeFromQueue(request) + self.removeFromRequestQueueAndPersist(request) } } self.executePendingRequests() @@ -745,6 +756,7 @@ extension OSUserExecutor: OSUserJwtConfigListener { } self.pendingAuthRequests[externalId] = nil OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) self.executePendingRequests(withDelay: true) } } @@ -798,6 +810,7 @@ extension OSUserExecutor: OSLoggable { """ 💛 OSUserExecutor has the following queues: userRequestQueue: \(self.userRequestQueue) + pendingAuthRequests: \(self.pendingAuthRequests) """ ) } From d579f060412a9f8d30542e0c62849eba6f825acb Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 24 Sep 2024 13:56:02 -0700 Subject: [PATCH 39/56] Add pending to fetch user requests --- .../OneSignalUser/Source/Executors/OSUserExecutor.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index d83ff15cb..b082eebc8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -556,6 +556,11 @@ extension OSUserExecutor { return } + guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { + pendRequestUntilAuthUpdated(request, externalId: request.identityModel.externalId) + return + } + guard request.prepareForExecution(newRecordsState: newRecordsState) else { executePendingRequests(withDelay: true) return From 19df3dbacb6ca173e7f839614f048b7ffd7da001 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 24 Sep 2024 13:56:19 -0700 Subject: [PATCH 40/56] Don't fire JWT invalid listeners multiple times * Use a string constant `OS_JWT_TOKEN_INVALID` for a jwt token when we internally invalidated it, instead of setting to `nil`. * OSIdentityModelRepo will not notify user manager when a token has been set to `OS_JWT_TOKEN_INVALID`. The user manager will already be notified of invalidation by executors. --- .../OneSignalUser/Source/OSIdentityModelRepo.swift | 3 ++- .../OneSignalUser/Source/OneSignalUserManagerImpl.swift | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index 230961f57..2b43a64e8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -97,7 +97,8 @@ extension OSIdentityModelRepo: OSModelChangedHandler { args.property == OS_JWT_BEARER_TOKEN, let model = args.model as? OSIdentityModel, let externalId = model.externalId, - let token = args.newValue as? String + let token = args.newValue as? String, + token != OS_JWT_TOKEN_INVALID // Don't notify when token is invalidated internally else { return } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index a624b0b55..13968d7cd 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -681,7 +681,13 @@ extension OneSignalUserManagerImpl { OneSignalLog.onesignalLog(.LL_ERROR, message: "Unable to find identity model for externalId: \(externalId)") return } - identityModel.jwtBearerToken = nil + + // Return, if the token has already been invalidated + guard identityModel.jwtBearerToken != OS_JWT_TOKEN_INVALID else { + return + } + + identityModel.jwtBearerToken = OS_JWT_TOKEN_INVALID fireJwtExpired(externalId: externalId) } From cccb58c39a1eca49e38576e2ee9c75d3397d05fc Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 24 Sep 2024 14:38:29 -0700 Subject: [PATCH 41/56] Subscription executor wrap up, add some more tests * The delete subscription request now has identity model, similar to the Create subscription request * The update subscription request is used only for the push sub, and it does not use User JWT, only a push token header * The "Device-Auth-Push-Token" header has to be base 64 encoded * Move some auth helpers into the JWT extension, and move execute request methods into an extension to address swiflint type_body_length violation --- .../Source/OneSignalCommonDefines.h | 1 + .../OSSubscriptionOperationExecutor.swift | 145 ++++++++++++------ .../OSRequestDeleteSubscription.swift | 25 +-- .../Source/Support/OSUserUtils.swift | 7 +- .../SubscriptionsExecutorTests.swift | 121 +++++++++++++++ .../UserConcurrencyTests.swift | 25 +-- 6 files changed, 257 insertions(+), 67 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 2bb00e656..577386177 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -363,6 +363,7 @@ typedef enum {GET, POST, HEAD, PUT, DELETE, OPTIONS, CONNECT, TRACE, PATCH} HTTP #define OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY @"OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY" // Subscription Executor +#define OS_SUBSCRIPTION_EXECUTOR @"OS_SUBSCRIPTION_EXECUTOR" #define OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY" #define OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY @"OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY" diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 3f66169b8..74ca5528f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -47,7 +47,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { init(newRecordsState: OSNewRecordsState, jwtConfig: OSUserJwtConfig) { self.newRecordsState = newRecordsState self.jwtConfig = jwtConfig - // Read unfinished deltas and requests from cache, if any... + self.jwtConfig.subscribe(self, key: OS_SUBSCRIPTION_EXECUTOR) uncacheDeltas() uncacheRequests() } @@ -141,17 +141,30 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { private func linkDeleteSubscriptionRequests(requests: inout [OSRequestDeleteSubscription]) { // Hook each uncached Request to the model in the store for (index, request) in requests.enumerated().reversed() { + // 1. Hook up the subscription model if let subscriptionModel = getSubscriptionModelFromStores(modelId: request.subscriptionModel.modelId) { - // 1. The model exists in the store, set it to be the Request's model + // a. The model exists in the store, set it to be the Request's model request.subscriptionModel = subscriptionModel } else if let subscriptionModel = subscriptionModels[request.subscriptionModel.modelId] { - // 2. The model exists in the dict of seen subscription models + // b. The model exists in the dict of seen subscription models request.subscriptionModel = subscriptionModel } else if !request.prepareForExecution(newRecordsState: newRecordsState) { - // 3. The model does not exist AND this request cannot be sent, drop this Request + // c. The model does not exist AND this request cannot be sent, drop this Request OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.init dropped \(request)") requests.remove(at: index) } + // 2. Hook up the identity model + if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(request.identityModel.modelId) { + // a. The model exist in the repo + request.identityModel = identityModel + } else if request.prepareForExecution(newRecordsState: newRecordsState) { + // b. The request can be sent, add the model to the repo + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(request.identityModel) + } else { + // c. The model do not exist AND this request cannot be sent, drop this Request + OneSignalLog.onesignalLog(.LL_WARN, message: "OSSubscriptionOperationExecutor.init dropped: \(request)") + requests.remove(at: index) + } } self.removeRequestQueue = requests OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) @@ -254,8 +267,20 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { self.addRequestQueue.append(request) case OS_REMOVE_SUBSCRIPTION_DELTA: + // Only create the request if the identity model exists + guard let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { + OneSignalLog.onesignalLog(.LL_ERROR, message: "OSSubscriptionOperationExecutor.processDeltaQueue dropped \(delta)") + continue + } + + // If JWT is on but the external ID does not exist, drop this Delta + if self.jwtConfig.isRequired == true, identityModel.externalId == nil { + print("❌ \(delta) is Invalid with JWT, being dropped") + } + let request = OSRequestDeleteSubscription( - subscriptionModel: subModel + subscriptionModel: subModel, + identityModel: identityModel ) self.removeRequestQueue.append(request) @@ -317,31 +342,11 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { } } } +} - func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { - if jwtConfig.isRequired ?? false { - self.pendRequestUntilAuthUpdated(request, externalId: externalId) - OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) - } - } - - func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { - self.dispatchQueue.async { - self.removeFromRequestQueueAndPersist(request) - guard let externalId = externalId else { - return - } - var requests = self.pendingAuthRequests[externalId] ?? [] - let inQueue = requests.contains(where: {$0 == request}) - guard !inQueue else { - return - } - requests.append(request) - self.pendingAuthRequests[externalId] = requests - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) - } - } +// MARK: - Execution +extension OSSubscriptionOperationExecutor { func executeCreateSubscriptionRequest(_ request: OSRequestCreateSubscription, inBackground: Bool) { guard !request.sentToClient else { return @@ -416,7 +421,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - // ECM TODO + // ECM TODO - Delete Subscription, not supported on JWT yet (9-23-2024) // guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { // pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) // return @@ -448,8 +453,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { - // ECM The delete subscription request doesn't have an identity model? - if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { + if let externalId = request.identityModel.externalId { self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) } request.sentToClient = false @@ -470,11 +474,6 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { guard !request.sentToClient else { return } - // ECM TODO -// guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { -// pendRequestUntilAuthUpdated(request, externalId:request.identityModel.externalId) -// return -// } guard request.prepareForExecution(newRecordsState: newRecordsState) else { return } @@ -499,11 +498,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { if let nsError = error as? NSError { let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code) if responseType == .unauthorized && (self.jwtConfig.isRequired ?? false) { - // ECM The update subscription request doesn't have an identity model? - if let externalId = OneSignalUserManagerImpl.sharedInstance.user.identityModel.externalId { - self.handleUnauthorizedError(externalId: externalId, error: nsError, request: request) - } - request.sentToClient = false + // TODO: Jwt, do we need to handle this case, as this request does not use user JWT } else if responseType != .retryable { // Fail, no retry, remove from cache and queue self.removeFromRequestQueueAndPersist(request) @@ -520,7 +515,9 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OneSignalOSCore.OSRequiresUserAuth, to: OneSignalOSCore.OSRequiresUserAuth) { print("❌ OSSubscriptionOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") - // ECM TODO If auth changed from false or unknown to true, process requests + if to == .on { + removeInvalidDeltasAndRequests() + } } func onJwtUpdated(externalId: String, token: String?) { @@ -528,6 +525,30 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { reQueuePendingRequestsForExternalId(externalId: externalId) } + func handleUnauthorizedError(externalId: String, error: NSError, request: OSUserRequest) { + if jwtConfig.isRequired ?? false { + self.pendRequestUntilAuthUpdated(request, externalId: externalId) + OneSignalUserManagerImpl.sharedInstance.invalidateJwtForExternalId(externalId: externalId, error: error) + } + } + + func pendRequestUntilAuthUpdated(_ request: OSUserRequest, externalId: String?) { + self.dispatchQueue.async { + self.removeFromRequestQueueAndPersist(request) + guard let externalId = externalId else { + return + } + var requests = self.pendingAuthRequests[externalId] ?? [] + let inQueue = requests.contains(where: {$0 == request}) + guard !inQueue else { + return + } + requests.append(request) + self.pendingAuthRequests[externalId] = requests + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) + } + } + private func reQueuePendingRequestsForExternalId(externalId: String) { self.dispatchQueue.async { guard let requests = self.pendingAuthRequests[externalId] else { @@ -538,18 +559,52 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { self.addRequestQueue.append(addRequest) } else if let removeRequest = request as? OSRequestDeleteSubscription { self.removeRequestQueue.append(removeRequest) - } else if let updateRequest = request as? OSRequestUpdateSubscription { - self.updateRequestQueue.append(updateRequest) } } OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue) OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue) - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_UPDATE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) self.pendingAuthRequests[externalId] = nil OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_PENDING_QUEUE_KEY, withValue: self.pendingAuthRequests) self.processRequestQueue(inBackground: false) } } + + /** + Drops deltas and requests that add and remove subscriptions on unidentified users. + Subscription updates are used only for push subscriptions, which are kept as they do not use User JWT. + */ + private func removeInvalidDeltasAndRequests() { + self.dispatchQueue.async { + print("❌ OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests called") + + for (index, delta) in self.deltaQueue.enumerated().reversed() { + if delta.name != OS_UPDATE_SUBSCRIPTION_DELTA, + let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId), + identityModel.externalId == nil + { + print(" \(delta) is Invalid, being removed") + self.deltaQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) + + for (index, request) in self.addRequestQueue.enumerated().reversed() { + if request.identityModel.externalId == nil { + print(" \(request) is Invalid, being removed") + self.addRequestQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + + for (index, request) in self.removeRequestQueue.enumerated().reversed() { + if request.identityModel.externalId == nil { + print(" \(request) is Invalid, being removed") + self.removeRequestQueue.remove(at: index) + } + } + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_SUBSCRIPTION_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.updateRequestQueue) + } + } } extension OSSubscriptionOperationExecutor: OSLoggable { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift index 35c36c2ad..91adb6eee 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestDeleteSubscription.swift @@ -41,22 +41,26 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { } var subscriptionModel: OSSubscriptionModel + var identityModel: OSIdentityModel - // Need the subscription_id func prepareForExecution(newRecordsState: OSNewRecordsState) -> Bool { - if let subscriptionId = subscriptionModel.subscriptionId, - newRecordsState.canAccess(subscriptionId), - let appId = OneSignalConfigManager.getAppId() - { - self.path = "apps/\(appId)/subscriptions/\(subscriptionId)" - return true - } else { + guard + let subscriptionId = subscriptionModel.subscriptionId, + let token = subscriptionModel.address, + newRecordsState.canAccess(subscriptionId), + let appId = OneSignalConfigManager.getAppId(), + let _ = checkUserRequirementsAndReturnAlias(identityModel, newRecordsState) + else { return false } + + self.path = "apps/\(appId)/subscriptions/by/type/\(subscriptionModel.type)/token/\(token)" + return true } - init(subscriptionModel: OSSubscriptionModel) { + init(subscriptionModel: OSSubscriptionModel, identityModel: OSIdentityModel) { self.subscriptionModel = subscriptionModel + self.identityModel = identityModel self.stringDescription = "" super.init() self.method = DELETE @@ -64,6 +68,7 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { func encode(with coder: NSCoder) { coder.encode(subscriptionModel, forKey: "subscriptionModel") + coder.encode(identityModel, forKey: "identityModel") coder.encode(method.rawValue, forKey: "method") // Encodes as String coder.encode(timestamp, forKey: "timestamp") } @@ -71,6 +76,7 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { required init?(coder: NSCoder) { guard let subscriptionModel = coder.decodeObject(forKey: "subscriptionModel") as? OSSubscriptionModel, + let identityModel = coder.decodeObject(forKey: "identityModel") as? OSIdentityModel, let rawMethod = coder.decodeObject(forKey: "method") as? UInt32, let timestamp = coder.decodeObject(forKey: "timestamp") as? Date else { @@ -78,6 +84,7 @@ class OSRequestDeleteSubscription: OneSignalRequest, OSUserRequest { return nil } self.subscriptionModel = subscriptionModel + self.identityModel = identityModel self.stringDescription = "" super.init() self.method = HTTPMethod(rawValue: rawMethod) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift index 06d1afac7..91e129e08 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift @@ -63,8 +63,11 @@ class OSUserUtils { if let pushSubscriptionId = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionId { headers["OneSignal-Subscription-Id"] = pushSubscriptionId } - if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address { - headers["Device-Auth-Push-Token"] = "Basic \(token)" + if let token = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.address, + let data = token.data(using: .utf8) + { + let base64String = data.base64EncodedString() + headers["Device-Auth-Push-Token"] = "Basic \(base64String)" } return headers } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index 240b7c752..8ba3bca02 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -190,4 +190,125 @@ final class SubscriptionExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self)) XCTAssertTrue(invalidatedCallbackWasCalled) } + + func testCreateSubscriptionRequests_Retry_OnTokenUpdate() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true + + let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + user.identityModel.jwtBearerToken = userA_InvalidJwtToken + + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor! + + let email = userA_email + MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) + executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in + invalidatedCallbackWasCalled = true + MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertEqual(mocks.client.networkRequestCount, 2) + } + + func testCreateSubscriptionRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken + + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + userB.identityModel.jwtBearerToken = userA_InvalidJwtToken + + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor! + + let email = userA_email + MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) + + executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userB.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + let addRequests = mocks.client.executedRequests.filter { request in + request.isKind(of: OSRequestCreateSubscription.self) + } + + XCTAssertEqual(addRequests.count, 3) + } + + func testDeleteSubscriptionRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { + /* Setup */ + let mocks = Mocks() + mocks.setAuthRequired(true) + + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken + + let userB = mocks.setUserManagerInternalUser(externalId: userB_EUID, onesignalId: userB_OSID) + userB.identityModel.jwtBearerToken = userA_InvalidJwtToken + + // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor + let executor = OneSignalUserManagerImpl.sharedInstance.subscriptionExecutor! + + let email = userA_email + MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) + + executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userB.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) + + var invalidatedCallbackWasCalled = false + OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in + invalidatedCallbackWasCalled = true + } + + /* When */ + executor.processDeltaQueue(inBackground: false) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userB_EUID, token: userB_ValidJwtToken) + + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + + /* Then */ + // The executor should execute this request since identity verification is required and the token was set + XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self)) + XCTAssertTrue(invalidatedCallbackWasCalled) + let deleteRequests = mocks.client.executedRequests.filter { request in + request.isKind(of: OSRequestDeleteSubscription.self) + } + + XCTAssertEqual(deleteRequests.count, 3) + } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift index 4169325cc..bf95fdd9e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -85,35 +85,38 @@ final class UserConcurrencyTests: XCTestCase { This test reproduces a crash in the Subscription Executor. It is possible for two threads to modify and cache queues concurrently. */ - // TODO: revisit this test once subscriptions are addressed for JWT func testSubscriptionExecutorConcurrency() throws { /* Setup */ let client = MockOneSignalClient() client.setMockResponseForRequest( - request: "", + request: "", response: [:] ) OneSignalCoreImpl.setSharedClient(client) - let jwtConfig = OSUserJwtConfig() - let executor = OSSubscriptionOperationExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: jwtConfig) - let operationRepo = OSOperationRepo(jwtConfig: jwtConfig) - operationRepo.addExecutor(executor) + // Set JWT to off, before accessing the User Manager + OneSignalUserManagerImpl.sharedInstance.setRequiresUserAuth(false) + + let identityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: UUID().uuidString], changeNotifier: OSEventProducer()) + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(identityModel) + + let executor = OSSubscriptionOperationExecutor(newRecordsState: OSNewRecordsState(), jwtConfig: OneSignalUserManagerImpl.sharedInstance.jwtConfig) + OneSignalUserManagerImpl.sharedInstance.operationRepo.addExecutor(executor) /* When */ DispatchQueue.concurrentPerform(iterations: 50) { _ in // 1. Enqueue Remove Subscription Deltas to the Operation Repo - operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) - operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: UUID().uuidString, model: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "email", value: "email")) + OneSignalUserManagerImpl.sharedInstance.operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: identityModel.modelId, model: OSSubscriptionModel(type: .email, address: userA_email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: userA_email)) + OneSignalUserManagerImpl.sharedInstance.operationRepo.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: identityModel.modelId, model: OSSubscriptionModel(type: .email, address: userA_email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: userA_email)) // 2. Flush Operation Repo - operationRepo.addFlushDeltaQueueToDispatchQueue() + OneSignalUserManagerImpl.sharedInstance.operationRepo.addFlushDeltaQueueToDispatchQueue() // 3. Simulate updating the executor's request queue from a network response - executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) - executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: nil, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer())), inBackground: false) + executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: userA_email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), identityModel: identityModel), inBackground: false) + executor.executeDeleteSubscriptionRequest(OSRequestDeleteSubscription(subscriptionModel: OSSubscriptionModel(type: .email, address: userA_email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), identityModel: identityModel), inBackground: false) } // 4. Run background threads From c27842c462f08823310f1a8de6bf362b63753c65 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 24 Sep 2024 16:45:14 -0700 Subject: [PATCH 42/56] [nits] Move public protocols out of User Manager file * OneSignalUserManagerImpl.swift violated the 1000 line file limit of Swiftlint * Options include modifing the rule but let's pull out 2 public protocols. * Additionally add more folders to organize the top-level files: MODELING for models and listeners, PUBLIC for publicly accessed objects and protocols --- .../OneSignal.xcodeproj/project.pbxproj | 42 ++++++++++--- .../{ => Modeling}/OSIdentityModel.swift | 0 .../OSIdentityModelStoreListener.swift | 0 .../{ => Modeling}/OSPropertiesModel.swift | 0 .../OSPropertiesModelStoreListener.swift | 0 .../{ => Modeling}/OSSubscriptionModel.swift | 0 .../OSSubscriptionModelStoreListener.swift | 0 .../Source/OneSignalUserManagerImpl.swift | 51 --------------- .../{ => Public}/OSJwtInvalidatedEvent.swift | 0 .../Source/Public/OSPushSubscription.swift | 40 ++++++++++++ .../OneSignalUser/Source/Public/OSUser.swift | 63 +++++++++++++++++++ .../Source/{ => Public}/OSUserState.swift | 0 12 files changed, 136 insertions(+), 60 deletions(-) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSIdentityModel.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSIdentityModelStoreListener.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSPropertiesModel.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSPropertiesModelStoreListener.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSSubscriptionModel.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Modeling}/OSSubscriptionModelStoreListener.swift (100%) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Public}/OSJwtInvalidatedEvent.swift (100%) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSPushSubscription.swift create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/{ => Public}/OSUserState.swift (100%) diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 2053fd1fc..a1a0f03dc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -75,6 +75,8 @@ 3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */; }; 3C2D8A5928B4C4E300BE41F6 /* OSDelta.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */; }; 3C2FF9D02C5FCD760081293B /* OSUserJwtConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */; }; + 3C3130E02CA383F800906665 /* OSUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3130DF2CA383F800906665 /* OSUser.swift */; }; + 3C3130E32CA3858500906665 /* OSPushSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C3130E22CA3858500906665 /* OSPushSubscription.swift */; }; 3C44673E296D099D0039A49E /* OneSignalMobileProvision.m in Sources */ = {isa = PBXBuildFile; fileRef = 912411FD1E73342200E41FD7 /* OneSignalMobileProvision.m */; }; 3C44673F296D09CC0039A49E /* OneSignalMobileProvision.h in Headers */ = {isa = PBXBuildFile; fileRef = 912411FC1E73342200E41FD7 /* OneSignalMobileProvision.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C448B9D2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */; }; @@ -1238,6 +1240,8 @@ 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSSubscriptionModel.swift; sourceTree = ""; }; 3C2D8A5828B4C4E300BE41F6 /* OSDelta.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSDelta.swift; sourceTree = ""; }; 3C2FF9CF2C5FCD760081293B /* OSUserJwtConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserJwtConfig.swift; sourceTree = ""; }; + 3C3130DF2CA383F800906665 /* OSUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUser.swift; sourceTree = ""; }; + 3C3130E22CA3858500906665 /* OSPushSubscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSPushSubscription.swift; sourceTree = ""; }; 3C448B9B2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSBackgroundTaskHandlerImpl.h; sourceTree = ""; }; 3C448B9C2936ADFD002F96BC /* OSBackgroundTaskHandlerImpl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSBackgroundTaskHandlerImpl.m; sourceTree = ""; }; 3C448BA12936B474002F96BC /* OSBackgroundTaskManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSBackgroundTaskManager.swift; sourceTree = ""; }; @@ -2101,6 +2105,30 @@ path = Jwt; sourceTree = ""; }; + 3C3130E12CA384BD00906665 /* Modeling */ = { + isa = PBXGroup; + children = ( + 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */, + 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */, + 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */, + 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */, + 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */, + 3CE795F828DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift */, + ); + path = Modeling; + sourceTree = ""; + }; + 3C3130E52CA385B700906665 /* Public */ = { + isa = PBXGroup; + children = ( + 3C3130DF2CA383F800906665 /* OSUser.swift */, + 3C3130E22CA3858500906665 /* OSPushSubscription.swift */, + 3C5117162B15C31E00563465 /* OSUserState.swift */, + DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */, + ); + path = Public; + sourceTree = ""; + }; 3C8544B72C5AEFF700F542A9 /* OneSignalOSCoreMocks */ = { isa = PBXGroup; children = ( @@ -2493,20 +2521,14 @@ isa = PBXGroup; children = ( 3CEE90A52BFE6A7700B0FB5B /* Support */, + 3C3130E12CA384BD00906665 /* Modeling */, 3C9AD6BA2B2284AB00BC1540 /* Executors */, 3C9AD6BD2B22877600BC1540 /* Requests */, + 3C3130E52CA385B700906665 /* Public */, DE69E1A9282ED8790090BB3D /* UnitTestApp-Bridging-Header.h */, - 3C0EF49D28A1DBCB00E5434B /* OSUserInternalImpl.swift */, DE69E1AA282ED8790090BB3D /* OneSignalUserManagerImpl.swift */, + 3C0EF49D28A1DBCB00E5434B /* OSUserInternalImpl.swift */, 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */, - 3C2C7DC7288F3C020020F9AE /* OSSubscriptionModel.swift */, - 3CE92279289FA88B001B1062 /* OSIdentityModelStoreListener.swift */, - 3CF8629D28A183F900776CA4 /* OSIdentityModel.swift */, - 3CF862A128A197D200776CA4 /* OSPropertiesModelStoreListener.swift */, - 3CF8629F28A1964F00776CA4 /* OSPropertiesModel.swift */, - 3CE795F828DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift */, - 3C5117162B15C31E00563465 /* OSUserState.swift */, - DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */, ); path = Source; sourceTree = ""; @@ -4315,6 +4337,7 @@ 3C277D7E2BD76E0000857606 /* OSIdentityModelRepo.swift in Sources */, 3CEE90A72BFE6ABD00B0FB5B /* OSPropertiesSupportedProperty.swift in Sources */, 3C9AD6C12B22886600BC1540 /* OSRequestUpdateSubscription.swift in Sources */, + 3C3130E02CA383F800906665 /* OSUser.swift in Sources */, 3C0EF49E28A1DBCB00E5434B /* OSUserInternalImpl.swift in Sources */, 3C8E6DFF28AB09AE0031E48A /* OSPropertyOperationExecutor.swift in Sources */, 3C9AD6CB2B228B5200BC1540 /* OSRequestIdentifyUser.swift in Sources */, @@ -4328,6 +4351,7 @@ 3C2C7DC8288F3C020020F9AE /* OSSubscriptionModel.swift in Sources */, 3CF8629E28A183F900776CA4 /* OSIdentityModel.swift in Sources */, 3CE795FB28DBDCE700736BD4 /* OSSubscriptionOperationExecutor.swift in Sources */, + 3C3130E32CA3858500906665 /* OSPushSubscription.swift in Sources */, 3C5117172B15C31E00563465 /* OSUserState.swift in Sources */, 3C9AD6BF2B22881D00BC1540 /* OSRequestFetchIdentityBySubscription.swift in Sources */, 3CE9227A289FA88B001B1062 /* OSIdentityModelStoreListener.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModel.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModelStoreListener.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelStoreListener.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModelStoreListener.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSPropertiesModel.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModel.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSPropertiesModel.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSPropertiesModelStoreListener.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSPropertiesModelStoreListener.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSPropertiesModelStoreListener.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModel.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSSubscriptionModelStoreListener.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 13968d7cd..56f73e4f4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -44,57 +44,6 @@ import OneSignalNotifications func sendPurchases(_ purchases: [[String: AnyObject]]) } -/** - This is the user interface exposed to the public. - */ -@objc public protocol OSUser { - var pushSubscription: OSPushSubscription { get } - var onesignalId: String? { get } - var externalId: String? { get } - /** - Add an observer to the user state, allowing the provider to be notified when the user state has changed. - Important: When using the observer to retrieve the `onesignalId`, check the `externalId` as well to confirm the values are associated with the expected user. - */ - func addObserver(_ observer: OSUserStateObserver) - func removeObserver(_ observer: OSUserStateObserver) - // Aliases - func addAlias(label: String, id: String) - func addAliases(_ aliases: [String: String]) - func removeAlias(_ label: String) - func removeAliases(_ labels: [String]) - // Tags - func addTag(key: String, value: String) - func addTags(_ tags: [String: String]) - func removeTag(_ tag: String) - func removeTags(_ tags: [String]) - func getTags() -> [String: String] - // Email - func addEmail(_ email: String) - func removeEmail(_ email: String) - // SMS - func addSms(_ number: String) - func removeSms(_ number: String) - // Language - func setLanguage(_ language: String) - // JWT Token Expire - typealias OSJwtInvalidatedHandler = (_ event: OSJwtInvalidatedEvent) -> Void - func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) -} - -/** - This is the push subscription interface exposed to the public. - */ -@objc public protocol OSPushSubscription { - var id: String? { get } - var token: String? { get } - var optedIn: Bool { get } - - func optIn() - func optOut() - func addObserver(_ observer: OSPushSubscriptionObserver) - func removeObserver(_ observer: OSPushSubscriptionObserver) -} - @objc public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { @objc public static let sharedInstance = OneSignalUserManagerImpl(jwtConfig: OSUserJwtConfig()) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSJwtInvalidatedEvent.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSJwtInvalidatedEvent.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSJwtInvalidatedEvent.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSPushSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSPushSubscription.swift new file mode 100644 index 000000000..0f2bddef7 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSPushSubscription.swift @@ -0,0 +1,40 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + This is the push subscription interface exposed to the public. + */ +@objc public protocol OSPushSubscription { + var id: String? { get } + var token: String? { get } + var optedIn: Bool { get } + + func optIn() + func optOut() + func addObserver(_ observer: OSPushSubscriptionObserver) + func removeObserver(_ observer: OSPushSubscriptionObserver) +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift new file mode 100644 index 000000000..28b556a4e --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift @@ -0,0 +1,63 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +/** + This is the user interface exposed to the public. + */ +@objc public protocol OSUser { + var pushSubscription: OSPushSubscription { get } + var onesignalId: String? { get } + var externalId: String? { get } + /** + Add an observer to the user state, allowing the provider to be notified when the user state has changed. + Important: When using the observer to retrieve the `onesignalId`, check the `externalId` as well to confirm the values are associated with the expected user. + */ + func addObserver(_ observer: OSUserStateObserver) + func removeObserver(_ observer: OSUserStateObserver) + // Aliases + func addAlias(label: String, id: String) + func addAliases(_ aliases: [String: String]) + func removeAlias(_ label: String) + func removeAliases(_ labels: [String]) + // Tags + func addTag(key: String, value: String) + func addTags(_ tags: [String: String]) + func removeTag(_ tag: String) + func removeTags(_ tags: [String]) + func getTags() -> [String: String] + // Email + func addEmail(_ email: String) + func removeEmail(_ email: String) + // SMS + func addSms(_ number: String) + func removeSms(_ number: String) + // Language + func setLanguage(_ language: String) + // JWT Token Expire + typealias OSJwtInvalidatedHandler = (_ event: OSJwtInvalidatedEvent) -> Void + func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSUserState.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUserState.swift similarity index 100% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSUserState.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUserState.swift From 74e4ef7c45aff03017741146852afab83cbd9252 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 25 Sep 2024 15:09:17 -0700 Subject: [PATCH 43/56] Update tests * Remove test on Update Subscription with JWT; it does not use User JWT * Make some changes to existing tests --- .../OneSignalExecutorMocks.swift | 3 +- .../Executors/IdentityExecutorTests.swift | 3 +- .../SubscriptionsExecutorTests.swift | 26 ----------------- .../Executors/UserExecutorTests.swift | 29 ++++++++++++------- .../UserConcurrencyTests.swift | 4 +-- 5 files changed, 24 insertions(+), 41 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift index 7965fd0f4..4709a0163 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/OneSignalExecutorMocks.swift @@ -61,7 +61,8 @@ open class OneSignalExecutorMocks: NSObject { externalId: externalId, pushSubscriptionModel: OSSubscriptionModel(type: .push, address: "", subscriptionId: testPushSubId, reachable: false, isDisabled: false, changeNotifier: OSEventProducer())) if let onesignalId = onesignalId { - user.identityModel.addAliases([OS_ONESIGNAL_ID: onesignalId]) + // Setting the OSID directly avoids generating a Delta + user.identityModel.aliases[OS_ONESIGNAL_ID] = onesignalId } return user } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index 3c986de11..a3afafa7b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -240,8 +240,7 @@ final class IdentityExecutorTests: XCTestCase { let addAliasRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestAddAliases.self) } - // It is 4 because setting user B's OneSignal ID counts as an add alias request - XCTAssertEqual(addAliasRequests.count, 4) + XCTAssertEqual(addAliasRequests.count, 3) } func testRemoveAliasRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index 8ba3bca02..c61cc5afe 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -165,32 +165,6 @@ final class SubscriptionExecutorTests: XCTestCase { XCTAssertTrue(invalidatedCallbackWasCalled) } - func testUpdateSubscription_IdentityVerificationRequired_withInvalidToken() { - /* Setup */ - let mocks = Mocks() - mocks.setAuthRequired(true) - OneSignalUserManagerImpl.sharedInstance.operationRepo.paused = true - - let user = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) - user.identityModel.jwtBearerToken = userA_InvalidJwtToken - let token = testPushToken - MockUserRequests.setUnauthorizedUpdateSubscriptionFailureResponse(with: mocks.client, token: token) - mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .push, address: token, subscriptionId: testPushSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: "token", value: token)) - - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } - - /* When */ - mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) - - /* Then */ - XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) - } - func testCreateSubscriptionRequests_Retry_OnTokenUpdate() { /* Setup */ let mocks = Mocks() diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index f23892c90..4612e1d0a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -348,12 +348,11 @@ final class UserExecutorTests: XCTestCase { mocks.setAuthRequired(true) - _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! - let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) @@ -364,8 +363,8 @@ final class UserExecutorTests: XCTestCase { } /* When */ - executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) - executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: userAIdentityModel) + executor.fetchUser(onesignalId: userA_OSID, identityModel: userA.identityModel) + executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: userA.identityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) @@ -379,24 +378,34 @@ final class UserExecutorTests: XCTestCase { XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) XCTAssertTrue(invalidatedCallbackWasCalled) - XCTAssertEqual(mocks.client.networkRequestCount, 4) + /* + Create and Fetch requests that fail + Create and Fetch requests that pass + Follow up Fetch made after the success of the Create request + */ + XCTAssertEqual(mocks.client.networkRequestCount, 5) } + /** + This test executes a Fetch on userA, and a Create on userB, encountering an unauthorized response for both requests. + The test next updates the JWT token for userA only. + It expects only the Fetch userA request to be sent next. + */ func testUserRequests_RetryRequests_OnTokenUpdate_ForOnlyUpdatedUser() { /* Setup */ let mocks = Mocks() mocks.setAuthRequired(true) - _ = mocks.setUserManagerInternalUser(externalId: userA_EUID) + let userA = mocks.setUserManagerInternalUser(externalId: userA_EUID, onesignalId: userA_OSID) // We need to use the user manager's executor because the onJWTUpdated callback won't fire on the mock executor let executor = OneSignalUserManagerImpl.sharedInstance.userExecutor! - let userAIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userA_OSID, OS_EXTERNAL_ID: userA_EUID], changeNotifier: OSEventProducer()) - userAIdentityModel.jwtBearerToken = userA_InvalidJwtToken + userA.identityModel.jwtBearerToken = userA_InvalidJwtToken let userBIdentityModel = OSIdentityModel(aliases: [OS_ONESIGNAL_ID: userB_OSID, OS_EXTERNAL_ID: userB_EUID], changeNotifier: OSEventProducer()) userBIdentityModel.jwtBearerToken = userA_InvalidJwtToken + OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(userBIdentityModel) MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userB_EUID) @@ -407,7 +416,7 @@ final class UserExecutorTests: XCTestCase { } /* When */ - executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) + executor.fetchUser(onesignalId: userA_OSID, identityModel: userA.identityModel) executor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userB_EUID, identityModel: userBIdentityModel) OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift index bf95fdd9e..52ffb5a79 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/UserConcurrencyTests.swift @@ -120,7 +120,7 @@ final class UserConcurrencyTests: XCTestCase { } // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2) /* Then */ // Previously caused crash: signal SIGABRT - malloc: double free for ptr @@ -165,7 +165,7 @@ final class UserConcurrencyTests: XCTestCase { } // 4. Run background threads - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 2) /* Then */ // Previously caused crash: signal SIGABRT - malloc: double free for ptr From 015632827f5a7afbe508049cc46cdc0ee4fdafc8 Mon Sep 17 00:00:00 2001 From: Nan Date: Mon, 30 Sep 2024 17:07:29 -0700 Subject: [PATCH 44/56] Update the remote params JWT key * Remote params returns `jwt_required` as the key to use --- .../OneSignalCore/Source/OneSignalCommonDefines.h | 3 +-- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 9 +++------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index 577386177..fc845fa26 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -79,7 +79,6 @@ #define OSUD_REQUIRES_USER_PRIVACY_CONSENT @"OSUD_REQUIRES_USER_PRIVACY_CONSENT" /* Identity Verification */ -// TODO: JWT 🔐 Figure out the key below and may need to relate to existing key IOS_REQUIRES_USER_ID_AUTHENTICATION #define OSUD_USE_IDENTITY_VERIFICATION @"OSUD_USE_IDENTITY_VERIFICATION" #define OS_JWT_BEARER_TOKEN @"OS_JWT_BEARER_TOKEN" #define OS_JWT_TOKEN_INVALID @"OS_JWT_TOKEN_INVALID" @@ -135,7 +134,7 @@ #define IOS_USES_PROVISIONAL_AUTHORIZATION @"uses_provisional_auth" #define IOS_REQUIRES_EMAIL_AUTHENTICATION @"require_email_auth" #define IOS_REQUIRES_SMS_AUTHENTICATION @"require_sms_auth" -#define IOS_REQUIRES_USER_ID_AUTHENTICATION @"require_user_id_auth" // TODO: JWT 🔐 Figure out the key, also think about needing to migrate this value +#define IOS_JWT_REQUIRED @"jwt_required" // Returned by remote params #define IOS_RECEIVE_RECEIPTS_ENABLE @"receive_receipts_enable" #define IOS_OUTCOMES_V2_SERVICE_ENABLE @"v2_enabled" #define IOS_LOCATION_SHARED @"location_shared" diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index db2745a86..b23d6da74 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -638,12 +638,9 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { NSString *userId = nil; [OneSignalCoreImpl.sharedClient executeRequest:[OSRequestGetIosParams withUserId:userId appId:appId] onSuccess:^(NSDictionary *result) { - - // TODO: JWT 🔐 Mock it for now to always be true - OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = true; -// if (result[IOS_REQUIRES_USER_ID_AUTHENTICATION]) { -// OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = [result[IOS_REQUIRES_USER_ID_AUTHENTICATION] boolValue]; -// } + if (result[IOS_JWT_REQUIRED]) { + OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = [result[IOS_JWT_REQUIRED] boolValue]; + } if (result[IOS_USES_PROVISIONAL_AUTHORIZATION] != (id)[NSNull null]) { [OneSignalUserDefaults.initStandard saveBoolForKey:OSUD_USES_PROVISIONAL_PUSH_AUTHORIZATION withValue:[result[IOS_USES_PROVISIONAL_AUTHORIZATION] boolValue]]; From a2420ee30257756491dbed77f1975a4790887761 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 1 Oct 2024 08:51:02 -0700 Subject: [PATCH 45/56] Re-use existing identity models for new users * If logging into an external ID that already exists in the SDK, re-use that one to keep the same model. --- .../Source/OSIdentityModelRepo.swift | 2 +- .../Source/OneSignalUserManagerImpl.swift | 30 ++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index 2b43a64e8..66c0ec10c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -109,7 +109,7 @@ extension OSIdentityModelRepo: OSModelChangedHandler { extension OSIdentityModelRepo: OSLoggable { func logSelf() { - print("OSIdentityModelRepo has the following models: ") + print("🥭 OSIdentityModelRepo has the following models: ") for model in models.values { print(" modelID: \(model.modelId), alises: \(model.aliases) token: \(model.jwtBearerToken ?? "nil")") } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 56f73e4f4..7e627f7e8 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -380,17 +380,29 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { OneSignalUserManagerImpl.sharedInstance.subscriptionModelStore.clearModelsFromStore() } + /** + Entry point to creating and setting a user. It is called by the SDK to generate an anonymous user and also + by clients to login a user. + */ private func _login(externalId: String?, token: String?) -> OSUserInternal { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return _mockUser } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignalUserManager internal _login called with externalId: \(externalId ?? "nil")") - // Logging into an identified user from an anonymous user, if JWT is not ON + /* + Logging in to a "new-to-the-sdk" externalId from an anonymous user, if JWT is OFF or UNKNOWN. + + Note: If we are logging in to an externalId that already exists in the SDK, from an anon user, we know the client has called: + login(userA) -> logout -> login(userA) + The userA is expected to exist and will not result in successfully identifying the anonymous user; + this login flow will instead fall into the createUser path below. + */ if let externalId = externalId, let user = _user, user.isAnonymous, - jwtConfig.isRequired != true + jwtConfig.isRequired != true, + identityModelRepo.get(externalId: externalId) == nil { user.identityModel.jwtBearerToken = token identifyUser(externalId: externalId, currentUser: user) @@ -449,15 +461,25 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { */ func setNewInternalUser(externalId: String?, pushSubscriptionModel: OSSubscriptionModel?) -> OSUserInternal { let aliases: [String: String]? + let identityModel: OSIdentityModel + if let externalIdToUse = externalId { aliases = [OS_EXTERNAL_ID: externalIdToUse] } else { aliases = nil } - let identityModel = OSIdentityModel(aliases: aliases, changeNotifier: OSEventProducer()) + // If there is an existing identity model with the same external ID, use it + if let externalId = externalId, + let existingIdentityModel = identityModelRepo.get(externalId: externalId) + { + identityModel = existingIdentityModel + } else { + identityModel = OSIdentityModel(aliases: aliases, changeNotifier: OSEventProducer()) + self.addIdentityModelToRepo(identityModel) + } + self.identityModelStore.add(id: OS_IDENTITY_MODEL_KEY, model: identityModel, hydrating: false) - self.addIdentityModelToRepo(identityModel) let propertiesModel = OSPropertiesModel(changeNotifier: OSEventProducer()) self.propertiesModelStore.add(id: OS_PROPERTIES_MODEL_KEY, model: propertiesModel, hydrating: false) From 5433ebb7889a73dc2a418b2b6b746ce8d55b3423 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 1 Oct 2024 09:04:21 -0700 Subject: [PATCH 46/56] Remove duplicate Create User requests * If multiple create user requests are enqueued for the same external ID, only keep the most recent one, and remove the previous. * These requests should all have the same identity model since they share external IDs, so only keeping the latest is adequate. * This prevents multiple Create User requests with the same external ID from being executed simultaneously, which is possible when JWT is on, as we allow future logins to be sent before past user's login succeeds. * An example of this is login(a) > login(b) > login(a) > login(b) but user A has an expired token. Once the token is updated for userA, potentially both logins could be executed if we don't prevent duplicates. --- .../Source/Executors/OSUserExecutor.swift | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index b082eebc8..25432448e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -181,6 +181,26 @@ class OSUserExecutor { OneSignalUserManagerImpl.sharedInstance.addIdentityModelToRepo(model) } + /// Checks if two requests are creating the same user by external ID + private func isDuplicateCreateUser(_ request: OSRequestCreateUser, _ other: OSUserRequest) -> Bool { + guard let other = other as? OSRequestCreateUser, + request.identityModel.externalId != nil, + other.identityModel.externalId != nil + else { + return false + } + return request.identityModel.externalId == other.identityModel.externalId + } + + /// Before enqueueing a Create User request, check for duplicates and remove previous matching duplicates + private func appendCreateUserToQueue(_ request: OSRequestCreateUser) { + self.dispatchQueue.async { + self.userRequestQueue.removeAll(where: { self.isDuplicateCreateUser(request, $0) }) + self.userRequestQueue.append(request) + OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) + } + } + func appendToQueue(_ request: OSUserRequest) { self.dispatchQueue.async { self.userRequestQueue.append(request) @@ -286,8 +306,7 @@ extension OSUserExecutor { let originalPushToken = user.pushSubscriptionModel.address let request = OSRequestCreateUser(identityModel: user.identityModel, propertiesModel: user.propertiesModel, pushSubscriptionModel: user.pushSubscriptionModel, originalPushToken: originalPushToken) - appendToQueue(request) - + appendCreateUserToQueue(request) executePendingRequests() } @@ -296,7 +315,7 @@ extension OSUserExecutor { */ func createUser(aliasLabel: String, aliasId: String, identityModel: OSIdentityModel) { let request = OSRequestCreateUser(aliasLabel: aliasLabel, aliasId: aliasId, identityModel: identityModel) - appendToQueue(request) + appendCreateUserToQueue(request) executePendingRequests() } From 01fde6ffad9c7681c048daf4b8cae2721e69cf33 Mon Sep 17 00:00:00 2001 From: Nan Date: Tue, 1 Oct 2024 09:22:24 -0700 Subject: [PATCH 47/56] Don't send push sub for previous users to avoid transfer * Remove the push subscription if not current user; we don't want to transfer the push sub. * This detail is meant to handle JWT on, and previous failed user creates can be sent even though the user has changed successfully. * However, don't remove the push sub if the user is anonymous or else the create will fail. Also, when JWT is off and anonymous users can be created, this will block requests until it succeeds so there is no risk of accidentally transferring the push sub to an old user. --- .../Source/Executors/OSUserExecutor.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 25432448e..82515739b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -342,11 +342,20 @@ extension OSUserExecutor { return } - // Hook up push subscription model if exists, it may be updated with a subscription_id, etc. - if let modelId = request.pushSubscriptionModel?.modelId, - let pushSubscriptionModel = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStore.getModel(modelId: modelId) { - request.pushSubscriptionModel = pushSubscriptionModel - request.updatePushSubscriptionModel(pushSubscriptionModel) + if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel) { + // Hook up push subscription model if exists, it may be updated with a subscription_id, etc. + if let modelId = request.pushSubscriptionModel?.modelId, + let pushSubscriptionModel = OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModelStore.getModel(modelId: modelId) { + request.pushSubscriptionModel = pushSubscriptionModel + request.updatePushSubscriptionModel(pushSubscriptionModel) + } + } else if request.identityModel.externalId != nil { + /* + Remove the push subscription if not current user; we don't want to transfer the push sub. + However, don't remove if the user is anonymous or else the create will fail. + This detail is meant to handle JWT on, and previous failed user creates can be sent even though the user has changed. + */ + request.parameters?.removeValue(forKey: "subscriptions") } guard request.addJWTHeaderIsValid(identityModel: request.identityModel) else { From 8d0d088626c26f55334e5c1b14bd13a2859b177a Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 08:34:32 -0700 Subject: [PATCH 48/56] Extract out User Manager Loggable extension for file length --- .../OneSignal.xcodeproj/project.pbxproj | 4 + .../OneSignalUserManagerImpl+OSLoggable.swift | 79 +++++++++++++++++++ .../Source/OneSignalUserManagerImpl.swift | 50 ------------ 3 files changed, 83 insertions(+), 50 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index a1a0f03dc..fdb97cca2 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -88,6 +88,7 @@ 3C47A975292642B100312125 /* OneSignalConfigManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3C47A973292642B100312125 /* OneSignalConfigManager.m */; }; 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */; }; 3C5117172B15C31E00563465 /* OSUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5117162B15C31E00563465 /* OSUserState.swift */; }; + 3C5929E32CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */; }; 3C62999F2BEEA34800649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C62999E2BEEA34800649187 /* PrivacyInfo.xcprivacy */; }; 3C6299A12BEEA38100649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A02BEEA38100649187 /* PrivacyInfo.xcprivacy */; }; 3C6299A32BEEA3CC00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */; }; @@ -1249,6 +1250,7 @@ 3C47A973292642B100312125 /* OneSignalConfigManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalConfigManager.m; sourceTree = ""; }; 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSOperationRepo.swift; sourceTree = ""; }; 3C5117162B15C31E00563465 /* OSUserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserState.swift; sourceTree = ""; }; + 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalUserManagerImpl+OSLoggable.swift"; sourceTree = ""; }; 3C62999E2BEEA34800649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299A02BEEA38100649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -2527,6 +2529,7 @@ 3C3130E52CA385B700906665 /* Public */, DE69E1A9282ED8790090BB3D /* UnitTestApp-Bridging-Header.h */, DE69E1AA282ED8790090BB3D /* OneSignalUserManagerImpl.swift */, + 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */, 3C0EF49D28A1DBCB00E5434B /* OSUserInternalImpl.swift */, 3C277D7D2BD76E0000857606 /* OSIdentityModelRepo.swift */, ); @@ -4328,6 +4331,7 @@ 3CE795F928DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift in Sources */, DE69E1AC282ED87A0090BB3D /* OneSignalUserManagerImpl.swift in Sources */, DE1DD0602C87D87B00787071 /* OSJwtInvalidatedEvent.swift in Sources */, + 3C5929E32CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift in Sources */, 3C9AD6CF2B228B7800BC1540 /* OSRequestAddAliases.swift in Sources */, 3C9AD6D32B228BB000BC1540 /* OSRequestUpdateProperties.swift in Sources */, 3C9AD6CD2B228B6300BC1540 /* OSRequestFetchUser.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift new file mode 100644 index 000000000..90f684a8c --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift @@ -0,0 +1,79 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection + with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalCore +import OneSignalOSCore + +extension OneSignalUserManagerImpl: OSLoggable { + @objc public func logSelf() { + print("💛 _user: \(String(describing: _user))") + print( + """ + 💛 identityModel: + aliases: \(String(describing: _user?.identityModel.aliases)) + jwt: \(String(describing: _user?.identityModel.jwtBearerToken)) + modelId: \(String(describing: _user?.identityModel.modelId)) + """ + ) + print( + """ + 💛 propertiesModel: + tags: \(String(describing: _user?.propertiesModel.tags)) + language: \(String(describing: _user?.propertiesModel.language)) + modelId: \(String(describing: _user?.propertiesModel.modelId)) + """ + ) + let subscriptionModels = subscriptionModelStore.getModels().values + for sub in subscriptionModels { + print( + """ + 💛 subscription model from store + addess: \(String(describing: sub.address)) + subscriptionId: \(String(describing: sub.subscriptionId)) + enabled: \(sub.enabled) + modelId: \(sub.modelId) + """ + ) + } + let pushSubModel = pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY) + print( + """ + 💛 push sub model from store + token: \(String(describing: pushSubModel?.address)) + subscriptionId: \(String(describing: pushSubModel?.subscriptionId)) + enabled: \(String(describing: pushSubModel?.enabled)) + notification_types: \(String(describing: pushSubModel?.notificationTypes)) + optedIn: \(String(describing: pushSubModel?.optedIn)) + modelId: \(String(describing: pushSubModel?.modelId)) + """ + ) + operationRepo.logSelf() + userExecutor?.logSelf() + identityModelRepo.logSelf() + print("") + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 7e627f7e8..921d2e6d6 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -950,53 +950,3 @@ extension OneSignalUserManagerImpl: OneSignalNotificationsDelegate { user.pushSubscriptionModel.address = pushToken } } - -extension OneSignalUserManagerImpl: OSLoggable { - @objc public func logSelf() { - print("💛 _user: \(String(describing: _user))") - print( - """ - 💛 identityModel: - aliases: \(String(describing: _user?.identityModel.aliases)) - jwt: \(String(describing: _user?.identityModel.jwtBearerToken)) - modelId: \(String(describing: _user?.identityModel.modelId)) - """ - ) - print( - """ - 💛 propertiesModel: - tags: \(String(describing: _user?.propertiesModel.tags)) - language: \(String(describing: _user?.propertiesModel.language)) - modelId: \(String(describing: _user?.propertiesModel.modelId)) - """ - ) - let subscriptionModels = subscriptionModelStore.getModels().values - for sub in subscriptionModels { - print( - """ - 💛 subscription model from store - addess: \(String(describing: sub.address)) - subscriptionId: \(String(describing: sub.subscriptionId)) - enabled: \(sub.enabled) - modelId: \(sub.modelId) - """ - ) - } - let pushSubModel = pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY) - print( - """ - 💛 push sub model from store - token: \(String(describing: pushSubModel?.address)) - subscriptionId: \(String(describing: pushSubModel?.subscriptionId)) - enabled: \(String(describing: pushSubModel?.enabled)) - notification_types: \(String(describing: pushSubModel?.notificationTypes)) - optedIn: \(String(describing: pushSubModel?.optedIn)) - modelId: \(String(describing: pushSubModel?.modelId)) - """ - ) - operationRepo.logSelf() - userExecutor?.logSelf() - identityModelRepo.logSelf() - print("") - } -} From b5ae6421ee4d69bd064db6599f2fab0847f9ae3a Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 20:50:41 -0700 Subject: [PATCH 49/56] Update JWT invalidated listener and event API * Update the API for the listener, add and removal function names, event name * The listener API is OSUserJwtInvalidatedListener * The event is OSUserJwtInvalidatedEvent --- .../OneSignalDevApp/AppDelegate.h | 2 +- .../OneSignalDevApp/AppDelegate.m | 10 +++--- .../OneSignalDevApp/SwiftTest.swift | 11 +++++- .../OneSignal.xcodeproj/project.pbxproj | 8 ++--- .../Source/OneSignalUserManagerImpl.swift | 34 +++++++++++++------ .../OneSignalUser/Source/Public/OSUser.swift | 3 -- ....swift => OSUserJwtInvalidatedEvent.swift} | 14 ++++++-- iOS_SDK/OneSignalSDK/Source/OneSignal.m | 10 ++++++ .../OneSignalSDK/Source/OneSignalFramework.h | 5 ++- .../Source/OneSignalSwiftInterface.swift | 8 +++++ 10 files changed, 76 insertions(+), 29 deletions(-) rename iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/{OSJwtInvalidatedEvent.swift => OSUserJwtInvalidatedEvent.swift} (81%) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h index b94a956d6..e1900bdac 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.h @@ -31,7 +31,7 @@ #import #import -@interface AppDelegate : UIResponder +@interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow *window; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index 80a8deca5..cc0b86083 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -76,11 +76,7 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( [OneSignal.User addObserver:self]; [OneSignal.Notifications addPermissionObserver:self]; [OneSignal.InAppMessages addClickListener:self]; - - - [OneSignal.User onJwtInvalidatedWithInvalidatedHandler:^(OSJwtInvalidatedEvent * _Nonnull invalidatedEvent) { - NSLog(@"JWT INVALIDATED CALLBACK FOR: %@", invalidatedEvent.externalId); - }]; + [OneSignal addUserJwtInvalidatedListener:self]; NSLog(@"UNUserNotificationCenter.delegate: %@", UNUserNotificationCenter.currentNotificationCenter.delegate); @@ -130,6 +126,10 @@ - (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state { NSLog(@"Dev App onUserStateDidChangeWithState: %@", [state jsonRepresentation]); } +- (void)onUserJwtInvalidatedWithEvent:(OSUserJwtInvalidatedEvent * _Nonnull)event { + NSLog(@"Dev App onUserJwtInvalidatedWithEvent: %@", [event jsonRepresentation]); +} + #pragma mark OSInAppMessageDelegate - (void)onClickInAppMessage:(OSInAppMessageClickEvent * _Nonnull)event { diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift index ddae6a8ec..57a662b73 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/SwiftTest.swift @@ -28,10 +28,19 @@ import Foundation import OneSignalFramework -class SwiftTest: NSObject { +class SwiftTest: NSObject, OSUserJwtInvalidatedListener { + func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) { + print("event: \(event.jsonRepresentation())") + print("externalId: \(event.externalId)") + } + func testSwiftUserModel() { let token1 = OneSignal.User.pushSubscription.token let token = OneSignal.User.pushSubscription.token OneSignal.Debug._dump() + OneSignal.login(externalId: "euid", token: "token") + OneSignal.updateUserJwt(externalId: "euid", token: "token") + OneSignal.addUserJwtInvalidatedListener(self) + OneSignal.removeUserJwtInvalidatedListener(self) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index fdb97cca2..361c68d16 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -351,7 +351,7 @@ DE16C14524D3724700670EFA /* OneSignalLifecycleObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */; }; DE16C14724D3727200670EFA /* OneSignalLifecycleObserver.h in Headers */ = {isa = PBXBuildFile; fileRef = DE16C14624D3727200670EFA /* OneSignalLifecycleObserver.h */; }; DE16C17024D3989A00670EFA /* OneSignalLifecycleObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */; }; - DE1DD0602C87D87B00787071 /* OSJwtInvalidatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */; }; + DE1DD0602C87D87B00787071 /* OSUserJwtInvalidatedEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1DD05F2C87D87B00787071 /* OSUserJwtInvalidatedEvent.swift */; }; DE20425E24E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; DE20425F24E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; DE20426024E21C2C00350E4F /* UIApplication+OneSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */; }; @@ -1521,7 +1521,7 @@ CACBAAAB218A662B000ACAA5 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; DE16C14324D3724700670EFA /* OneSignalLifecycleObserver.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalLifecycleObserver.m; sourceTree = ""; }; DE16C14624D3727200670EFA /* OneSignalLifecycleObserver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalLifecycleObserver.h; sourceTree = ""; }; - DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSJwtInvalidatedEvent.swift; sourceTree = ""; }; + DE1DD05F2C87D87B00787071 /* OSUserJwtInvalidatedEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserJwtInvalidatedEvent.swift; sourceTree = ""; }; DE20425C24E21C1500350E4F /* UIApplication+OneSignal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIApplication+OneSignal.h"; sourceTree = ""; }; DE20425D24E21C2C00350E4F /* UIApplication+OneSignal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIApplication+OneSignal.m"; sourceTree = ""; }; DE3568E92C88F56600AF447C /* PropertyExecutorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyExecutorTests.swift; sourceTree = ""; }; @@ -2126,7 +2126,7 @@ 3C3130DF2CA383F800906665 /* OSUser.swift */, 3C3130E22CA3858500906665 /* OSPushSubscription.swift */, 3C5117162B15C31E00563465 /* OSUserState.swift */, - DE1DD05F2C87D87B00787071 /* OSJwtInvalidatedEvent.swift */, + DE1DD05F2C87D87B00787071 /* OSUserJwtInvalidatedEvent.swift */, ); path = Public; sourceTree = ""; @@ -4330,7 +4330,7 @@ files = ( 3CE795F928DB99B500736BD4 /* OSSubscriptionModelStoreListener.swift in Sources */, DE69E1AC282ED87A0090BB3D /* OneSignalUserManagerImpl.swift in Sources */, - DE1DD0602C87D87B00787071 /* OSJwtInvalidatedEvent.swift in Sources */, + DE1DD0602C87D87B00787071 /* OSUserJwtInvalidatedEvent.swift in Sources */, 3C5929E32CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift in Sources */, 3C9AD6CF2B228B7800BC1540 /* OSRequestAddAliases.swift in Sources */, 3C9AD6D32B228BB000BC1540 /* OSRequestUpdateProperties.swift in Sources */, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 921d2e6d6..8a1840909 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -73,9 +73,19 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { var hasCalledStart = false - private var jwtInvalidatedHandler: OSJwtInvalidatedHandler? let jwtConfig: OSUserJwtConfig + private var _userJwtInvalidatedObserver: OSObservable? + var userJwtInvalidatedObserver: OSObservable { + if let observer = _userJwtInvalidatedObserver { + return observer + } + let userJwtInvalidatedObserver = OSObservable(change: #selector(OSUserJwtInvalidatedListener.onUserJwtInvalidated(event:))) + _userJwtInvalidatedObserver = userJwtInvalidatedObserver + + return userJwtInvalidatedObserver + } + var user: OSUserInternal { guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: nil) else { return _mockUser @@ -633,6 +643,16 @@ extension OneSignalUserManagerImpl { // MARK: - JWT extension OneSignalUserManagerImpl { + @objc + public func addUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) { + self.userJwtInvalidatedObserver.addObserver(listener) + } + + @objc + public func removeUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) { + self.userJwtInvalidatedObserver.removeObserver(listener) + } + @objc public func setRequiresUserAuth(_ required: Bool) { jwtConfig.isRequired = required @@ -664,12 +684,8 @@ extension OneSignalUserManagerImpl { } private func fireJwtExpired(externalId: String) { - guard let jwtInvalidatedHandler = self.jwtInvalidatedHandler else { - return - } - let invalidatedEvent = OSJwtInvalidatedEvent(externalId: externalId) - - jwtInvalidatedHandler(invalidatedEvent) + let event = OSUserJwtInvalidatedEvent(externalId: externalId) + userJwtInvalidatedObserver.notifyChange(event) } private func getMessageFromJwtError(_ error: NSError) -> String { @@ -683,10 +699,6 @@ extension OneSignalUserManagerImpl { } extension OneSignalUserManagerImpl: OSUser { - public func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) { - jwtInvalidatedHandler = invalidatedHandler - } - public var User: OSUser { start() return self diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift index 28b556a4e..9693ff25b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUser.swift @@ -57,7 +57,4 @@ func removeSms(_ number: String) // Language func setLanguage(_ language: String) - // JWT Token Expire - typealias OSJwtInvalidatedHandler = (_ event: OSJwtInvalidatedEvent) -> Void - func onJwtInvalidated(invalidatedHandler: @escaping OSJwtInvalidatedHandler) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSJwtInvalidatedEvent.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUserJwtInvalidatedEvent.swift similarity index 81% rename from iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSJwtInvalidatedEvent.swift rename to iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUserJwtInvalidatedEvent.swift index f6fd98245..b4b14f054 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSJwtInvalidatedEvent.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Public/OSUserJwtInvalidatedEvent.swift @@ -25,12 +25,20 @@ THE SOFTWARE. */ -import Foundation - -@objc public class OSJwtInvalidatedEvent: NSObject { +@objc public class OSUserJwtInvalidatedEvent: NSObject { @objc public let externalId: String init(externalId: String) { self.externalId = externalId } + + @objc public func jsonRepresentation() -> NSDictionary { + return [ + "externalId": externalId + ] + } +} + +@objc public protocol OSUserJwtInvalidatedListener { + @objc func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) } diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index b23d6da74..7489caa7e 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -208,6 +208,16 @@ + (void)logout { [OneSignalUserManagerImpl.sharedInstance logout]; } ++ (void)addUserJwtInvalidatedListener:(id)listener { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"User Jwt Invalidated Listener added successfully"]; + [OneSignalUserManagerImpl.sharedInstance addUserJwtInvalidatedListener:listener]; +} + ++ (void)removeUserJwtInvalidatedListener:(id)listener { + [OneSignalLog onesignalLog:ONE_S_LL_VERBOSE message:@"User Jwt Invalidated Listener removed successfully"]; + [OneSignalUserManagerImpl.sharedInstance removeUserJwtInvalidatedListener:listener]; +} + + (void)updateUserJwt:(NSString * _Nonnull)externalId withToken:(NSString * _Nonnull)token { if ([OneSignalConfigManager shouldAwaitAppIdAndLogMissingPrivacyConsentForMethod:@"updateUserJwt"]) { return; diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h b/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h index 7315d413f..31dd53aa1 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalFramework.h @@ -70,7 +70,10 @@ typedef void (^OSFailureBlock)(NSError* error); + (void)login:(NSString * _Nonnull)externalId; + (void)login:(NSString * _Nonnull)externalId withToken:(NSString * _Nullable)token NS_SWIFT_NAME(login(externalId:token:)); -+ (void)updateUserJwt:(NSString * _Nonnull)externalId withToken:(NSString * _Nonnull)token; ++ (void)addUserJwtInvalidatedListener:(id _Nonnull)listener NS_REFINED_FOR_SWIFT; ++ (void)removeUserJwtInvalidatedListener:(id_Nonnull)listener NS_REFINED_FOR_SWIFT; ++ (void)updateUserJwt:(NSString * _Nonnull)externalId withToken:(NSString * _Nonnull)token +NS_SWIFT_NAME(updateUserJwt(externalId:token:)); + (void)logout; #pragma mark Notifications diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift index b29c525cb..ca386a8c3 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalSwiftInterface.swift @@ -33,6 +33,14 @@ import OneSignalNotifications import OneSignalCore public extension OneSignal { + static func addUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) { + __add(listener) + } + + static func removeUserJwtInvalidatedListener(_ listener: OSUserJwtInvalidatedListener) { + __remove(listener) + } + static var User: OSUser { return __user() } From 7d5db5e51605413e5dc9890f80487d5b3ef5da32 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 21:29:10 -0700 Subject: [PATCH 50/56] Update tests after JWT listener API change --- .../OneSignal.xcodeproj/project.pbxproj | 4 ++ .../MockUserJwtInvalidatedListener.swift | 46 +++++++++++++++++++ .../Executors/IdentityExecutorTests.swift | 40 +++++++--------- .../Executors/PropertyExecutorTests.swift | 24 ++++------ .../SubscriptionsExecutorTests.swift | 40 +++++++--------- .../Executors/UserExecutorTests.swift | 42 +++++++---------- 6 files changed, 109 insertions(+), 87 deletions(-) create mode 100644 iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserJwtInvalidatedListener.swift diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 361c68d16..e4dda7d43 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ 3C4F9E4428A4466C009F453A /* OSOperationRepo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */; }; 3C5117172B15C31E00563465 /* OSUserState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5117162B15C31E00563465 /* OSUserState.swift */; }; 3C5929E32CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */; }; + 3C5929E52CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C5929E42CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift */; }; 3C62999F2BEEA34800649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C62999E2BEEA34800649187 /* PrivacyInfo.xcprivacy */; }; 3C6299A12BEEA38100649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A02BEEA38100649187 /* PrivacyInfo.xcprivacy */; }; 3C6299A32BEEA3CC00649187 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */; }; @@ -1251,6 +1252,7 @@ 3C4F9E4328A4466C009F453A /* OSOperationRepo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSOperationRepo.swift; sourceTree = ""; }; 3C5117162B15C31E00563465 /* OSUserState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OSUserState.swift; sourceTree = ""; }; 3C5929E22CAD9EC50020D6FF /* OneSignalUserManagerImpl+OSLoggable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OneSignalUserManagerImpl+OSLoggable.swift"; sourceTree = ""; }; + 3C5929E42CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockUserJwtInvalidatedListener.swift; sourceTree = ""; }; 3C62999E2BEEA34800649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299A02BEEA38100649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 3C6299A22BEEA3CC00649187 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; @@ -2210,6 +2212,7 @@ 3C8706712BDEE076000D8CD2 /* MockUserDefines.swift */, 3CC063E52B6D7F96002BB07F /* OneSignalUserMocks.swift */, DE3568EB2C88F5BD00AF447C /* OneSignalExecutorMocks.swift */, + 3C5929E42CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift */, ); path = OneSignalUserMocks; sourceTree = ""; @@ -4167,6 +4170,7 @@ 3C8706702BDE0957000D8CD2 /* MockUserRequests.swift in Sources */, 3C8706722BDEE076000D8CD2 /* MockUserDefines.swift in Sources */, 3CC063E62B6D7F96002BB07F /* OneSignalUserMocks.swift in Sources */, + 3C5929E52CAE523E0020D6FF /* MockUserJwtInvalidatedListener.swift in Sources */, DE3568EC2C88F5BD00AF447C /* OneSignalExecutorMocks.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserJwtInvalidatedListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserJwtInvalidatedListener.swift new file mode 100644 index 000000000..0abd0d675 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/OneSignalUserMocks/MockUserJwtInvalidatedListener.swift @@ -0,0 +1,46 @@ +/* + Modified MIT License + + Copyright 2024 OneSignal + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + 1. The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + 2. All copies of substantial portions of the Software may only be used in connection +with services provided by OneSignal. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. + */ + +import OneSignalUser + +public class MockUserJwtInvalidatedListener: OSUserJwtInvalidatedListener { + public var invalidatedCallbackWasCalled = false + private var callback: (() -> Void)? + + public init() { } + + public func setCallback(_ callback: @escaping () -> Void) { + self.callback = callback + } + + public func onUserJwtInvalidated(event: OSUserJwtInvalidatedEvent) { + invalidatedCallbackWasCalled = true + + guard let callback = callback else { return } + callback() + } +} diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift index a3afafa7b..7c181f350 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/IdentityExecutorTests.swift @@ -125,10 +125,8 @@ final class IdentityExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: aliases) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) @@ -136,7 +134,7 @@ final class IdentityExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testRemoveAlias_IdentityVerificationRequired_withInvalidToken_firesCallback() { @@ -151,10 +149,8 @@ final class IdentityExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedRemoveAliasFailureResponse(with: mocks.client, aliasLabel: userA_AliasLabel) mocks.identityExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.identityExecutor.processDeltaQueue(inBackground: false) @@ -162,7 +158,7 @@ final class IdentityExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testAddAliasRequests_Retry_OnTokenUpdate() { @@ -182,12 +178,12 @@ final class IdentityExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedAddAliasFailureResponse(with: mocks.client, aliases: userA_Aliases) executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: user.identityModel.modelId, model: user.identityModel, property: "aliases", value: aliases)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + userJwtInvalidatedListener.setCallback { MockUserRequests.setAddAliasesResponse(with: mocks.client, aliases: aliases) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -195,7 +191,7 @@ final class IdentityExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } @@ -219,10 +215,8 @@ final class IdentityExecutorTests: XCTestCase { executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value: aliases)) executor.enqueueDelta(OSDelta(name: OS_ADD_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value: aliases)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -236,7 +230,7 @@ final class IdentityExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestAddAliases.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) let addAliasRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestAddAliases.self) } @@ -263,10 +257,8 @@ final class IdentityExecutorTests: XCTestCase { executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userA.identityModel.modelId, model: userA.identityModel, property: "aliases", value: aliases)) executor.enqueueDelta(OSDelta(name: OS_REMOVE_ALIAS_DELTA, identityModelId: userB.identityModel.modelId, model: userB.identityModel, property: "aliases", value: aliases)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -279,7 +271,7 @@ final class IdentityExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestRemoveAlias.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) let removeAliasRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestRemoveAlias.self) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift index 0961c876c..85dcef5f4 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/PropertyExecutorTests.swift @@ -126,10 +126,8 @@ final class PropertyExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) mocks.propertyExecutor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.propertyExecutor.processDeltaQueue(inBackground: false) @@ -137,7 +135,7 @@ final class PropertyExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testUpdateRequests_Retry_OnTokenUpdate() { @@ -157,12 +155,12 @@ final class PropertyExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedUpdatePropertiesFailureResponses(with: mocks.client, tags: tags) executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: user.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + userJwtInvalidatedListener.setCallback { MockUserRequests.setAddTagsResponse(with: mocks.client, tags: tags) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -170,7 +168,7 @@ final class PropertyExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } @@ -194,10 +192,8 @@ final class PropertyExecutorTests: XCTestCase { executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userA.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) executor.enqueueDelta(OSDelta(name: OS_UPDATE_PROPERTIES_DELTA, identityModelId: userB.identityModel.modelId, model: OSPropertiesModel(changeNotifier: OSEventProducer()), property: "tags", value: tags)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -211,7 +207,7 @@ final class PropertyExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestUpdateProperties.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) let updateRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestUpdateProperties.self) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift index c61cc5afe..c3fc5ab9c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/SubscriptionsExecutorTests.swift @@ -125,10 +125,8 @@ final class SubscriptionExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) @@ -136,7 +134,7 @@ final class SubscriptionExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testDeleteEmail_IdentityVerificationRequired_withInvalidToken() { @@ -151,10 +149,8 @@ final class SubscriptionExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedRemoveEmailFailureResponse(with: mocks.client, email: email) mocks.subscriptionExecutor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: testEmailSubId, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.subscriptionExecutor.processDeltaQueue(inBackground: false) @@ -162,7 +158,7 @@ final class SubscriptionExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testCreateSubscriptionRequests_Retry_OnTokenUpdate() { @@ -181,12 +177,12 @@ final class SubscriptionExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedAddEmailFailureResponse(with: mocks.client, email: email) executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: user.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + userJwtInvalidatedListener.setCallback { MockUserRequests.setAddEmailResponse(with: mocks.client, email: email) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -194,7 +190,7 @@ final class SubscriptionExecutorTests: XCTestCase { /* Then */ XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } @@ -218,10 +214,8 @@ final class SubscriptionExecutorTests: XCTestCase { executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) executor.enqueueDelta(OSDelta(name: OS_ADD_SUBSCRIPTION_DELTA, identityModelId: userB.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: nil, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -234,7 +228,7 @@ final class SubscriptionExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) let addRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestCreateSubscription.self) } @@ -262,10 +256,8 @@ final class SubscriptionExecutorTests: XCTestCase { executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userA.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) executor.enqueueDelta(OSDelta(name: OS_REMOVE_SUBSCRIPTION_DELTA, identityModelId: userB.identityModel.modelId, model: OSSubscriptionModel(type: .email, address: email, subscriptionId: UUID().uuidString, reachable: true, isDisabled: false, changeNotifier: OSEventProducer()), property: OSSubscriptionType.email.rawValue, value: email)) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.processDeltaQueue(inBackground: false) @@ -278,7 +270,7 @@ final class SubscriptionExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestDeleteSubscription.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) let deleteRequests = mocks.client.executedRequests.filter { request in request.isKind(of: OSRequestDeleteSubscription.self) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift index 4612e1d0a..63ffcde3d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUserTests/Executors/UserExecutorTests.swift @@ -232,10 +232,8 @@ final class UserExecutorTests: XCTestCase { newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.userExecutor.createUser(aliasLabel: OS_EXTERNAL_ID, aliasId: userA_EUID, identityModel: newIdentityModel) @@ -244,7 +242,7 @@ final class UserExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testFetchUser_IdentityVerificationRequired_butNoToken() { @@ -294,10 +292,8 @@ final class UserExecutorTests: XCTestCase { newIdentityModel.jwtBearerToken = userA_InvalidJwtToken MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ mocks.userExecutor.fetchUser(onesignalId: userA_OSID, identityModel: newIdentityModel) @@ -306,7 +302,7 @@ final class UserExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) } func testUserRequests_Retry_OnTokenUpdate() { @@ -324,21 +320,21 @@ final class UserExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + userJwtInvalidatedListener.setCallback { MockUserRequests.setDefaultFetchUserResponseForHydration(with: mocks.client, externalId: userA_EUID) OneSignalUserManagerImpl.sharedInstance.updateUserJwt(externalId: userA_EUID, token: userA_ValidJwtToken) } + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userAIdentityModel) - OneSignalCoreMocks.waitForBackgroundThreads(seconds: 0.5) + OneSignalCoreMocks.waitForBackgroundThreads(seconds: 1) /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 2) } @@ -357,10 +353,8 @@ final class UserExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userA_EUID) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userA.identityModel) @@ -377,7 +371,7 @@ final class UserExecutorTests: XCTestCase { // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestCreateUser.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) /* Create and Fetch requests that fail Create and Fetch requests that pass @@ -410,10 +404,8 @@ final class UserExecutorTests: XCTestCase { MockUserRequests.setUnauthorizedFetchUserFailureResponses(with: mocks.client, onesignalId: userA_OSID) MockUserRequests.setUnauthorizedCreateUserFailureResponses(with: mocks.client, externalId: userB_EUID) - var invalidatedCallbackWasCalled = false - OneSignalUserManagerImpl.sharedInstance.User.onJwtInvalidated { _ in - invalidatedCallbackWasCalled = true - } + let userJwtInvalidatedListener = MockUserJwtInvalidatedListener() + OneSignalUserManagerImpl.sharedInstance.addUserJwtInvalidatedListener(userJwtInvalidatedListener) /* When */ executor.fetchUser(onesignalId: userA_OSID, identityModel: userA.identityModel) @@ -429,7 +421,7 @@ final class UserExecutorTests: XCTestCase { /* Then */ // The executor should execute this request since identity verification is required and the token was set XCTAssertTrue(mocks.client.hasExecutedRequestOfType(OSRequestFetchUser.self)) - XCTAssertTrue(invalidatedCallbackWasCalled) + XCTAssertTrue(userJwtInvalidatedListener.invalidatedCallbackWasCalled) XCTAssertEqual(mocks.client.networkRequestCount, 3) } } From 0dc2cee78f4651249731b4befc23fd841529a309 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 08:42:21 -0700 Subject: [PATCH 51/56] Disable push sub when logout called (JWT on) --- .../Source/Modeling/OSSubscriptionModel.swift | 19 +++++++++++++++++++ .../OSSubscriptionModelStoreListener.swift | 8 ++++++++ .../Source/OneSignalUserManagerImpl.swift | 11 +++++++++++ .../OSRequestUpdateSubscription.swift | 14 ++++++++++---- 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift index 0a930320a..53510fa63 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModel.swift @@ -98,6 +98,10 @@ enum OSSubscriptionType: String { Internal subscription model. */ class OSSubscriptionModel: OSModel { + struct Constants { + static let isDisabledInternallyKey = "isDisabledInternallyKey" + } + var type: OSSubscriptionType var address: String? { // This is token on push subs so must remain Optional @@ -194,6 +198,21 @@ class OSSubscriptionModel: OSModel { } } + /** + Set to `true` by the SDK when logout is called with Identity Verification turned on. + The properties of `_isDisabled` and `notificationTypes` remain unchanged, to maintain correct data. + When a subscription update is made, this value will be read and `enabled = false` and `notification_types = -2` will be sent. + When a user logs in, this property will be set to `false` and the subscription will be included in the User Create request.. + */ + var _isDisabledInternally = false { + didSet { + guard _isDisabledInternally != oldValue else { + return + } + self.set(property: Constants.isDisabledInternallyKey, newValue: _isDisabledInternally) + } + } + // Properties for push subscription var testType: Int? { didSet { diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift index a7965a130..9d1651071 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSSubscriptionModelStoreListener.swift @@ -62,6 +62,14 @@ class OSSubscriptionModelStoreListener: OSModelStoreListener { } func getUpdateModelDelta(_ args: OSModelChangedArgs) -> OSDelta? { + /* + Don't generate a Delta if setting internal disable to false, which will generate a subscription update. + This means a user is logging in and a create user will be sent with the updated subscription included. + */ + if args.property == OSSubscriptionModel.Constants.isDisabledInternallyKey && args.newValue as? Bool == false { + return nil + } + return OSDelta( name: OS_UPDATE_SUBSCRIPTION_DELTA, identityModelId: OneSignalUserManagerImpl.sharedInstance.user.identityModel.modelId, diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 8a1840909..ef1c85486 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -400,6 +400,10 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { } OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OneSignalUserManager internal _login called with externalId: \(externalId ?? "nil")") + if externalId != nil { + pushSubscriptionModel?._isDisabledInternally = false + } + /* Logging in to a "new-to-the-sdk" externalId from an anonymous user, if JWT is OFF or UNKNOWN. @@ -440,6 +444,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { prepareForNewUser() _user = nil createUserIfNil() + + /* + If Identity Verification is on, disable the push subscription. + */ + if jwtConfig.isRequired == true { + user.pushSubscriptionModel._isDisabledInternally = true + } } @objc diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift index cb687938e..b4efa481d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Requests/OSRequestUpdateSubscription.swift @@ -65,17 +65,23 @@ class OSRequestUpdateSubscription: OneSignalRequest, OSUserRequest { var subscriptionParams = subscriptionObject subscriptionParams.removeValue(forKey: "address") subscriptionParams.removeValue(forKey: "notificationTypes") + subscriptionParams.removeValue(forKey: OSSubscriptionModel.Constants.isDisabledInternallyKey) subscriptionParams["token"] = subscriptionModel.address subscriptionParams["device_os"] = subscriptionModel.deviceOs subscriptionParams["sdk"] = subscriptionModel.sdk subscriptionParams["app_version"] = subscriptionModel.appVersion - // notificationTypes defaults to -1 instead of nil, don't send if it's -1 - if subscriptionModel.notificationTypes != -1 { - subscriptionParams["notification_types"] = subscriptionModel.notificationTypes + if subscriptionModel._isDisabledInternally { + subscriptionParams["enabled"] = false + subscriptionParams["notification_types"] = -2 + } else { + // notificationTypes defaults to -1 instead of nil, don't send if it's -1 + if subscriptionModel.notificationTypes != -1 { + subscriptionParams["notification_types"] = subscriptionModel.notificationTypes + } + subscriptionParams["enabled"] = subscriptionModel.enabled } - subscriptionParams["enabled"] = subscriptionModel.enabled // TODO: The above is not quite right. If we hydrate, we will over-write any pending updates // May use subscriptionObject, but enabled and notification_types should be sent together... From 5ad412fd235d08d1c0f3f9401572fa5306227f72 Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 09:16:26 -0700 Subject: [PATCH 52/56] Fire user observer on logout (JWT on) * Usually, on logout, the user observer will fire once the anonymous user is created to the backend and returns with an OSID. However, when Identity Verification is on, that will not happen, so fire the observer early with `nil` values to represent there is no user in the SDK currently. * Firing the observer will save the state and necessary to know when the user logs back in. This is used by the messaging controller to fetch IAM appropriately. On a new session, it will not fetch IAM if logged out, but as the user observer, it will fetch once a user logs in. --- .../Source/Modeling/OSIdentityModel.swift | 22 +------------------ .../Source/OneSignalUserManagerImpl.swift | 4 ++++ .../Source/Support/OSUserUtils.swift | 20 +++++++++++++++++ 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift index c8d3c62c0..dcbe778d7 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Modeling/OSIdentityModel.swift @@ -134,26 +134,6 @@ class OSIdentityModel: OSModel { let newExternalId = remoteAliases[OS_EXTERNAL_ID] internalAddAliases(remoteAliases) - fireUserStateChanged(newOnesignalId: newOnesignalId, newExternalId: newExternalId) - } - - /** - Fires the user observer if `onesignal_id` OR `external_id` has changed from the previous snapshot (previous hydration). - */ - private func fireUserStateChanged(newOnesignalId: String?, newExternalId: String?) { - let prevOnesignalId = OneSignalUserDefaults.initShared().getSavedString(forKey: OS_SNAPSHOT_ONESIGNAL_ID, defaultValue: nil) - let prevExternalId = OneSignalUserDefaults.initShared().getSavedString(forKey: OS_SNAPSHOT_EXTERNAL_ID, defaultValue: nil) - - guard prevOnesignalId != newOnesignalId || prevExternalId != newExternalId else { - return - } - - OneSignalUserDefaults.initShared().saveString(forKey: OS_SNAPSHOT_ONESIGNAL_ID, withValue: newOnesignalId) - OneSignalUserDefaults.initShared().saveString(forKey: OS_SNAPSHOT_EXTERNAL_ID, withValue: newExternalId) - - let curUserState = OSUserState(onesignalId: newOnesignalId, externalId: newExternalId) - let changedState = OSUserChangedState(current: curUserState) - - OneSignalUserManagerImpl.sharedInstance.userStateChangesObserver.notifyChange(changedState) + OSUserUtils.fireUserStateChanged(newOnesignalId: newOnesignalId, newExternalId: newExternalId) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index ef1c85486..639c3f04f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -447,9 +447,13 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { /* If Identity Verification is on, disable the push subscription. + Since the anonymous placeholder user will not be created to the backend, + fire the user observer here to represent "no user" in the SDK. + This is necessary so internal user observers can know when a user logs out and then back in. */ if jwtConfig.isRequired == true { user.pushSubscriptionModel._isDisabledInternally = true + OSUserUtils.fireUserStateChanged(newOnesignalId: nil, newExternalId: nil) } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift index 91e129e08..dc70751fc 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Support/OSUserUtils.swift @@ -71,4 +71,24 @@ class OSUserUtils { } return headers } + + /** + Fires the user observer if `onesignal_id` OR `external_id` has changed from the previous snapshot (previous hydration). + */ + static func fireUserStateChanged(newOnesignalId: String?, newExternalId: String?) { + let prevOnesignalId = OneSignalUserDefaults.initShared().getSavedString(forKey: OS_SNAPSHOT_ONESIGNAL_ID, defaultValue: nil) + let prevExternalId = OneSignalUserDefaults.initShared().getSavedString(forKey: OS_SNAPSHOT_EXTERNAL_ID, defaultValue: nil) + + guard prevOnesignalId != newOnesignalId || prevExternalId != newExternalId else { + return + } + + OneSignalUserDefaults.initShared().saveString(forKey: OS_SNAPSHOT_ONESIGNAL_ID, withValue: newOnesignalId) + OneSignalUserDefaults.initShared().saveString(forKey: OS_SNAPSHOT_EXTERNAL_ID, withValue: newExternalId) + + let curUserState = OSUserState(onesignalId: newOnesignalId, externalId: newExternalId) + let changedState = OSUserChangedState(current: curUserState) + + OneSignalUserManagerImpl.sharedInstance.userStateChangesObserver.notifyChange(changedState) + } } From 0d9c62d94c188d01dfcec906278269335829d12f Mon Sep 17 00:00:00 2001 From: Nan Date: Wed, 2 Oct 2024 22:41:59 -0700 Subject: [PATCH 53/56] [nit] move a method to extension for swiftlint --- .../Source/OneSignalUserManagerImpl.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 639c3f04f..065c7bc2f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -576,16 +576,6 @@ public class OneSignalUserManagerImpl: NSObject, OneSignalUserManager { } updatePropertiesDeltas(property: .purchases, value: purchases) } - - @objc - public func updateUserJwt(externalId: String, token: String) { - guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updateUserJwt") else { - return - } - OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_VERBOSE, message: "Update User JWT called with externalId: \(externalId) and token: \(token)") - - identityModelRepo.updateJwtToken(externalId: externalId, token: token) - } } // MARK: - Sessions @@ -678,6 +668,16 @@ extension OneSignalUserManagerImpl { jwtConfig.subscribe(listener, key: key) } + @objc + public func updateUserJwt(externalId: String, token: String) { + guard !OneSignalConfigManager.shouldAwaitAppIdAndLogMissingPrivacyConsent(forMethod: "updateUserJwt") else { + return + } + OneSignalLog.onesignalLog(ONE_S_LOG_LEVEL.LL_VERBOSE, message: "Update User JWT called with externalId: \(externalId) and token: \(token)") + + identityModelRepo.updateJwtToken(externalId: externalId, token: token) + } + @objc public func invalidateJwtForExternalId(externalId: String, error: NSError) { guard jwtConfig.isRequired == true else { From 1f60b081094b1de586f1ae77bbeedb8fa83524f6 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 4 Oct 2024 08:54:29 -0700 Subject: [PATCH 54/56] [nit] Clean up logging, remove hardcoded prints --- .../OneSignalDevApp/ViewController.m | 4 +-- .../Controller/OSMessagingController.m | 2 -- .../Source/Jwt/OSUserJwtConfig.swift | 8 ++--- .../Source/OSOperationRepo.swift | 18 +++++++----- .../OSIdentityOperationExecutor.swift | 17 +++++------ .../OSPropertyOperationExecutor.swift | 29 ++++++------------- .../OSSubscriptionOperationExecutor.swift | 19 +++++------- .../Source/Executors/OSUserExecutor.swift | 28 +++++++----------- .../Source/OSIdentityModelRepo.swift | 7 +++-- .../OneSignalUserManagerImpl+OSLoggable.swift | 29 ++++++++++--------- 10 files changed, 68 insertions(+), 93 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m index fcd3c591d..b63fdd4a4 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/ViewController.m @@ -197,14 +197,14 @@ - (IBAction)inAppMessagingSegmentedControlValueChanged:(UISegmentedControl *)sen - (IBAction)loginExternalUserId:(UIButton *)sender { NSString* externalUserId = self.externalUserIdTextField.text; NSString* token = self.tokenTextField.text; - NSLog(@"❌ Dev App: Logging in to external user ID %@ and token %@", externalUserId, token); + NSLog(@"Dev App: Logging in to external user ID %@ and token %@", externalUserId, token); [OneSignal login:externalUserId withToken:token]; } - (IBAction)updateJwt:(id)sender { NSString* externalUserId = self.externalUserIdTextField.text; NSString* token = self.tokenTextField.text; - NSLog(@"❌ Dev App: updating JWT for external user ID %@ and token %@", externalUserId, token); + NSLog(@"Dev App: updating JWT for external user ID %@ and token %@", externalUserId, token); [OneSignal updateUserJwt:externalUserId withToken:token]; } diff --git a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m index 8154b458e..cbc638354 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/OneSignalInAppMessages/Controller/OSMessagingController.m @@ -1154,7 +1154,6 @@ - (void)onPushSubscriptionDidChangeWithState:(OSPushSubscriptionChangedState * _ #pragma mark OSUserStateObserver Methods - (void)onUserStateDidChangeWithState:(OSUserChangedState * _Nonnull)state { - NSLog(@"❌ OSMessagingController onUserStateDidChangeWithState: %@", [state jsonRepresentation]); if (state.current.onesignalId && shouldRetryGetInAppMessagesOnUserChange) { shouldRetryGetInAppMessagesOnUserChange = false; [self getInAppMessagesFromServer]; @@ -1167,7 +1166,6 @@ - (void)onRequiresUserAuthChangedFrom:(enum OSRequiresUserAuth)from to:(enum OSR } - (void)onJwtUpdatedWithExternalId:(NSString *)externalId token:(NSString *)token { - NSLog(@"❌ OSMessagingController onJwtUpdatedWithExternalId: %@ token: %@", externalId, token); if (![token isEqual: OS_JWT_TOKEN_INVALID] && shouldRetryGetInAppMessagesOnJwtUpdated) { shouldRetryGetInAppMessagesOnJwtUpdated = false; [self getInAppMessagesFromServer]; diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift index 1a86ae435..5ebe2d7c5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/Jwt/OSUserJwtConfig.swift @@ -64,8 +64,7 @@ public class OSUserJwtConfig { return } - print("❌ OSUserJwtConfig.requiresUserAuth: changing from \(oldValue) to \(requiresUserAuth), firing \(changeNotifier)") - + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserJwtConfig.requiresUserAuth: changing from \(oldValue) to \(requiresUserAuth), firing listeners") // Persist new value OneSignalUserDefaults.initShared().saveInteger(forKey: OSUD_USE_IDENTITY_VERIFICATION, withValue: requiresUserAuth.rawValue) @@ -93,9 +92,6 @@ public class OSUserJwtConfig { public init() { let rawValue = OneSignalUserDefaults.initShared().getSavedInteger(forKey: OSUD_USE_IDENTITY_VERIFICATION, defaultValue: OSRequiresUserAuth.unknown.rawValue) - - print("❌ OSUserJwtConfig init(): \(String(describing: OSRequiresUserAuth(rawValue: rawValue))))") - requiresUserAuth = OSRequiresUserAuth(rawValue: rawValue) ?? OSRequiresUserAuth.unknown } @@ -104,7 +100,7 @@ public class OSUserJwtConfig { } public func onJwtTokenChanged(externalId: String, token: String?) { - print("❌ OSUserJwtConfig.onJwtTokenChanged \(externalId): \(token)") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserJwtConfig.onJwtTokenChanged for \(externalId) with token \(token ?? "nil"), firing listeners") changeNotifier.fire { listener in listener.onJwtUpdated(externalId: externalId, token: token) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift index 963945e75..c95561288 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalOSCore/Source/OSOperationRepo.swift @@ -54,8 +54,6 @@ public class OSOperationRepo { public init(jwtConfig: OSUserJwtConfig) { self.jwtConfig = jwtConfig self.jwtConfig.subscribe(self, key: OS_OPERATION_REPO) - print("❌ OSOperationRepo init(\(String(describing: jwtConfig.isRequired))) called") - // Read the Deltas from cache, if any... guard let deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_OPERATION_REPO_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSOperationRepo is unable to uncache the OSDelta queue.") @@ -73,7 +71,7 @@ public class OSOperationRepo { } guard jwtConfig.isRequired != nil else { - print("❌ OSOperationRepo.start() returning early due to unknown Identity Verification status.") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSOperationRepo.start() returning early due to unknown Identity Verification status") return } @@ -185,7 +183,6 @@ public class OSOperationRepo { extension OSOperationRepo: OSUserJwtConfigListener { public func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { - print("❌ OSOperationRepo onRequiresUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process deltas if to == .on { removeInvalidDeltas() @@ -194,7 +191,7 @@ extension OSOperationRepo: OSUserJwtConfigListener { } public func onJwtUpdated(externalId: String, token: String?) { - print("❌ OSOperationRepo onJwtUpdated for \(externalId) to \(String(describing: token))") + // Not used for now } /** @@ -203,14 +200,19 @@ extension OSOperationRepo: OSUserJwtConfigListener { Executors will handle this. */ func removeInvalidDeltas() { - print("❌ OSOperationRepo removeInvalidDeltas TODO!") + // Not used for now } } extension OSOperationRepo: OSLoggable { public func logSelf() { - print("💛 Operation Repo: deltaQueue: \(self.deltaQueue )") - print("💛 Operation Repo: executors that are subscribed:") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: + """ + Operation Repo: deltaQueue: \(self.deltaQueue) + + Operation Repo: executors that are subscribed: + """ + ) for executor in self.executors { executor.logSelf() } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift index 090490135..f57ea8a1a 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSIdentityOperationExecutor.swift @@ -45,7 +45,6 @@ class OSIdentityOperationExecutor: OSOperationExecutor { self.newRecordsState = newRecordsState self.jwtConfig = jwtConfig self.jwtConfig.subscribe(self, key: OS_IDENTITY_EXECUTOR) - print("❌ OSIdentityOperationExecutor init(\(jwtConfig.isRequired))") // Read unfinished deltas and requests from cache, if any... uncacheDeltas() uncacheRequests() @@ -206,7 +205,7 @@ class OSIdentityOperationExecutor: OSOperationExecutor { // If JWT is on but the external ID does not exist, drop this Delta if self.jwtConfig.isRequired == true, model.externalId == nil { - print("❌ \(delta) is Invalid with JWT, being dropped") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSIdentityOperationExecutor.processDeltaQueue dropped \(delta)") } switch delta.name { @@ -401,7 +400,6 @@ class OSIdentityOperationExecutor: OSOperationExecutor { extension OSIdentityOperationExecutor: OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { - print("❌ OSIdentityOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests if to == .on { removeInvalidDeltasAndRequests() @@ -409,7 +407,6 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { } func onJwtUpdated(externalId: String, token: String?) { - print("❌ OSIdentityOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } @@ -435,10 +432,9 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { private func removeInvalidDeltasAndRequests() { self.dispatchQueue.async { - print("❌ OSIdentityOperationExecutor.removeInvalidDeltasAndRequests called") for (index, delta) in self.deltaQueue.enumerated().reversed() { if (delta.model as? OSIdentityModel)?.externalId == nil { - print(" \(delta) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSIdentityOperationExecutor.removeInvalidDeltasAndRequests dropped \(delta)") self.deltaQueue.remove(at: index) } } @@ -446,7 +442,7 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { for (index, request) in self.addRequestQueue.enumerated().reversed() { if request.identityModel.externalId == nil { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSIdentityOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)") self.addRequestQueue.remove(at: index) } } @@ -454,7 +450,7 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { for (index, request) in self.removeRequestQueue.enumerated().reversed() { if request.identityModel.externalId == nil { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSIdentityOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)") self.removeRequestQueue.remove(at: index) } } @@ -465,13 +461,14 @@ extension OSIdentityOperationExecutor: OSUserJwtConfigListener { extension OSIdentityOperationExecutor: OSLoggable { func logSelf() { - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 OSIdentityOperationExecutor has the following queues: + OSIdentityOperationExecutor has the following queues: addRequestQueue: \(self.addRequestQueue) removeRequestQueue: \(self.removeRequestQueue) deltaQueue: \(self.deltaQueue) pendingAuthRequests: \(self.pendingAuthRequests) + """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift index ff50d921a..b2e2bf25b 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSPropertyOperationExecutor.swift @@ -75,8 +75,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { self.newRecordsState = newRecordsState self.jwtConfig = jwtConfig self.jwtConfig.subscribe(self, key: OS_PROPERTIES_EXECUTOR) - print("❌ OSPropertyOperationExecutor init(\(String(describing: jwtConfig.isRequired)))") - // Read unfinished deltas and requests from cache, if any... // Note that we should only have deltas for the current user as old ones are flushed.. uncacheDeltas() @@ -84,8 +82,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } private func uncacheDeltas() { - print("❌ OSPropertyOperationExecutor uncacheDeltas called") - if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] { for (index, delta) in deltaQueue.enumerated().reversed() { guard let model = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId) else { @@ -97,7 +93,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { // If JWT is on but the external ID does not exist, drop this Delta if jwtConfig.isRequired == true, model.externalId == nil { - print("❌ removing \(delta)") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSPropertyOperationExecutor.uncacheDeltas dropped \(delta)") deltaQueue.remove(at: index) } } @@ -106,7 +102,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } else { OneSignalLog.onesignalLog(.LL_ERROR, message: "OSPropertyOperationExecutor error encountered reading from cache for \(OS_PROPERTIES_EXECUTOR_DELTA_QUEUE_KEY)") } - print("❌ OSPropertyOperationExecutor uncacheDeltas done, \(self.deltaQueue)") } private func uncacheUpdateRequests() { @@ -117,15 +112,13 @@ class OSPropertyOperationExecutor: OSOperationExecutor { } if let pendingRequests = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_PROPERTIES_EXECUTOR_PENDING_QUEUE_KEY, defaultValue: [:]) as? [String: [OSRequestUpdateProperties]] { - print("❌ prop executor uncached pending \(pendingRequests)") - for requests in pendingRequests.values { for request in requests { updateRequestQueue.append(request) } } } - print("❌ prop executor uncached requests \(updateRequestQueue)") + // Hook each uncached Request to the model in the store for (index, request) in updateRequestQueue.enumerated().reversed() { if jwtConfig.isRequired == true, @@ -172,7 +165,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { */ func processDeltaQueue(inBackground: Bool) { guard jwtConfig.isRequired != nil else { - print("❌ OSPropertyOperationExecutor processDeltaQueue returning early due to requiresAuth: \(String(describing: jwtConfig.isRequired))") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSPropertyOperationExecutor processDeltaQueue returning early due to requiresAuth: \(String(describing: jwtConfig.isRequired))") return } @@ -198,7 +191,7 @@ class OSPropertyOperationExecutor: OSOperationExecutor { // If JWT is on but the external ID does not exist, drop this Delta if self.jwtConfig.isRequired == true, identityModel.externalId == nil { - print("❌ \(delta) is Invalid with JWT, being dropped") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSPropertyOperationExecutor.processDeltaQueue dropped \(delta)") } let combinedSoFar: OSCombinedProperties? = combinedProperties[identityModel.modelId] @@ -317,7 +310,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { return } - print("ECM executing properties request: %@", request.identityModel.externalId) request.sentToClient = true let backgroundTaskIdentifier = PROPERTIES_EXECUTOR_BACKGROUND_TASK + UUID().uuidString @@ -375,7 +367,6 @@ class OSPropertyOperationExecutor: OSOperationExecutor { extension OSPropertyOperationExecutor: OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { - print("❌ OSPropertyOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests if to == .on { removeInvalidDeltasAndRequests() @@ -383,7 +374,6 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { } func onJwtUpdated(externalId: String, token: String?) { - print("❌ OSPropertyOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } @@ -404,13 +394,11 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { private func removeInvalidDeltasAndRequests() { self.dispatchQueue.async { - print("❌ OSPropertyOperationExecutor.removeInvalidDeltasAndRequests called") - for (index, delta) in self.deltaQueue.enumerated().reversed() { if let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId), identityModel.externalId == nil { - print(" \(delta) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSPropertyOperationExecutor.removeInvalidDeltasAndRequests dropped \(delta)") self.deltaQueue.remove(at: index) } } @@ -418,7 +406,7 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { for (index, request) in self.updateRequestQueue.enumerated().reversed() { if request.identityModel.externalId == nil { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSPropertyOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)") self.updateRequestQueue.remove(at: index) } } @@ -429,12 +417,13 @@ extension OSPropertyOperationExecutor: OSUserJwtConfigListener { extension OSPropertyOperationExecutor: OSLoggable { func logSelf() { - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 OSPropertyOperationExecutor has the following queues: + OSPropertyOperationExecutor has the following queues: updateRequestQueue: \(self.updateRequestQueue) deltaQueue: \(self.deltaQueue) pendingAuthRequests: \(self.pendingAuthRequests) + """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift index 74ca5528f..e98ab7e4f 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSSubscriptionOperationExecutor.swift @@ -257,7 +257,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { // If JWT is on but the external ID does not exist, drop this Delta if self.jwtConfig.isRequired == true, identityModel.externalId == nil { - print("❌ \(delta) is Invalid with JWT, being dropped") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.processDeltaQueue dropped \(delta)") } let request = OSRequestCreateSubscription( @@ -275,7 +275,7 @@ class OSSubscriptionOperationExecutor: OSOperationExecutor { // If JWT is on but the external ID does not exist, drop this Delta if self.jwtConfig.isRequired == true, identityModel.externalId == nil { - print("❌ \(delta) is Invalid with JWT, being dropped") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.processDeltaQueue dropped \(delta)") } let request = OSRequestDeleteSubscription( @@ -514,14 +514,12 @@ extension OSSubscriptionOperationExecutor { extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OneSignalOSCore.OSRequiresUserAuth, to: OneSignalOSCore.OSRequiresUserAuth) { - print("❌ OSSubscriptionOperationExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") if to == .on { removeInvalidDeltasAndRequests() } } func onJwtUpdated(externalId: String, token: String?) { - print("❌ OSSubscriptionOperationExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") reQueuePendingRequestsForExternalId(externalId: externalId) } @@ -575,14 +573,12 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { */ private func removeInvalidDeltasAndRequests() { self.dispatchQueue.async { - print("❌ OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests called") - for (index, delta) in self.deltaQueue.enumerated().reversed() { if delta.name != OS_UPDATE_SUBSCRIPTION_DELTA, let identityModel = OneSignalUserManagerImpl.sharedInstance.getIdentityModel(delta.identityModelId), identityModel.externalId == nil { - print(" \(delta) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests dropped \(delta)") self.deltaQueue.remove(at: index) } } @@ -590,7 +586,7 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { for (index, request) in self.addRequestQueue.enumerated().reversed() { if request.identityModel.externalId == nil { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)") self.addRequestQueue.remove(at: index) } } @@ -598,7 +594,7 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { for (index, request) in self.removeRequestQueue.enumerated().reversed() { if request.identityModel.externalId == nil { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSSubscriptionOperationExecutor.removeInvalidDeltasAndRequests dropped \(request)") self.removeRequestQueue.remove(at: index) } } @@ -609,14 +605,15 @@ extension OSSubscriptionOperationExecutor: OSUserJwtConfigListener { extension OSSubscriptionOperationExecutor: OSLoggable { func logSelf() { - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 OSSubscriptionOperationExecutor has the following queues: + OSSubscriptionOperationExecutor has the following queues: addRequestQueue: \(self.addRequestQueue) removeRequestQueue: \(self.removeRequestQueue) updateRequestQueue: \(self.updateRequestQueue) deltaQueue: \(self.deltaQueue) pendingAuthRequests: \(self.pendingAuthRequests) + """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift index 82515739b..19cc066ff 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/Executors/OSUserExecutor.swift @@ -49,8 +49,6 @@ class OSUserExecutor { self.newRecordsState = newRecordsState self.jwtConfig = jwtConfig self.jwtConfig.subscribe(self, key: OS_USER_EXECUTOR) - print("❌ OSUserExecutor init requiresAuth: \(jwtConfig.isRequired)") - uncacheUserRequests() migrateTransferSubscriptionRequests() executePendingRequests() @@ -60,7 +58,7 @@ class OSUserExecutor { private func uncacheUserRequests() { var userRequestQueue: [OSUserRequest] = [] var cachedRequestQueue: [OSUserRequest] = [] - print(" OSUserExecutor uncacheUserRequests called") + // Read unfinished Create User + Identify User + Get Identity By Subscription requests from cache, if any... if let cache = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, defaultValue: []) as? [OSUserRequest] { cachedRequestQueue = cache @@ -79,7 +77,7 @@ class OSUserExecutor { if request.isKind(of: OSRequestFetchIdentityBySubscription.self), let req = request as? OSRequestFetchIdentityBySubscription { // Remove this request if JWT is enabled guard jwtConfig.isRequired != true else { - print(" uncacheUserRequests dropping request \(req)") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSUserExecutor.uncacheUserRequests dropped \(req)") continue } if let identityModel = getIdentityModel(req.identityModel.modelId) { @@ -98,7 +96,7 @@ class OSUserExecutor { req.identityModel.externalId == nil { // Remove this request if there is no EUID - print(" uncacheUserRequests dropping request \(req)") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSUserExecutor.uncacheUserRequests dropped \(req)") continue } @@ -115,7 +113,6 @@ class OSUserExecutor { // If JWT is enabled, we migrate this request into a Create User request guard jwtConfig.isRequired != true else { - print(" uncacheUserRequests converting \(req) to createUser") convertIdentifyUserToCreateUser(req) continue } @@ -146,7 +143,6 @@ class OSUserExecutor { } self.userRequestQueue = userRequestQueue OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) - print(" OSUserExecutor uncacheUserRequests done, now has queue: \(self.userRequestQueue)") } /** @@ -166,6 +162,7 @@ class OSUserExecutor { } private func convertIdentifyUserToCreateUser(_ request: OSRequestIdentifyUser) { + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSUserExecutor.convertIdentifyUserToCreateUser for \(request)") if OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.aliasId) { self.createUser(OneSignalUserManagerImpl.sharedInstance.user) } else { @@ -239,7 +236,7 @@ class OSUserExecutor { */ func executePendingRequests(withDelay: Bool = false) { guard jwtConfig.isRequired != nil else { - print("❌ OSUserExecutor.executePendingRequests returning early due to unknown Identity Verification status.") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSUserExecutor.executePendingRequests returning early due to unknown Identity Verification status") return } @@ -766,7 +763,6 @@ extension OSUserExecutor { extension OSUserExecutor: OSUserJwtConfigListener { func onRequiresUserAuthChanged(from: OSRequiresUserAuth, to: OSRequiresUserAuth) { - print("❌ OSUserExecutor onUserAuthChanged from \(String(describing: from)) to \(String(describing: to))") // If auth changed from false or unknown to true, process requests if to == .on { removeInvalidRequests() @@ -776,7 +772,6 @@ extension OSUserExecutor: OSUserJwtConfigListener { func onJwtUpdated(externalId: String, token: String?) { reQueuePendingRequestsForExternalId(externalId: externalId) - print("❌ OSUserExecutor onJwtUpdated for \(externalId) to \(String(describing: token))") } private func reQueuePendingRequestsForExternalId(externalId: String) { @@ -796,24 +791,20 @@ extension OSUserExecutor: OSUserJwtConfigListener { private func removeInvalidRequests() { self.dispatchQueue.async { - print("❌ OSUserExecutor.removeInvalidRequests called") - for request in self.userRequestQueue { guard self.isRequestValidWithAuth(request) else { - print(" \(request) is Invalid, being removed") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: OSUserExecutor.removeInvalidRequests dropped \(request)") self.userRequestQueue.removeAll(where: { $0 == request}) continue } if request.isKind(of: OSRequestIdentifyUser.self), let req = request as? OSRequestIdentifyUser { - print(" \(request) is IdentifyUser, being converted") + OneSignalLog.onesignalLog(.LL_DEBUG, message: "Invalid with JWT: \(request) is IdentifyUser, being converted") self.userRequestQueue.removeAll(where: { $0 == request}) self.convertIdentifyUserToCreateUser(req) } } - OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_USER_EXECUTOR_USER_REQUEST_QUEUE_KEY, withValue: self.userRequestQueue) - print(" OSUserExecutor.removeInvalidRequests done, \(self.userRequestQueue)") } } @@ -839,11 +830,12 @@ extension OSUserExecutor: OSUserJwtConfigListener { extension OSUserExecutor: OSLoggable { func logSelf() { - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 OSUserExecutor has the following queues: + OSUserExecutor has the following queues: userRequestQueue: \(self.userRequestQueue) pendingAuthRequests: \(self.pendingAuthRequests) + """ ) } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift index 66c0ec10c..ef82e264e 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OSIdentityModelRepo.swift @@ -102,16 +102,17 @@ extension OSIdentityModelRepo: OSModelChangedHandler { else { return } - print("❌ OSIdentityModelRepo onModelUpdated for \(externalId): \(token)") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityModelRepo onModelUpdated for \(externalId) with token \(token)") OneSignalUserManagerImpl.sharedInstance.jwtConfig.onJwtTokenChanged(externalId: externalId, token: token) } } extension OSIdentityModelRepo: OSLoggable { func logSelf() { - print("🥭 OSIdentityModelRepo has the following models: ") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityModelRepo has the following models:") + for model in models.values { - print(" modelID: \(model.modelId), alises: \(model.aliases) token: \(model.jwtBearerToken ?? "nil")") + OneSignalLog.onesignalLog(.LL_VERBOSE, message: " modelID: \(model.modelId), alises: \(model.aliases) token: \(model.jwtBearerToken ?? "nil")") } } } diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift index 90f684a8c..63acbd27d 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl+OSLoggable.swift @@ -30,50 +30,53 @@ import OneSignalOSCore extension OneSignalUserManagerImpl: OSLoggable { @objc public func logSelf() { - print("💛 _user: \(String(describing: _user))") - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 identityModel: + OneSignalUserManagerImpl: + _user: \(String(describing: _user)) + + identityModel: aliases: \(String(describing: _user?.identityModel.aliases)) jwt: \(String(describing: _user?.identityModel.jwtBearerToken)) modelId: \(String(describing: _user?.identityModel.modelId)) - """ - ) - print( - """ - 💛 propertiesModel: + + propertiesModel: tags: \(String(describing: _user?.propertiesModel.tags)) language: \(String(describing: _user?.propertiesModel.language)) modelId: \(String(describing: _user?.propertiesModel.modelId)) + """ ) + let subscriptionModels = subscriptionModelStore.getModels().values for sub in subscriptionModels { - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 subscription model from store + subscription model from store addess: \(String(describing: sub.address)) subscriptionId: \(String(describing: sub.subscriptionId)) enabled: \(sub.enabled) modelId: \(sub.modelId) + """ ) } + let pushSubModel = pushSubscriptionModelStore.getModel(key: OS_PUSH_SUBSCRIPTION_MODEL_KEY) - print( + OneSignalLog.onesignalLog(.LL_VERBOSE, message: """ - 💛 push sub model from store + push sub model from store token: \(String(describing: pushSubModel?.address)) subscriptionId: \(String(describing: pushSubModel?.subscriptionId)) enabled: \(String(describing: pushSubModel?.enabled)) notification_types: \(String(describing: pushSubModel?.notificationTypes)) optedIn: \(String(describing: pushSubModel?.optedIn)) modelId: \(String(describing: pushSubModel?.modelId)) + """ ) operationRepo.logSelf() userExecutor?.logSelf() identityModelRepo.logSelf() - print("") } } From 0a81b7a82fd35c3a96ff9594f1da8599f0de1964 Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 4 Oct 2024 12:58:12 -0700 Subject: [PATCH 55/56] Revert back to production servers * Revert back to prod servers * Add app clips back --- .../OneSignalDevApp/AppDelegate.m | 4 +- .../project.pbxproj | 227 +++++++++++++++++- .../Source/OneSignalCommonDefines.h | 4 +- .../Source/OneSignalMobileProvision.m | 2 +- 4 files changed, 226 insertions(+), 11 deletions(-) diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index cc0b86083..b14f5a82e 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -91,8 +91,8 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -#define ONESIGNAL_APP_ID_DEFAULT @"0139bd6f-451f-438c-8886-4e0f0fe3a085" -#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"0139bd6f-451f-438c-8886-4e0f0fe3a085" +#define ONESIGNAL_APP_ID_DEFAULT @"77e32082-ea27-42e3-a898-c72e141824ef" +#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"77e32082-ea27-42e3-a898-c72e141824ef" + (NSString*)getOneSignalAppId { NSString* userDefinedAppId = [[NSUserDefaults standardUserDefaults] objectForKey:ONESIGNAL_APP_ID_KEY_FOR_TESTING]; diff --git a/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj index 52c8cceae..556c0c10e 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalDevApp/OneSignalExample.xcodeproj/project.pbxproj @@ -47,6 +47,32 @@ DE12F3F8289B2B7F002F63AA /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE12F3F6289B2B7F002F63AA /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DE61E483294810B900CD12F1 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E482294810B900CD12F1 /* OneSignalFramework.framework */; }; DE61E484294810B900CD12F1 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E482294810B900CD12F1 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E4852948117000CD12F1 /* OneSignalExampleClip.app in Embed App Clips */ = {isa = PBXBuildFile; fileRef = DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + DE61E48A2948117A00CD12F1 /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4892948117A00CD12F1 /* OneSignalCore.framework */; }; + DE61E48B2948117A00CD12F1 /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4892948117A00CD12F1 /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E48E2948117D00CD12F1 /* OneSignalExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E48D2948117D00CD12F1 /* OneSignalExtension.framework */; }; + DE61E48F2948117D00CD12F1 /* OneSignalExtension.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E48D2948117D00CD12F1 /* OneSignalExtension.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E4912948118200CD12F1 /* OneSignalFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4902948118200CD12F1 /* OneSignalFramework.framework */; }; + DE61E4922948118200CD12F1 /* OneSignalFramework.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4902948118200CD12F1 /* OneSignalFramework.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E4942948118500CD12F1 /* OneSignalNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4932948118500CD12F1 /* OneSignalNotifications.framework */; }; + DE61E4952948118500CD12F1 /* OneSignalNotifications.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4932948118500CD12F1 /* OneSignalNotifications.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E4972948118900CD12F1 /* OneSignalOSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */; }; + DE61E4982948118900CD12F1 /* OneSignalOSCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E49A2948118C00CD12F1 /* OneSignalOutcomes.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */; }; + DE61E49B2948118D00CD12F1 /* OneSignalOutcomes.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE61E49D2948119100CD12F1 /* OneSignalUser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E49C2948119100CD12F1 /* OneSignalUser.framework */; }; + DE61E49E2948119100CD12F1 /* OneSignalUser.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE61E49C2948119100CD12F1 /* OneSignalUser.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + DE68DA5B24C7695900FC95A8 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA5A24C7695900FC95A8 /* AppDelegate.m */; }; + DE68DA5E24C7695900FC95A8 /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA5D24C7695900FC95A8 /* SceneDelegate.m */; }; + DE68DA6124C7695900FC95A8 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA6024C7695900FC95A8 /* ViewController.m */; }; + DE68DA6424C7695900FC95A8 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6224C7695900FC95A8 /* Main.storyboard */; }; + DE68DA6624C7695A00FC95A8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6524C7695A00FC95A8 /* Assets.xcassets */; }; + DE68DA6924C7695A00FC95A8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DE68DA6724C7695A00FC95A8 /* LaunchScreen.storyboard */; }; + DE68DA6C24C7695A00FC95A8 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = DE68DA6B24C7695A00FC95A8 /* main.m */; }; + DE68DA7724C769F200FC95A8 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 03432CDB1EBD426A0071FC48 /* CoreLocation.framework */; }; + DE68DA7824C769F900FC95A8 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9112E8A61E724EE00022A1CB /* SystemConfiguration.framework */; }; + DE68DA7924C76A0300FC95A8 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 91B6EA051E83215000B5CF01 /* UserNotifications.framework */; }; + DE68DA7A24C76A1800FC95A8 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CACBAAB5218A7136000ACAA5 /* WebKit.framework */; }; DE7D180727026BB5002D3A5D /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; }; DE7D180827026BB5002D3A5D /* OneSignalCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; DE7D180927026BC5002D3A5D /* OneSignalCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = DE7D180627026BB5002D3A5D /* OneSignalCore.framework */; }; @@ -81,6 +107,13 @@ remoteGlobalIDString = 945C59DB296CF2A00097041D; remoteInfo = OneSignalWidgetExtensionExtension; }; + DE61E4862948117000CD12F1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9112E87A1E724C320022A1CB /* Project object */; + proxyType = 1; + remoteGlobalIDString = DE68DA5624C7695900FC95A8; + remoteInfo = OneSignalExampleClip; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -102,10 +135,28 @@ dstPath = "$(CONTENTS_FOLDER_PATH)/AppClips"; dstSubfolderSpec = 16; files = ( + DE61E4852948117000CD12F1 /* OneSignalExampleClip.app in Embed App Clips */, ); name = "Embed App Clips"; runOnlyForDeploymentPostprocessing = 0; }; + DE61E48C2948117A00CD12F1 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + DE61E4952948118500CD12F1 /* OneSignalNotifications.framework in Embed Frameworks */, + DE61E49E2948119100CD12F1 /* OneSignalUser.framework in Embed Frameworks */, + DE61E4922948118200CD12F1 /* OneSignalFramework.framework in Embed Frameworks */, + DE61E48B2948117A00CD12F1 /* OneSignalCore.framework in Embed Frameworks */, + DE61E48F2948117D00CD12F1 /* OneSignalExtension.framework in Embed Frameworks */, + DE61E49B2948118D00CD12F1 /* OneSignalOutcomes.framework in Embed Frameworks */, + DE61E4982948118900CD12F1 /* OneSignalOSCore.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; DEA226FA261F74060092FF58 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -181,6 +232,7 @@ DE61E4962948118900CD12F1 /* OneSignalOSCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalOSCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE61E4992948118C00CD12F1 /* OneSignalOutcomes.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalOutcomes.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DE61E49C2948119100CD12F1 /* OneSignalUser.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OneSignalUser.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OneSignalExampleClip.app; sourceTree = BUILT_PRODUCTS_DIR; }; DE68DA5924C7695900FC95A8 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; DE68DA5A24C7695900FC95A8 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; DE68DA5C24C7695900FC95A8 /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; @@ -257,6 +309,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE68DA5424C7695900FC95A8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DE61E48A2948117A00CD12F1 /* OneSignalCore.framework in Frameworks */, + DE61E4912948118200CD12F1 /* OneSignalFramework.framework in Frameworks */, + DE61E4972948118900CD12F1 /* OneSignalOSCore.framework in Frameworks */, + DE61E48E2948117D00CD12F1 /* OneSignalExtension.framework in Frameworks */, + DE61E49A2948118C00CD12F1 /* OneSignalOutcomes.framework in Frameworks */, + DE61E49D2948119100CD12F1 /* OneSignalUser.framework in Frameworks */, + DE68DA7A24C76A1800FC95A8 /* WebKit.framework in Frameworks */, + DE61E4942948118500CD12F1 /* OneSignalNotifications.framework in Frameworks */, + DE68DA7924C76A0300FC95A8 /* UserNotifications.framework in Frameworks */, + DE68DA7724C769F200FC95A8 /* CoreLocation.framework in Frameworks */, + DE68DA7824C769F900FC95A8 /* SystemConfiguration.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -279,6 +349,7 @@ children = ( 9112E8821E724C320022A1CB /* OneSignalExample.app */, 9150E7721E73BEDC00C5D46A /* OneSignalNotificationServiceExtension.appex */, + DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */, 945C59DC296CF2A00097041D /* OneSignalWidgetExtensionExtension.appex */, ); name = Products; @@ -420,6 +491,7 @@ ); dependencies = ( 9150E7791E73BEDD00C5D46A /* PBXTargetDependency */, + DE61E4872948117000CD12F1 /* PBXTargetDependency */, 945C59EF296CF2A10097041D /* PBXTargetDependency */, ); name = OneSignalExample; @@ -465,6 +537,26 @@ productReference = 945C59DC296CF2A00097041D /* OneSignalWidgetExtensionExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */ = { + isa = PBXNativeTarget; + buildConfigurationList = DE68DA7424C7695A00FC95A8 /* Build configuration list for PBXNativeTarget "OneSignalExampleClip" */; + buildPhases = ( + DE68DA5324C7695900FC95A8 /* Sources */, + DE68DA5424C7695900FC95A8 /* Frameworks */, + DE68DA5524C7695900FC95A8 /* Resources */, + DE61E48C2948117A00CD12F1 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = OneSignalExampleClip; + packageProductDependencies = ( + ); + productName = OneSignalDevAppClip; + productReference = DE68DA5724C7695900FC95A8 /* OneSignalExampleClip.app */; + productType = "com.apple.product-type.application.on-demand-install-capable"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -477,6 +569,7 @@ TargetAttributes = { 9112E8811E724C320022A1CB = { CreatedOnToolsVersion = 8.2.1; + DevelopmentTeam = 99SW8E36CT; LastSwiftMigration = 1320; ProvisioningStyle = Automatic; SystemCapabilities = { @@ -504,6 +597,9 @@ 945C59DB296CF2A00097041D = { CreatedOnToolsVersion = 14.1; }; + DE68DA5624C7695900FC95A8 = { + CreatedOnToolsVersion = 12.0; + }; }; }; buildConfigurationList = 9112E87D1E724C320022A1CB /* Build configuration list for PBXProject "OneSignalExample" */; @@ -524,6 +620,7 @@ targets = ( 9112E8811E724C320022A1CB /* OneSignalExample */, 9150E7711E73BEDC00C5D46A /* OneSignalNotificationServiceExtension */, + DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */, 945C59DB296CF2A00097041D /* OneSignalWidgetExtensionExtension */, ); }; @@ -559,6 +656,16 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE68DA5524C7695900FC95A8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE68DA6924C7695A00FC95A8 /* LaunchScreen.storyboard in Resources */, + DE68DA6624C7695A00FC95A8 /* Assets.xcassets in Resources */, + DE68DA6424C7695900FC95A8 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -596,6 +703,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + DE68DA5324C7695900FC95A8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DE68DA6124C7695900FC95A8 /* ViewController.m in Sources */, + DE68DA5B24C7695900FC95A8 /* AppDelegate.m in Sources */, + DE68DA6C24C7695A00FC95A8 /* main.m in Sources */, + DE68DA5E24C7695900FC95A8 /* SceneDelegate.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -610,6 +728,12 @@ target = 945C59DB296CF2A00097041D /* OneSignalWidgetExtensionExtension */; targetProxy = 945C59EE296CF2A10097041D /* PBXContainerItemProxy */; }; + DE61E4872948117000CD12F1 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = DE68DA5624C7695900FC95A8 /* OneSignalExampleClip */; + targetProxy = DE61E4862948117000CD12F1 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -763,7 +887,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example; PRODUCT_NAME = OneSignalExample; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "OneSignalDevApp/OneSignalExample-Bridging-Header.h"; @@ -795,7 +919,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example; PRODUCT_NAME = OneSignalExample; SUPPORTS_MACCATALYST = YES; SWIFT_OBJC_BRIDGING_HEADER = "OneSignalDevApp/OneSignalExample-Bridging-Header.h"; @@ -823,7 +947,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalNotificationServiceExtensionA; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -851,7 +975,7 @@ ); MARKETING_VERSION = 1.4.4; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalNotificationServiceExtensionA; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalNotificationServiceExtensionA; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -894,7 +1018,7 @@ MARKETING_VERSION = 1.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -941,7 +1065,7 @@ ); MARKETING_VERSION = 1.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.staging.OneSignalWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.OneSignalWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SUPPORTS_MACCATALYST = YES; @@ -953,6 +1077,88 @@ }; name = Release; }; + DE68DA7224C7695A00FC95A8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalDevAppClip/OneSignalDevAppClip.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1.4.4; + DEVELOPMENT_TEAM = 99SW8E36CT; + "DYLIB_INSTALL_NAME_BASE[arch=*]" = "@rpath"; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + OS_APP_CLIP, + ); + INFOPLIST_FILE = OneSignalDevAppClip/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.4.4; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.Clip; + PRODUCT_NAME = OneSignalExampleClip; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DE68DA7324C7695A00FC95A8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = OneSignalDevAppClip/OneSignalDevAppClip.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1.4.4; + DEVELOPMENT_TEAM = 99SW8E36CT; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_PREPROCESSOR_DEFINITIONS = OS_APP_CLIP; + INFOPLIST_FILE = OneSignalDevAppClip/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.4.4; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.onesignal.example.Clip; + PRODUCT_NAME = OneSignalExampleClip; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -992,6 +1198,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + DE68DA7424C7695A00FC95A8 /* Build configuration list for PBXNativeTarget "OneSignalExampleClip" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DE68DA7224C7695A00FC95A8 /* Debug */, + DE68DA7324C7695A00FC95A8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 9112E87A1E724C320022A1CB /* Project object */; diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h index fc845fa26..f953e6c10 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalCommonDefines.h @@ -33,8 +33,8 @@ // Networking #define OS_API_VERSION @"1" #define OS_API_ACCEPT_HEADER @"application/vnd.onesignal.v" OS_API_VERSION @"+json" -#define OS_API_SERVER_URL @"https://api.staging.onesignal.com/" -#define OS_IAM_WEBVIEW_BASE_URL @"https://staging.onesignal.com/" +#define OS_API_SERVER_URL @"https://api.onesignal.com/" +#define OS_IAM_WEBVIEW_BASE_URL @"https://onesignal.com/" // OneSignalUserDefault keys // String values start with "OSUD_" to maintain a level of uniqueness from other libs and app code diff --git a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m index 450b5cfca..61f1aae9c 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m +++ b/iOS_SDK/OneSignalSDK/OneSignalCore/Source/OneSignalMobileProvision.m @@ -87,7 +87,7 @@ + (OSUIApplicationReleaseMode) releaseMode { NSDictionary *entitlements = nil; NSDictionary *provision = [self getProvision]; if (provision) { - // [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"provision: %@", provision]]; + [OneSignalLog onesignalLog:ONE_S_LL_DEBUG message:[NSString stringWithFormat:@"provision: %@", provision]]; entitlements = [provision objectForKey:@"Entitlements"]; } else From 57c6f086688c205f6b973ab0606a4187eed5f76c Mon Sep 17 00:00:00 2001 From: Nan Date: Fri, 4 Oct 2024 17:29:32 -0700 Subject: [PATCH 56/56] Handle when jwt_required is not returned --- .../Source/OneSignalUserManagerImpl.swift | 14 ++++++++++++++ iOS_SDK/OneSignalSDK/Source/OneSignal.m | 3 +++ 2 files changed, 17 insertions(+) diff --git a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift index 065c7bc2f..378fbbfa5 100644 --- a/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift +++ b/iOS_SDK/OneSignalSDK/OneSignalUser/Source/OneSignalUserManagerImpl.swift @@ -663,6 +663,20 @@ extension OneSignalUserManagerImpl { jwtConfig.isRequired = required } + /** + This is called when remote params does not return the property `IOS_JWT_REQUIRED`. + It is likely this feature is not enabled for the app, so we will assume it is off. + However, don't overwrite the value if this has already been set. + */ + @objc + public func remoteParamsReturnedUnknownRequiresUserAuth() { + guard jwtConfig.isRequired == nil else { + return + } + OneSignalLog.onesignalLog(.LL_DEBUG, message: "remoteParamsReturnedUnknownRequiresUserAuth called") + jwtConfig.isRequired = false + } + @objc public func subscribeToJwtConfig(_ listener: OSUserJwtConfigListener, key: String) { jwtConfig.subscribe(listener, key: key) diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 7489caa7e..088a8538f 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -650,6 +650,9 @@ + (void)downloadIOSParamsWithAppId:(NSString *)appId { [OneSignalCoreImpl.sharedClient executeRequest:[OSRequestGetIosParams withUserId:userId appId:appId] onSuccess:^(NSDictionary *result) { if (result[IOS_JWT_REQUIRED]) { OneSignalUserManagerImpl.sharedInstance.requiresUserAuth = [result[IOS_JWT_REQUIRED] boolValue]; + } else { + // Remote params did not return IOS_JWT_REQUIRED + [OneSignalUserManagerImpl.sharedInstance remoteParamsReturnedUnknownRequiresUserAuth]; } if (result[IOS_USES_PROVISIONAL_AUTHORIZATION] != (id)[NSNull null]) {