diff --git a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m index 7f697eca9..4d5428739 100644 --- a/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m +++ b/iOS_SDK/OneSignalDevApp/OneSignalDevApp/AppDelegate.m @@ -85,12 +85,12 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( return YES; } -#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"ONESIGNAL_APP_ID_KEY_FOR_TESTING" +#define ONESIGNAL_APP_ID_KEY_FOR_TESTING @"77e32082-ea27-42e3-a898-c72e141824ef" + (NSString*)getOneSignalAppId { NSString* onesignalAppId = [[NSUserDefaults standardUserDefaults] objectForKey:ONESIGNAL_APP_ID_KEY_FOR_TESTING]; if (!onesignalAppId) - onesignalAppId = @"0ba9731b-33bd-43f4-8b59-61172e27447d"; + onesignalAppId = @"77e32082-ea27-42e3-a898-c72e141824ef"; return onesignalAppId; } diff --git a/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.h b/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.h index ef9c61686..5dbcf3e3a 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.h +++ b/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.h @@ -33,6 +33,8 @@ NS_ASSUME_NONNULL_BEGIN @protocol OSDynamicTriggerControllerDelegate - (void)dynamicTriggerFired; +// Alerts the observer that a trigger evaluated to true +- (void)dynamicTriggerCompleted:(NSString *)triggerId; @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.m b/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.m index 9d3fde49a..90e9f2cc0 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.m +++ b/iOS_SDK/OneSignalSDK/Source/OSDynamicTriggerController.m @@ -58,7 +58,6 @@ - (instancetype)init { } - (BOOL)dynamicTriggerShouldFire:(OSTrigger *)trigger withMessageId:(NSString *)messageId { - if (!trigger.value) return false; @@ -79,10 +78,12 @@ - (BOOL)dynamicTriggerShouldFire:(OSTrigger *)trigger withMessageId:(NSString *) // Check what type of trigger it is if ([trigger.kind isEqualToString:OS_DYNAMIC_TRIGGER_KIND_SESSION_TIME]) { let currentDuration = fabs([[OneSignal sessionLaunchTime] timeIntervalSinceNow]); - - if ([self evaluateTimeInterval:requiredTimeValue withCurrentValue:currentDuration forOperator:trigger.operatorType]) + if ([self evaluateTimeInterval:requiredTimeValue withCurrentValue:currentDuration forOperator:trigger.operatorType]) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"session time trigger completed: %@", trigger.triggerId]]; + [self.delegate dynamicTriggerCompleted:trigger.triggerId]; + //[self.delegate dynamicTriggerFired:trigger.triggerId]; return true; - + } offset = requiredTimeValue - currentDuration; } else if ([trigger.kind isEqualToString:OS_DYNAMIC_TRIGGER_KIND_MIN_TIME_SINCE]) { @@ -92,9 +93,10 @@ - (BOOL)dynamicTriggerShouldFire:(OSTrigger *)trigger withMessageId:(NSString *) let timestampSinceLastMessage = fabs([self.timeSinceLastMessage timeIntervalSinceNow]); - if ([self evaluateTimeInterval:requiredTimeValue withCurrentValue:timestampSinceLastMessage forOperator:trigger.operatorType]) + if ([self evaluateTimeInterval:requiredTimeValue withCurrentValue:timestampSinceLastMessage forOperator:trigger.operatorType]) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"time since last inapp trigger completed: %@", trigger.triggerId]]; return true; - + } offset = requiredTimeValue - timestampSinceLastMessage; } @@ -103,17 +105,18 @@ - (BOOL)dynamicTriggerShouldFire:(OSTrigger *)trigger withMessageId:(NSString *) return false; // If we reach this point, it means we need to return false and set up a timer for a future time - let timer = [NSTimer timerWithTimeInterval:offset + NSTimer *timer = [NSTimer timerWithTimeInterval:offset target:self selector:@selector(timerFiredForMessage:) userInfo:@{@"trigger" : trigger} repeats:false]; - if (timer) + if (timer) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"timer added for triggerId: %@, messageId: %@", trigger.triggerId, messageId]]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; + } [self.scheduledMessages addObject:trigger.triggerId]; } - return false; } diff --git a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageDisplayStats.m b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageDisplayStats.m index 96a446356..76004877a 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSInAppMessageDisplayStats.m +++ b/iOS_SDK/OneSignalSDK/Source/OSInAppMessageDisplayStats.m @@ -111,7 +111,9 @@ - (BOOL)isDelayTimeSatisfied:(NSTimeInterval)date { } - (BOOL)shouldDisplayAgain { - return _displayQuantity < _displayLimit; + BOOL result = _displayQuantity < _displayLimit; + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"In app message shouldDisplayAgain: %hhu", result]]; + return result; } - (void)incrementDisplayQuantity { @@ -131,6 +133,7 @@ - (void)encodeWithCoder:(NSCoder *)encoder { [encoder encodeInteger:_displayQuantity forKey:@"displayQuantity"]; [encoder encodeDouble:_displayDelay forKey:@"displayDelay"]; [encoder encodeDouble:_lastDisplayTime forKey:@"lastDisplayTime"]; + [encoder encodeBool:_redisplayEnabled forKey:@"redisplayEnabled"]; } - (id)initWithCoder:(NSCoder *)decoder { @@ -139,6 +142,7 @@ - (id)initWithCoder:(NSCoder *)decoder { _displayQuantity = [decoder decodeIntegerForKey:@"displayQuantity"]; _displayDelay = [decoder decodeDoubleForKey:@"displayDelay"]; _lastDisplayTime = [decoder decodeDoubleForKey:@"lastDisplayTime"]; + _redisplayEnabled = [decoder decodeBoolForKey:@"redisplayEnabled"]; } return self; } diff --git a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m index 078726a32..bfdc7186e 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m +++ b/iOS_SDK/OneSignalSDK/Source/OSMessagingController.m @@ -322,11 +322,13 @@ - (void)messageViewImpressionRequest:(OSInAppMessage *)message { - (void)evaluateMessages { [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Evaluating in app messages"]; for (OSInAppMessage *message in self.messages) { - // Make changes to IAM if redisplay available - [self setDataForRedisplay:message]; - // Should we show the in app message - if ([self shouldShowInAppMessage:message]) { - [self presentInAppMessage:message]; + if ([self.triggerController messageMatchesTriggers:message]) { + // Make changes to IAM if redisplay available + [self setDataForRedisplay:message]; + // Should we show the in app message + if ([self shouldShowInAppMessage:message]) { + [self presentInAppMessage:message]; + } } } } @@ -491,8 +493,11 @@ - (void)hideWindow { - (void)persistInAppMessageForRedisplay:(OSInAppMessage *)message { // If the IAM doesn't have the re display prop or is a preview IAM there is no need to save it - if (![message.displayStats isRedisplayEnabled] || message.isPreview) - return; + if (![message.displayStats isRedisplayEnabled] || message.isPreview) { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"not persisting %@",message.displayStats]]; + return; + } + let displayTimeSeconds = self.dateGenerator(); message.displayStats.lastDisplayTime = displayTimeSeconds; @@ -697,9 +702,25 @@ - (void)addKeySceneToWindow:(UIWindow*)window { } } +- (void)dynamicTriggerCompleted:(NSString *)triggerId { + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:[NSString stringWithFormat:@"messageDynamicTriggerCompleted called with triggerId: %@", triggerId]]; + [self makeRedisplayMessagesAvailableWithTriggers:@[triggerId]]; +} + +- (void)makeRedisplayMessagesAvailableWithTriggers:(NSArray *)triggerIds { + for (OSInAppMessage *message in self.messages) { + if ([self.redisplayedInAppMessages objectForKey:message.messageId] + && [_triggerController hasSharedTriggers:message newTriggersKeys:triggerIds]) { + message.isTriggerChanged = YES; + } + } +} + #pragma mark OSTriggerControllerDelegate Methods + - (void)triggerConditionChanged { // We should re-evaluate all in-app messages + [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Trigger condition changed"]; [self evaluateMessages]; } @@ -741,5 +762,6 @@ - (void)messageViewDidSelectAction:(OSInAppMessage *)message withAction:(OSInApp - (void)webViewContentFinishedLoading {} #pragma mark OSTriggerControllerDelegate Methods - (void)triggerConditionChanged {} +- (void)dynamicTriggerCompleted:(NSString *)triggerId {} @end diff --git a/iOS_SDK/OneSignalSDK/Source/OSTriggerController.h b/iOS_SDK/OneSignalSDK/Source/OSTriggerController.h index 0ac1d723e..4a25dd2f7 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSTriggerController.h +++ b/iOS_SDK/OneSignalSDK/Source/OSTriggerController.h @@ -39,7 +39,7 @@ NS_ASSUME_NONNULL_BEGIN It is also called when the app changes trigger values */ - (void)triggerConditionChanged; - +- (void)dynamicTriggerCompleted:(NSString *)triggerId; @end @interface OSTriggerController : NSObject diff --git a/iOS_SDK/OneSignalSDK/Source/OSTriggerController.m b/iOS_SDK/OneSignalSDK/Source/OSTriggerController.m index 98df5e4f2..5327eaaad 100644 --- a/iOS_SDK/OneSignalSDK/Source/OSTriggerController.m +++ b/iOS_SDK/OneSignalSDK/Source/OSTriggerController.m @@ -85,7 +85,9 @@ - (BOOL)hasSharedTriggers:(OSInAppMessage *)message newTriggersKeys:(NSArray *andConditions in message.triggers) { for (OSTrigger *trigger in andConditions) { - if ([triggerKey isEqual:trigger.property]) { + // Dynamic triggers depends on triggerId + // Common triggers changed by user depends on property + if ([triggerKey isEqual:trigger.property] || [triggerKey isEqualToString:trigger.triggerId]) { // At least one trigger has changed return YES; } @@ -263,4 +265,8 @@ - (void)dynamicTriggerFired { [self.delegate triggerConditionChanged]; } +- (void)dynamicTriggerCompleted:(NSString *)triggerId { + [self.delegate dynamicTriggerCompleted:triggerId]; +} + @end diff --git a/iOS_SDK/OneSignalSDK/Source/OneSignal.m b/iOS_SDK/OneSignalSDK/Source/OneSignal.m index b91719977..a8ce082a4 100755 --- a/iOS_SDK/OneSignalSDK/Source/OneSignal.m +++ b/iOS_SDK/OneSignalSDK/Source/OneSignal.m @@ -1712,6 +1712,7 @@ + (void)registerUserInternal { } [OneSignal onesignal_Log:ONE_S_LL_VERBOSE message:@"Calling OneSignal create/on_session"]; + sessionLaunchTime = [NSDate date]; if (mShareLocation && [OneSignalLocation lastLocation]) { @@ -2801,7 +2802,6 @@ @implementation OneSignal (SessionStatusDelegate) + (void)onSessionEnding:(NSArray *)lastInfluences { if (_outcomeEventsController) [_outcomeEventsController clearOutcomes]; - [OneSignalTracker onSessionEnded:lastInfluences]; } @@ -2850,7 +2850,6 @@ + (void)load { injectToProperClass(@selector(onesignalSetApplicationIconBadgeNumber:), @selector(setApplicationIconBadgeNumber:), @[], [OneSignalAppDelegate class], [UIApplication class]); [self setupUNUserNotificationCenterDelegate]; - sessionLaunchTime = [NSDate date]; } diff --git a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m index 1df4ab877..87f9ff59e 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingIntegrationTests.m @@ -1422,6 +1422,79 @@ - (void)testIAMHTMLLoadWithDefaultLanguage { XCTAssertTrue([url containsString:OS_TEST_MESSAGE_ID]); } +- (void)testInAppMessageDisplayMultipleTimesSessionDurationTrigger { + [OneSignal pauseInAppMessages:NO]; + let trigger = [OSTrigger dynamicTriggerWithKind:OS_DYNAMIC_TRIGGER_KIND_SESSION_TIME withOperator:OSTriggerOperatorTypeGreaterThanOrEqualTo withValue:@0]; + let message = [OSInAppMessageTestHelper testMessageWithTriggers:@[@[trigger]] withRedisplayLimit:5 delay:@30]; + let registrationJson = [OSInAppMessageTestHelper testRegistrationJsonWithMessages:@[message.jsonRepresentation]]; + + //Time interval mock + NSDateComponents* comps = [[NSDateComponents alloc]init]; + comps.year = 2020; + comps.month = 9; + comps.day = 10; + comps.hour = 10; + comps.minute = 1; + + NSCalendar* calendar = [NSCalendar currentCalendar]; + NSDate* date = [calendar dateFromComponents:comps]; + NSTimeInterval firstInterval = [date timeIntervalSince1970]; + + // Init OneSignal IAM with redisplay + [self initOneSignalWithRegistrationJSON:registrationJson]; + + [OSMessagingControllerOverrider setMockDateGenerator: ^NSTimeInterval(void) { + return firstInterval; + }]; + + XCTAssertEqual(OSMessagingControllerOverrider.messageDisplayQueue.count, 1); + + // Dismiss IAM will make display quantity increase and last display time to change + [OSMessagingControllerOverrider dismissCurrentMessage]; + // Check IAMs was removed from queue + XCTAssertEqual(OSMessagingControllerOverrider.messageDisplayQueue.count, 0); + // Check if data after dismiss is set correctly + XCTAssertEqual(OSMessagingControllerOverrider.messagesForRedisplay.count, 1); + let displayQuantity = OSMessagingControllerOverrider.messagesForRedisplay.allValues[0].displayStats.displayQuantity; + let displayTime = OSMessagingControllerOverrider.messagesForRedisplay.allValues[0].displayStats.lastDisplayTime; + XCTAssertEqual(displayQuantity, 1); + XCTAssertTrue(displayTime > 0); + + [UnitTestCommonMethods runBackgroundThreads]; + + // 3. Kill the app and wait 31 seconds + [UnitTestCommonMethods backgroundApp]; + [NSDateOverrider advanceSystemTimeBy:31]; + [UnitTestCommonMethods runBackgroundThreads]; + //Time interval mock + comps.minute = 32; + NSDate* secondDate = [calendar dateFromComponents:comps]; + NSTimeInterval secondInterval = [secondDate timeIntervalSince1970]; + [OSMessagingControllerOverrider setMockDateGenerator: ^NSTimeInterval(void) { + return secondInterval; + }]; + + // Init OneSignal IAM with redisplay + [self initOneSignalWithRegistrationJSON:registrationJson]; + + [OSMessagingControllerOverrider setMockDateGenerator: ^NSTimeInterval(void) { + return firstInterval; + }]; + [UnitTestCommonMethods foregroundApp]; + [UnitTestCommonMethods runBackgroundThreads]; + + XCTAssertEqual(OSMessagingControllerOverrider.messageDisplayQueue.count, 1); + + [OSMessagingControllerOverrider dismissCurrentMessage]; + // Check IAMs was removed from queue + XCTAssertEqual(OSMessagingControllerOverrider.messageDisplayQueue.count, 0); + // Check if data after dismiss is set correctly + XCTAssertEqual(OSMessagingControllerOverrider.messagesForRedisplay.count, 1); + let secondDisplayQuantity = OSMessagingControllerOverrider.messagesForRedisplay.allValues[0].displayStats.displayQuantity; + let secondDisplayTime = OSMessagingControllerOverrider.messagesForRedisplay.allValues[0].displayStats.lastDisplayTime; + XCTAssertEqual(secondDisplayQuantity, 2); +} + // Helper method that adds an OSInAppMessage to the IAM messageDisplayQueue // Mock response JSON and initializes the OneSignal SDK - (void)initOneSignalWithInAppMessage:(OSInAppMessage *)message { diff --git a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingTests.m b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingTests.m index 26406c393..6698400b4 100644 --- a/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingTests.m +++ b/iOS_SDK/OneSignalSDK/UnitTests/InAppMessagingTests.m @@ -541,7 +541,7 @@ - (void)testDynamicTriggerSessionDurationLaunchesTimer { XCTAssertFalse(triggered); XCTAssertTrue(NSTimerOverrider.hasScheduledTimer); - XCTAssertTrue(fabs(NSTimerOverrider.mostRecentTimerInterval - 30.0f) < 0.1f); + XCTAssertTrue(fabs(NSTimerOverrider.mostRecentTimerInterval - 30.0f) < 1.1f); }