diff --git a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj index 5837befcb..0cdec0008 100644 --- a/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj +++ b/iOS_SDK/OneSignalSDK/OneSignal.xcodeproj/project.pbxproj @@ -99,6 +99,9 @@ 7A9173A2231971E5007848FA /* OneSignalReceiveReceiptsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A9173A1231971E5007848FA /* OneSignalReceiveReceiptsController.m */; }; 7AA2848A2406FC6400C25D76 /* OSInAppMessageTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A1F2D8E2406EFC5007799A9 /* OSInAppMessageTag.m */; }; 7AA2848B2406FC6500C25D76 /* OSInAppMessageTag.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A1F2D8E2406EFC5007799A9 /* OSInAppMessageTag.m */; }; + 7AD172382416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD172372416D53B00A78B19 /* OSInAppMessageLocationPrompt.m */; }; + 7AD172392416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD172372416D53B00A78B19 /* OSInAppMessageLocationPrompt.m */; }; + 7AD1723A2416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD172372416D53B00A78B19 /* OSInAppMessageLocationPrompt.m */; }; 7AD8DDE7234BD3BE00747A8A /* OneSignalUserDefaults.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AD8DDE6234BD3BE00747A8A /* OneSignalUserDefaults.m */; }; 7ADE379422E8B69C00263048 /* OneSignalOutcomeEventsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ADE379322E8B69C00263048 /* OneSignalOutcomeEventsController.m */; }; 7ADE37AD22F2554400263048 /* OneSignalOutcomeEventsController.m in Sources */ = {isa = PBXBuildFile; fileRef = 7ADE379322E8B69C00263048 /* OneSignalOutcomeEventsController.m */; }; @@ -422,6 +425,8 @@ 7A880F302404AE7B0081F5E8 /* OSInAppMessagePushPrompt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessagePushPrompt.m; sourceTree = ""; }; 7A9173A1231971E5007848FA /* OneSignalReceiveReceiptsController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalReceiveReceiptsController.m; sourceTree = ""; }; 7A9173A3231971F8007848FA /* OneSignalReceiveReceiptsController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalReceiveReceiptsController.h; sourceTree = ""; }; + 7AD172362416D52D00A78B19 /* OSInAppMessageLocationPrompt.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OSInAppMessageLocationPrompt.h; sourceTree = ""; }; + 7AD172372416D53B00A78B19 /* OSInAppMessageLocationPrompt.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OSInAppMessageLocationPrompt.m; sourceTree = ""; }; 7AD8DDE6234BD3BE00747A8A /* OneSignalUserDefaults.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OneSignalUserDefaults.m; sourceTree = ""; }; 7AD8DDE8234BD3CF00747A8A /* OneSignalUserDefaults.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OneSignalUserDefaults.h; sourceTree = ""; }; 7ADE379322E8B69C00263048 /* OneSignalOutcomeEventsController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OneSignalOutcomeEventsController.m; sourceTree = ""; }; @@ -967,6 +972,8 @@ 7A880F2E2404AD010081F5E8 /* OSInAppMessagePrompt.h */, 7A880F2F2404AD920081F5E8 /* OSInAppMessagePushPrompt.h */, 7A880F302404AE7B0081F5E8 /* OSInAppMessagePushPrompt.m */, + 7AD172362416D52D00A78B19 /* OSInAppMessageLocationPrompt.h */, + 7AD172372416D53B00A78B19 /* OSInAppMessageLocationPrompt.m */, ); name = Model; sourceTree = ""; @@ -1237,6 +1244,7 @@ files = ( 9D1BD968237A28FC00A064F7 /* OSUniqueOutcomeNotification.m in Sources */, 7A9173A2231971E5007848FA /* OneSignalReceiveReceiptsController.m in Sources */, + 7AD172382416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */, 9124120E1E73342200E41FD7 /* OneSignal.m in Sources */, CACBAA97218A6243000ACAA5 /* OSMessagingController.m in Sources */, CA36F35921C33A2500300C77 /* OSInAppMessageController.m in Sources */, @@ -1310,6 +1318,7 @@ files = ( 9D1BD969237A28FC00A064F7 /* OSUniqueOutcomeNotification.m in Sources */, 9124120F1E73342200E41FD7 /* OneSignal.m in Sources */, + 7AD172392416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */, CACBAA98218A6243000ACAA5 /* OSMessagingController.m in Sources */, CA36F35A21C33A2500300C77 /* OSInAppMessageController.m in Sources */, CA08FC741FE99AFF004C445F /* OneSignalClient.m in Sources */, @@ -1452,6 +1461,7 @@ CACBAAAA218A65AE000ACAA5 /* InAppMessagingTests.m in Sources */, 4529DEE71FA82CDC00CEAB1D /* UNUserNotificationCenterOverrider.m in Sources */, 4529DEDB1FA8284E00CEAB1D /* NSDataOverrider.m in Sources */, + 7AD1723A2416D53B00A78B19 /* OSInAppMessageLocationPrompt.m in Sources */, CA7FC8A221927229002C4FD9 /* OSDynamicTriggerController.m in Sources */, 9D3300F623145AF3000F0A83 /* OneSignalViewHelper.m in Sources */, 4529DEF31FA8440A00CEAB1D /* UIAlertViewOverrider.m in Sources */, diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageAction.m b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageAction.m index 34eab1f7a..ca1626b4d 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageAction.m +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageAction.m @@ -27,6 +27,7 @@ #import "OSInAppMessageAction.h" #import "OSInAppMessagePushPrompt.h" +#import "OSInAppMessageLocationPrompt.h" @implementation OSInAppMessageAction @@ -96,6 +97,8 @@ + (instancetype)instanceWithJson:(NSDictionary *)json { for (NSString *prompt in promptActionsStrings) { if ([prompt isEqualToString:@"push"]) { [promptActions addObject:[[OSInAppMessagePushPrompt alloc] init]]; + } else if ([prompt isEqualToString:@"location"]) { + [promptActions addObject:[[OSInAppMessageLocationPrompt alloc] init]]; } } @@ -105,8 +108,7 @@ + (instancetype)instanceWithJson:(NSDictionary *)json { return action; } -- (NSString *)description -{ +- (NSString *)description { return [NSString stringWithFormat:@"OSInAppMessageAction outcome: %@ \ntag: %@ promptAction: %@", _outcomes, _tags, [_promptActions description]]; } diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.h b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.h new file mode 100644 index 000000000..b63484bb2 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.h @@ -0,0 +1,39 @@ +/** +* Modified MIT License +* +* Copyright 2020 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. +*/ + +#ifndef OSInAppMessageLocationPrompt_h +#define OSInAppMessageLocationPrompt_h + +#import "OSInAppMessagePrompt.h" + +@interface OSInAppMessageLocationPrompt : NSObject + +@property (nonatomic) BOOL hasPrompted; + +@end + +#endif /* OSInAppMessageLocationPrompt_h */ diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.m b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.m new file mode 100644 index 000000000..598ed33d9 --- /dev/null +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageLocationPrompt.m @@ -0,0 +1,56 @@ +/** +* Modified MIT License +* +* Copyright 2020 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 +#import "OSInAppMessageLocationPrompt.h" + +@interface OneSignal () + ++ (void)promptLocation:(void (^)(BOOL accepted))completionHandler; + +@end + +@implementation OSInAppMessageLocationPrompt + +- (instancetype)init +{ + self = [super init]; + if (self) { + _hasPrompted = NO; + } + return self; +} + +- (void)handlePrompt:(void (^)(BOOL accepted))completionHandler { + [OneSignal promptLocation:completionHandler]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"OSInAppMessageLocationPrompt hasPrompted:%@", _hasPrompted ? @"YES" : @"NO"]; +} + +@end diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageOutcome.m b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageOutcome.m index 342fc5d86..3c7beca76 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageOutcome.m +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageOutcome.m @@ -65,4 +65,8 @@ + (instancetype _Nullable)instancePreviewFromPayload:(OSNotificationPayload * _N return nil; } +- (NSString *)description { + return [NSString stringWithFormat:@"OSInAppMessageOutcome name: %@\nweight: %@unique: %s\n", _name, _weight, _unique ? "YES" : "NO"]; +} + @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePrompt.h b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePrompt.h index 1eaf7b928..9c8ce14ca 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePrompt.h +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePrompt.h @@ -34,7 +34,7 @@ @protocol OSInAppMessagePrompt -@property (nonatomic) BOOL didAppear; +@property (nonatomic) BOOL hasPrompted; - (void)handlePrompt:(void (^)(BOOL accepted))completionHandler; diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.h b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.h index 851280ade..9a026159b 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.h +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.h @@ -32,7 +32,7 @@ @interface OSInAppMessagePushPrompt : NSObject -@property (nonatomic) BOOL didAppear; +@property (nonatomic) BOOL hasPrompted; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.m b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.m index b6b81c89b..b62b09788 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.m +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessagePushPrompt.m @@ -34,7 +34,7 @@ - (instancetype)init { self = [super init]; if (self) { - _didAppear = NO; + _hasPrompted = NO; } return self; } @@ -43,9 +43,8 @@ - (void)handlePrompt:(void (^)(BOOL accepted))completionHandler { [OneSignal promptForPushNotificationsWithUserResponse:completionHandler fallbackToSettings:YES]; } -- (NSString *)description -{ - return [NSString stringWithFormat:@"OSInAppMessagePushPrompt didappear:%@", _didAppear ? @"YES" : @"NO"]; +- (NSString *)description { + return [NSString stringWithFormat:@"OSInAppMessagePushPrompt hasPrompted:%@", _hasPrompted ? @"YES" : @"NO"]; } @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.h b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.h index 4aa750870..d42283c9f 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.h +++ b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.h @@ -33,7 +33,13 @@ NS_ASSUME_NONNULL_BEGIN -@interface OSMessagingController : NSObject +@protocol OSMessagingControllerDelegate + +- (void)onApplicationDidBecomeActive; + +@end + +@interface OSMessagingController : NSObject @property (class, readonly) BOOL isInAppMessagingPaused; diff --git a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m index a6c7475e1..937518ce5 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m @@ -72,6 +72,8 @@ @interface OSMessagingController () @property (nonatomic, nullable) NSObject*currentPromptAction; +@property (nonatomic) BOOL isAppInactive; + @end @implementation OSMessagingController @@ -136,6 +138,7 @@ - (instancetype)init { self.clickedClickIds = [[NSMutableSet alloc] initWithSet:[standardUserDefaults getSavedSetForKey:OS_IAM_CLICKED_SET_KEY defaultValue:nil]]; self.impressionedInAppMessages = [[NSMutableSet alloc] initWithSet:[standardUserDefaults getSavedSetForKey:OS_IAM_IMPRESSIONED_SET_KEY defaultValue:nil]]; self.currentPromptAction = nil; + self.isAppInactive = NO; // BOOL that controls if in-app messaging is paused or not (false by default) [self setInAppMessagingPaused:false]; } @@ -200,7 +203,12 @@ - (void)presentInAppMessage:(OSInAppMessage *)message { // Return early if an IAM is already showing if (self.isInAppMessageShowing) return; - + // Return early if the app is not active + if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Pause IAMs display due to app inactivity"]; + _isAppInactive = YES; + return; + } [self displayMessage:message]; }; } @@ -340,7 +348,6 @@ - (void)setDataForRedisplay:(OSInAppMessage *)message { [self.impressionedInAppMessages removeObject:message.messageId]; [message clearClickIds]; return; - } } } @@ -424,7 +431,9 @@ - (void)messageViewControllerWasDismissed { if (!_currentPromptAction) { [self evaluateMessageDisplayQueue]; - } // else do nothing prompt is handling the re-showing + } else { //do nothing prompt is handling the re-showing + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Stop evaluateMessageDisplayQueue because prompt is currently displayed"]; + } } } @@ -473,26 +482,25 @@ - (void)persistInAppMessageForRedisplay:(OSInAppMessage *)message { [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"persistInAppMessageForRedisplay: %@ \nredisplayedInAppMessages: %@", [message description], [_redisplayedInAppMessages description]]]; } -- (void)handlePromptAction:(NSArray *> *)promptActions { +- (void)handlePromptActions:(NSArray *> *)promptActions { for (NSObject *promptAction in promptActions) { - if (![promptAction didAppear]) { + // Don't show prompt twice + if (!promptAction.hasPrompted) { _currentPromptAction = promptAction; + break; } - break; } if (_currentPromptAction) { [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"IAM prompt to handle: %@", [_currentPromptAction description]]]; - _currentPromptAction.didAppear = YES; + _currentPromptAction.hasPrompted = YES; [_currentPromptAction handlePrompt:^(BOOL accepted) { _currentPromptAction = nil; - // IAM dismissed by action - if (!_viewController) { - [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"IAM with prompt dismissed from actionTaken"]; - [self evaluateMessageDisplayQueue]; - } - //TODO: continue handling more than one prompt + [self handlePromptActions:promptActions]; }]; + } else if (!_viewController) { // IAM dismissed by action + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"IAM with prompt dismissed from actionTaken"]; + [self evaluateMessageDisplayQueue]; } } @@ -503,7 +511,7 @@ - (void)messageViewDidSelectAction:(OSInAppMessage *)message withAction:(OSInApp if (action.clickUrl) [self handleMessageActionWithURL:action]; - [self handlePromptAction:action.promptActions]; + [self handlePromptActions:action.promptActions]; if (self.actionClickBlock) self.actionClickBlock(action); @@ -609,6 +617,16 @@ - (void)triggerConditionChanged { [self evaluateMessages]; } +#pragma mark OSMessagingControllerDelegate Methods +- (void)onApplicationDidBecomeActive { + // To avoid excesive message evaluation + // we should re-evaluate all in-app messages only if it was paused by inactive + if (_isAppInactive) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Evaluating messages due to inactive app"]; + _isAppInactive = NO; + [self evaluateMessages]; + } +} @end @implementation DummyOSMessagingController diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index 2159236f7..0d5d9864d 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -556,7 +556,7 @@ + (id)initWithLaunchOptions:(NSDictionary*)launchOptions _outcomeEventsController = [[OneSignalOutcomeEventsController alloc] init:self.sessionManager]; if (appId && mShareLocation) - [OneSignalLocation getLocation:false]; + [OneSignalLocation getLocation:false withCompletionHandler:nil]; /* * No need to call the handleNotificationOpened:userInfo as it will be called from one of the following selectors @@ -1393,12 +1393,16 @@ + (void)setLocationShared:(BOOL)enable { } } -+ (void) promptLocation { ++ (void)promptLocation { + [self promptLocation:nil]; +} + ++ (void)promptLocation:(void (^)(BOOL accepted))completionHandler { // return if the user has not granted privacy permissions if ([self shouldLogMissingPrivacyConsentErrorWithMethodName:@"promptLocation"]) return; - [OneSignalLocation getLocation:true]; + [OneSignalLocation getLocation:true withCompletionHandler:completionHandler]; } + (BOOL)isLocationShared { diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.h b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.h index 654daab69..f0aba0c25 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.h +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.h @@ -47,11 +47,11 @@ typedef struct os_last_location { + (bool)started; + (void)internalGetLocation:(bool)prompt; - (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations; -+ (void) getLocation:(bool)prompt; -+ (void) sendLocation; ++ (void)getLocation:(bool)prompt withCompletionHandler:(void (^)(BOOL accepted))completionHandler; ++ (void)sendLocation; + (os_last_location*)lastLocation; + (void)clearLastLocation; -+ (void)onfocus:(BOOL)isActive; ++ (void)onFocus:(BOOL)isActive; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m index f3e879a62..624bbd107 100644 --- a/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignalLocation.m @@ -26,6 +26,7 @@ */ #import +#import #import "OneSignalLocation.h" #import "OneSignalHelper.h" @@ -64,6 +65,12 @@ @implementation OneSignalLocation #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wundeclared-selector" +NSMutableArray *_locationListeners; ++(NSMutableArray*)locationListeners { + if (!_locationListeners) + _locationListeners = [NSMutableArray new]; + return _locationListeners; +} NSObject *_mutexObjectForLastLocation; +(NSObject*)mutexObjectForLastLocation { @@ -97,7 +104,10 @@ + (void)clearLastLocation { } } -+ (void)getLocation:(bool)prompt { ++ (void)getLocation:(bool)prompt withCompletionHandler:(void (^)(BOOL accepted))completionHandler { + if (completionHandler) + [OneSignalLocation.locationListeners addObject:completionHandler]; + if (hasDelayed) [OneSignalLocation internalGetLocation:prompt]; else { @@ -108,17 +118,17 @@ + (void)getLocation:(bool)prompt { [OneSignalLocation internalGetLocation:prompt]; }); } - - //Listen to app going to and from background + // Listen to app going to and from background } -+ (void)onfocus:(BOOL)isActive { ++ (void)onFocus:(BOOL)isActive { // return if the user has not granted privacy permissions if ([OneSignal requiresUserPrivacyConsent]) return; - if(!locationManager || ![self started]) return; + if (!locationManager || ![self started]) + return; /** We have a state switch @@ -129,58 +139,80 @@ + (void)onfocus:(BOOL)isActive { Otherwise set timer to NULL **/ - NSTimeInterval remainingTimerTime = sendLocationTimer.fireDate.timeIntervalSinceNow; NSTimeInterval requiredWaitTime = isActive ? foregroundSendLocationWaitTime : backgroundSendLocationWaitTime ; NSTimeInterval adjustedTime = remainingTimerTime > 0 ? remainingTimerTime : requiredWaitTime; - if(isActive) { + if (isActive) { if(sendLocationTimer && initialLocationSent) { //Keep timer going with the remaining time [sendLocationTimer invalidate]; sendLocationTimer = [NSTimer scheduledTimerWithTimeInterval:adjustedTime target:self selector:@selector(sendLocation) userInfo:nil repeats:NO]; } - } - else { - + } else { //Check if always granted - if( (int)[NSClassFromString(@"CLLocationManager") performSelector:@selector(authorizationStatus)] == 3) { + if ((int)[NSClassFromString(@"CLLocationManager") performSelector:@selector(authorizationStatus)] == kCLAuthorizationStatusAuthorizedAlways) { [OneSignalLocation beginTask]; [sendLocationTimer invalidate]; sendLocationTimer = [NSTimer scheduledTimerWithTimeInterval:adjustedTime target:self selector:@selector(sendLocation) userInfo:nil repeats:NO]; [[NSRunLoop mainRunLoop] addTimer:sendLocationTimer forMode:NSRunLoopCommonModes]; + } else { + sendLocationTimer = NULL; } - else sendLocationTimer = NULL; } } -+ (void) beginTask { ++ (void)beginTask { fcTask = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ [OneSignalLocation endTask]; }]; } -+ (void) endTask { ++ (void)endTask { [[UIApplication sharedApplication] endBackgroundTask: fcTask]; fcTask = UIBackgroundTaskInvalid; } ++ (void)sendAndClearLocationListener:(BOOL)accept { + onesignal_Log(ONE_S_LL_DEBUG, [NSString stringWithFormat:@"OneSignalLocation sendAndClearLocationListener listeners: %@", OneSignalLocation.locationListeners]); + for (int i = 0; i < OneSignalLocation.locationListeners.count; i++) { + ((void (^)(BOOL accepted))[OneSignalLocation.locationListeners objectAtIndex:i])(accept); + } + // We only call the listeners once + [OneSignalLocation.locationListeners removeAllObjects]; +} + ++ (void)sendCurrentAuthStatusToListeners { + id clLocationManagerClass = NSClassFromString(@"CLLocationManager"); + CLAuthorizationStatus permissionStatus = [clLocationManagerClass performSelector:@selector(authorizationStatus)]; + if (permissionStatus == kCLAuthorizationStatusNotDetermined) + return; + // If already given or denied the permission, listeners should have the response + let denied = permissionStatus == kCLAuthorizationStatusRestricted || permissionStatus == kCLAuthorizationStatusDenied; + [self sendAndClearLocationListener:!denied]; +} + (void)internalGetLocation:(bool)prompt { - if ([self started]) + if ([self started]) { + [self sendCurrentAuthStatusToListeners]; return; + } id clLocationManagerClass = NSClassFromString(@"CLLocationManager"); // Check for location in plist - if (![clLocationManagerClass performSelector:@selector(locationServicesEnabled)]) + if (![clLocationManagerClass performSelector:@selector(locationServicesEnabled)]) { + [self sendAndClearLocationListener:false]; return; - int permissionStatus = [clLocationManagerClass performSelector:@selector(authorizationStatus)]; + } + + CLAuthorizationStatus permissionStatus = [clLocationManagerClass performSelector:@selector(authorizationStatus)]; // return if permission not determined and should not prompt - if (permissionStatus == 0 && !prompt) + if (permissionStatus == kCLAuthorizationStatusNotDetermined && !prompt) return; + [self sendCurrentAuthStatusToListeners]; locationManager = [[clLocationManagerClass alloc] init]; [locationManager setValue:[self sharedInstance] forKey:@"delegate"]; @@ -192,15 +224,15 @@ + (void)internalGetLocation:(bool)prompt { NSArray* backgroundModes = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"UIBackgroundModes"]; NSString* alwaysDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysUsageDescription"] ?: [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationAlwaysAndWhenInUseUsageDescription"]; // use background location updates if always permission granted or prompt allowed - if (backgroundModes && [backgroundModes containsObject:@"location"] && alwaysDescription && (permissionStatus == 3 || prompt)) { + if (backgroundModes && [backgroundModes containsObject:@"location"] && alwaysDescription && (permissionStatus == kCLAuthorizationStatusAuthorizedAlways || prompt)) { [locationManager performSelector:@selector(requestAlwaysAuthorization)]; - if ([OneSignalHelper isIOSVersionGreaterThanOrEqual:@"9.0"]) { + if ([OneSignalHelper isIOSVersionGreaterThanOrEqual:@"9.0"]) [locationManager setValue:@YES forKey:@"allowsBackgroundLocationUpdates"]; - } } else if ([[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSLocationWhenInUseUsageDescription"]) { - if (permissionStatus == 0) [locationManager performSelector:@selector(requestWhenInUseAuthorization)]; + if (permissionStatus == kCLAuthorizationStatusNotDetermined) + [locationManager performSelector:@selector(requestWhenInUseAuthorization)]; } else onesignal_Log(ONE_S_LL_ERROR, @"Include a privacy NSLocationAlwaysUsageDescription or NSLocationWhenInUseUsageDescription in your info.plist to request location permissions."); @@ -218,8 +250,10 @@ + (void)internalGetLocation:(bool)prompt { - (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations { // return if the user has not granted privacy permissions or location shared is false - if ([OneSignal requiresUserPrivacyConsent] || ![OneSignal isLocationShared]) + if ([OneSignal requiresUserPrivacyConsent] || ![OneSignal isLocationShared]) { + [OneSignalLocation sendAndClearLocationListener:false]; return; + } [manager performSelector:@selector(stopUpdatingLocation)]; @@ -243,20 +277,22 @@ - (void)locationManager:(id)manager didUpdateLocations:(NSArray *)locations { lastLocation->cords = cords; } - if(!sendLocationTimer) + if (!sendLocationTimer) [OneSignalLocation resetSendTimer]; - if(!initialLocationSent) + if (!initialLocationSent) [OneSignalLocation sendLocation]; - + + [OneSignalLocation sendAndClearLocationListener:true]; } --(void)locationManager:(id)manager didFailWithError:(NSError *)error { +- (void)locationManager:(id)manager didFailWithError:(NSError *)error { [OneSignal onesignal_Log:ONE_S_LL_ERROR message:[NSString stringWithFormat:@"CLLocationManager did fail with error: %@", error]]; + [OneSignalLocation sendAndClearLocationListener:false]; } + (void)resetSendTimer { - NSTimeInterval requiredWaitTime = [UIApplication sharedApplication].applicationState == UIApplicationStateActive ? foregroundSendLocationWaitTime : backgroundSendLocationWaitTime ; + NSTimeInterval requiredWaitTime = [UIApplication sharedApplication].applicationState == UIApplicationStateActive ? foregroundSendLocationWaitTime : backgroundSendLocationWaitTime; sendLocationTimer = [NSTimer scheduledTimerWithTimeInterval:requiredWaitTime target:self selector:@selector(sendLocation) userInfo:nil repeats:NO]; } @@ -267,7 +303,8 @@ + (void)sendLocation { return; @synchronized(OneSignalLocation.mutexObjectForLastLocation) { - if (!lastLocation || ![OneSignal mUserId]) return; + if (!lastLocation || ![OneSignal mUserId]) + return; //Fired from timer and not initial location fetched if (initialLocationSent) diff --git a/iOS_SDK/OneSignalSDK/Source/UIApplicationDelegate+OneSignal.m b/iOS_SDK/OneSignalSDK/Source/UIApplicationDelegate+OneSignal.m index 85e88b3e1..48304f922 100644 --- a/iOS_SDK/OneSignalSDK/Source/UIApplicationDelegate+OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/UIApplicationDelegate+OneSignal.m @@ -36,6 +36,7 @@ #import "OneSignalLocation.h" #import "OneSignalSelectorHelpers.h" #import "OneSignalHelper.h" +#import "OSMessagingController.h" @interface OneSignal (UN_extra) + (void) didRegisterForRemoteNotifications:(UIApplication*)app deviceToken:(NSData*)inDeviceToken; @@ -113,7 +114,7 @@ - (void) setOneSignalDelegate:(id)delegate { // Used to track how long the app has been closed injectToProperClass(@selector(oneSignalApplicationWillTerminate:), @selector(applicationWillTerminate:), delegateSubclasses, newClass, delegateClass); - + [self setOneSignalDelegate:delegate]; } @@ -255,7 +256,7 @@ - (void) oneSignalApplicationDidEnterBackground:(UIApplication*)application { [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"oneSignalApplicationDidEnterBackground"]; if ([OneSignal app_id]) - [OneSignalLocation onfocus:NO]; + [OneSignalLocation onFocus:NO]; if ([self respondsToSelector:@selector(oneSignalApplicationDidEnterBackground:)]) [self oneSignalApplicationDidEnterBackground:application]; @@ -266,7 +267,8 @@ - (void)oneSignalApplicationDidBecomeActive:(UIApplication*)application { if ([OneSignal app_id]) { [OneSignalTracker onFocus:NO]; - [OneSignalLocation onfocus:YES]; + [OneSignalLocation onFocus:YES]; + [[OSMessagingController sharedInstance] onApplicationDidBecomeActive]; } if ([self respondsToSelector:@selector(oneSignalApplicationDidBecomeActive:)]) diff --git a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m index 8b1fcdc29..84d9d89a3 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m @@ -867,7 +867,7 @@ - (void)testIAMClickedLaunchesTagSendAndRemoveAPIRequest { XCTAssertEqual(0 ,[OneSignalClientOverrider.lastHTTPRequest[@"tags"] count]); } -- (void)testIAMClickedLaunchesPrompt { +- (void)testIAMClickedLaunchesPushPrompt { let message = [OSInAppMessageTestHelper testMessageJsonWithTriggerPropertyName:OS_DYNAMIC_TRIGGER_KIND_SESSION_TIME withId:@"test_id1" withOperator:OSTriggerOperatorTypeLessThan withValue:@10.0]; let registrationResponse = [OSInAppMessageTestHelper testRegistrationJsonWithMessages:@[message]]; @@ -879,13 +879,61 @@ - (void)testIAMClickedLaunchesPrompt { [UnitTestCommonMethods initOneSignalAndThreadWait]; NSMutableDictionary *actionJson = [OSInAppMessageTestHelper.testActionJson mutableCopy]; - [actionJson setValue:@[@"push"] forKey:@"prompts"]; - let action = [OSInAppMessageAction instanceWithJson: actionJson]; + actionJson[@"prompts"] = @[@"push"]; + let action = [OSInAppMessageAction instanceWithJson:actionJson]; + let testMessage = [OSInAppMessage instanceWithJson:message]; + + XCTAssertEqual(1, action.promptActions.count); + XCTAssertFalse(action.promptActions[0].hasPrompted); + [OSMessagingController.sharedInstance messageViewDidSelectAction:testMessage withAction:action]; + XCTAssertTrue(action.promptActions[0].hasPrompted); +} + +- (void)testIAMClickedLaunchesLocationPrompt { + let message = [OSInAppMessageTestHelper testMessageJsonWithTriggerPropertyName:OS_DYNAMIC_TRIGGER_KIND_SESSION_TIME withId:@"test_id1" withOperator:OSTriggerOperatorTypeLessThan withValue:@10.0]; + + let registrationResponse = [OSInAppMessageTestHelper testRegistrationJsonWithMessages:@[message]]; + + // the trigger should immediately evaluate to true and should + // be shown once the SDK is fully initialized. + [OneSignalClientOverrider setMockResponseForRequest:NSStringFromClass([OSRequestRegisterUser class]) withResponse:registrationResponse]; + + [UnitTestCommonMethods initOneSignalAndThreadWait]; + + NSMutableDictionary *actionJson = [OSInAppMessageTestHelper.testActionJson mutableCopy]; + actionJson[@"prompts"] = @[@"location"]; + let action = [OSInAppMessageAction instanceWithJson:actionJson]; let testMessage = [OSInAppMessage instanceWithJson:message]; - XCTAssertFalse(action.promptActions[0].didAppear); + XCTAssertEqual(1, action.promptActions.count); + XCTAssertFalse(action.promptActions[0].hasPrompted); [OSMessagingController.sharedInstance messageViewDidSelectAction:testMessage withAction:action]; - XCTAssertTrue(action.promptActions[0].didAppear); + XCTAssertTrue(action.promptActions[0].hasPrompted); +} + +- (void)testIAMClickedLaunchesPushLocationPrompt { + let message = [OSInAppMessageTestHelper testMessageJsonWithTriggerPropertyName:OS_DYNAMIC_TRIGGER_KIND_SESSION_TIME withId:@"test_id1" withOperator:OSTriggerOperatorTypeLessThan withValue:@10.0]; + + let registrationResponse = [OSInAppMessageTestHelper testRegistrationJsonWithMessages:@[message]]; + + // the trigger should immediately evaluate to true and should + // be shown once the SDK is fully initialized. + [OneSignalClientOverrider setMockResponseForRequest:NSStringFromClass([OSRequestRegisterUser class]) withResponse:registrationResponse]; + + [UnitTestCommonMethods initOneSignalAndThreadWait]; + + NSMutableDictionary *actionJson = [OSInAppMessageTestHelper.testActionJson mutableCopy]; + actionJson[@"prompts"] = @[@"push", @"location"]; + let action = [OSInAppMessageAction instanceWithJson:actionJson]; + let testMessage = [OSInAppMessage instanceWithJson:message]; + + XCTAssertEqual(2, action.promptActions.count); + XCTAssertFalse(action.promptActions[0].hasPrompted); + XCTAssertFalse(action.promptActions[1].hasPrompted); + [OSMessagingController.sharedInstance messageViewDidSelectAction:testMessage withAction:action]; + [UnitTestCommonMethods runBackgroundThreads]; + XCTAssertTrue(action.promptActions[0].hasPrompted); + XCTAssertTrue(action.promptActions[1].hasPrompted); } - (void)testDisablingIAMs_stillCreatesMessageQueue_butPreventsMessageDisplay { diff --git a/iOS_SDK/OneSignalSDK/UnitTests/OutcomeIntegrationTests.m b/iOS_SDK/OneSignalSDK/UnitTests/OutcomeIntegrationTests.m index 29520d1ef..84fe33fcb 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/OutcomeIntegrationTests.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/OutcomeIntegrationTests.m @@ -89,7 +89,7 @@ - (void)testUnattributedSession_onFocusUnattributed { [OneSignalTracker onFocus:true]; [UnitTestCommonMethods runBackgroundThreads]; - // 4. Ensure onfocus is made right away. + // 4. Ensure onFocus is made right away. [RestClientAsserts assertOnFocusAtIndex:2 withTime:60]; } @@ -118,7 +118,7 @@ - (void)testIndirectSession_onFocusAttributed { [NSTimerOverrider runPendingSelectors]; [UnitTestCommonMethods runBackgroundThreads]; - // 7. Ensure onfocus is sent in the background. + // 7. Ensure onFocus is sent in the background. [RestClientAsserts assertOnFocusAtIndex:4 withTime:15]; } @@ -147,7 +147,7 @@ - (void)testDirectSession_onFocusAttributed { [NSTimerOverrider runPendingSelectors]; [UnitTestCommonMethods runBackgroundThreads]; - // 7. Ensure onfocus is sent in the background. + // 7. Ensure onFocus is sent in the background. [RestClientAsserts assertOnFocusAtIndex:4 withTime:15]; } diff --git a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.h b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.h index 1f0d5063c..3b6050452 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.h +++ b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.h @@ -38,7 +38,6 @@ NS_ASSUME_NONNULL_BEGIN + (void)setSeenMessages:(NSMutableSet *)seenMessages; + (void)setMockDateGenerator:(NSTimeInterval(^)(void))testDateGenerator; + (BOOL)isInAppMessageShowing; -+ (BOOL)currentPromptAppear; + (NSArray *)messageDisplayQueue; + (NSMutableDictionary *)messagesForRedisplay; diff --git a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.m b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.m index 9669e3751..3bace3473 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/Shadows/OSMessagingControllerOverrider.m @@ -93,10 +93,6 @@ + (void)dismissCurrentMessage { return [OSMessagingController.sharedInstance messageViewControllerWasDismissed]; } -+ (BOOL)currentPromptAppear { - return OSMessagingController.sharedInstance.currentPromptAction.didAppear; -} - + (BOOL)isInAppMessageShowing { return OSMessagingController.sharedInstance.isInAppMessageShowing; }