From 3b20bf97bf2e1d080c06cb0c38dcdaab6d3c1aae Mon Sep 17 00:00:00 2001 From: Nikola Zagorchev Date: Mon, 17 Jun 2024 17:30:51 +0300 Subject: [PATCH] [MC-1470] Custom templates actions (Custom Templates Part 5) (#336) * Implement actions * Add unit tests * Do not trigger action for functions * Remove the string type prop from InAppNotification, use the enum * Check if presented * Move isAction to internal header --- CleverTapSDK.xcodeproj/project.pbxproj | 10 ++ CleverTapSDK/CTInAppDisplayViewController.m | 14 +- CleverTapSDK/CTInAppNotification.h | 1 - CleverTapSDK/CTInAppNotification.m | 8 +- .../CTInAppNotificationDisplayDelegate.h | 4 +- CleverTapSDK/CTInAppUtils.h | 1 + CleverTapSDK/CTInAppUtils.m | 26 ++- CleverTapSDK/CTNotificationAction.h | 3 +- CleverTapSDK/CTNotificationAction.m | 8 + CleverTapSDK/InApps/CTInAppDisplayManager.m | 100 +++++++++-- .../InApps/CTInAppHTMLViewController.m | 8 +- CleverTapSDK/InApps/CTInAppStore.h | 1 + CleverTapSDK/InApps/CTInAppStore.m | 10 ++ .../CustomTemplates/CTAppFunctionBuilder.m | 2 +- .../CTCustomTemplate-Internal.h | 1 + .../InApps/CustomTemplates/CTCustomTemplate.h | 1 + .../InApps/CustomTemplates/CTCustomTemplate.m | 3 + .../CustomTemplates/CTCustomTemplateBuilder.h | 3 + .../CustomTemplates/CTCustomTemplateBuilder.m | 1 + .../CTCustomTemplateInAppData-Internal.h | 20 +++ .../CTCustomTemplateInAppData.h | 2 + .../CTCustomTemplateInAppData.m | 29 +++ .../CTCustomTemplatesManager-Internal.h | 5 +- .../CTCustomTemplatesManager.h | 3 + .../CTCustomTemplatesManager.m | 58 +++++- .../CustomTemplates/CTInAppTemplateBuilder.m | 2 +- .../CTTemplateContext-Internal.h | 10 +- .../CustomTemplates/CTTemplateContext.m | 52 +++++- .../InApps/CTInAppEvaluationManagerTest.m | 4 +- .../InApps/CTNotificationActionTest.m | 8 + .../CTCustomTemplateInAppDataTest.m | 37 +++- .../CustomTemplates/CTCustomTemplateTest.m | 17 +- .../CTCustomTemplatesManagerTest.m | 126 +++++++++---- .../CTInAppNotificationDisplayDelegateMock.h | 23 +++ .../CTInAppNotificationDisplayDelegateMock.m | 32 ++++ .../CustomTemplates/CTTemplateContextTest.m | 169 +++++++++++++++++- .../CustomTemplates/CTTemplatePresenterMock.h | 3 + .../CustomTemplates/CTTemplatePresenterMock.m | 2 + 38 files changed, 698 insertions(+), 109 deletions(-) create mode 100644 CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h create mode 100644 CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h create mode 100644 CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m diff --git a/CleverTapSDK.xcodeproj/project.pbxproj b/CleverTapSDK.xcodeproj/project.pbxproj index 30cb48f4..f272d0ff 100644 --- a/CleverTapSDK.xcodeproj/project.pbxproj +++ b/CleverTapSDK.xcodeproj/project.pbxproj @@ -343,6 +343,8 @@ 6B535FB72AD56C60002A2663 /* CTMultiDelegateManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */; }; 6B535FB82AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; 6B535FB92AD56C60002A2663 /* CTMultiDelegateManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */; }; + 6B9157B92C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m in Sources */ = {isa = PBXBuildFile; fileRef = 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */; }; + 6B9157BB2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */; }; 6B9DEE9F2B4D8A500097EF40 /* clevertap-logo.png in Resources */ = {isa = PBXBuildFile; fileRef = 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */; }; 6BA3B2DB2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; 6BA3B2DC2B03E926004E834B /* CTQueueType.h in Headers */ = {isa = PBXBuildFile; fileRef = 6BA3B2DA2B03E926004E834B /* CTQueueType.h */; }; @@ -876,6 +878,9 @@ 6B4A0F902B45EF6D00A42C6D /* CTInAppTriggerManagerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppTriggerManagerTest.m; sourceTree = ""; }; 6B535FB42AD56C60002A2663 /* CTMultiDelegateManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTMultiDelegateManager.h; sourceTree = ""; }; 6B535FB52AD56C60002A2663 /* CTMultiDelegateManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTMultiDelegateManager.m; sourceTree = ""; }; + 6B9157B72C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTInAppNotificationDisplayDelegateMock.h; sourceTree = ""; }; + 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CTInAppNotificationDisplayDelegateMock.m; sourceTree = ""; }; + 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTCustomTemplateInAppData-Internal.h"; sourceTree = ""; }; 6B9DEE9E2B4D8A500097EF40 /* clevertap-logo.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "clevertap-logo.png"; sourceTree = ""; }; 6B9DEEA02B4DF1B70097EF40 /* CTInAppImagePrefetchManager+Tests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CTInAppImagePrefetchManager+Tests.h"; sourceTree = ""; }; 6BA3B2DA2B03E926004E834B /* CTQueueType.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CTQueueType.h; sourceTree = ""; }; @@ -1455,6 +1460,8 @@ 6B32A0B32B9F2E8F009ADC57 /* CTTestTemplateProducer.m */, 6BB778CD2BEE48C300A41628 /* CTCustomTemplateInAppDataTest.m */, 6BB778D12BF267B600A41628 /* CTTemplateContextTest.m */, + 6B9157B72C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.h */, + 6B9157B82C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m */, ); path = CustomTemplates; sourceTree = ""; @@ -1484,6 +1491,7 @@ 6BB778C52BECEC2700A41628 /* CTCustomTemplateInAppData.h */, 6BB778C62BECEC2700A41628 /* CTCustomTemplateInAppData.m */, 6BB778D82BFD277400A41628 /* CTCustomTemplatesManager-Internal.h */, + 6B9157BA2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h */, ); path = CustomTemplates; sourceTree = ""; @@ -2009,6 +2017,7 @@ 6BF5A5912ACC854800CDED20 /* CTInAppDisplayManager.h in Headers */, 6B32A0A12B99033F009ADC57 /* CTCustomTemplateBuilder-Internal.h in Headers */, 07B94547219EA34300D4C542 /* CTMessageMO.h in Headers */, + 6B9157BB2C11D07200B1C907 /* CTCustomTemplateInAppData-Internal.h in Headers */, 4E25E3C2278887A70008C888 /* CTIdentityRepoFactory.h in Headers */, 071EB4FF217F6427008F0FAB /* CTHeaderViewController.h in Headers */, 071EB4D6217F6427008F0FAB /* CTInAppFCManager.h in Headers */, @@ -2434,6 +2443,7 @@ 6BEEC2CE2AEC49F100BD4EC5 /* CTImpressionManagerTest.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, 4E2BFB9C2AD69BCA00DEB247 /* XCTestCase+XCTestCase_Tests.m in Sources */, + 6B9157B92C10F40C00B1C907 /* CTInAppNotificationDisplayDelegateMock.m in Sources */, 6A59D20F2A3351A800531F9D /* LeanplumCTTest.m in Sources */, 32790957299CC099001FE140 /* CTUtilsTest.m in Sources */, 6A2E4C18291E8A4A00385536 /* CleverTapInstanceConfigTests.m in Sources */, diff --git a/CleverTapSDK/CTInAppDisplayViewController.m b/CleverTapSDK/CTInAppDisplayViewController.m index c15a58e0..f781f9b9 100644 --- a/CleverTapSDK/CTInAppDisplayViewController.m +++ b/CleverTapSDK/CTInAppDisplayViewController.m @@ -228,11 +228,8 @@ - (void)buttonTapped:(UIButton*)button { - (void)handleButtonClickFromIndex:(int)index { CTNotificationButton *button = self.notification.buttons[index]; - NSURL *buttonCTA = button.actionURL; NSString *buttonText = button.text; NSString *campaignId = self.notification.campaignId; - NSDictionary *buttonCustomExtras = button.customExtras; - if (campaignId == nil) { campaignId = @""; } @@ -262,24 +259,21 @@ - (void)handleButtonClickFromIndex:(int)index { return; } - if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationCTA:buttonCustomExtras:forNotification:fromViewController:withExtras:)]) { - [self.delegate handleNotificationCTA:buttonCTA buttonCustomExtras:buttonCustomExtras forNotification:self.notification fromViewController:self withExtras:@{CLTAP_NOTIFICATION_ID_TAG:campaignId, @"wzrk_c2a": buttonText}]; + if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationAction:forNotification:withExtras:)]) { + [self.delegate handleNotificationAction:button.action forNotification:self.notification withExtras:@{CLTAP_NOTIFICATION_ID_TAG:campaignId, @"wzrk_c2a": buttonText}]; } } - (void)handleImageTapGesture { CTNotificationButton *button = self.notification.buttons[0]; - NSURL *buttonCTA = button.actionURL; NSString *buttonText = @""; NSString *campaignId = self.notification.campaignId; - NSDictionary *buttonCustomExtras = button.customExtras; - if (campaignId == nil) { campaignId = @""; } - if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationCTA:buttonCustomExtras:forNotification:fromViewController:withExtras:)]) { - [self.delegate handleNotificationCTA:buttonCTA buttonCustomExtras:buttonCustomExtras forNotification:self.notification fromViewController:self withExtras:@{CLTAP_NOTIFICATION_ID_TAG:campaignId, @"wzrk_c2a": buttonText}]; + if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationAction:forNotification:withExtras:)]) { + [self.delegate handleNotificationAction:button.action forNotification:self.notification withExtras:@{CLTAP_NOTIFICATION_ID_TAG:campaignId, @"wzrk_c2a": buttonText}]; } } diff --git a/CleverTapSDK/CTInAppNotification.h b/CleverTapSDK/CTInAppNotification.h index 59c5fa26..4199c0a7 100644 --- a/CleverTapSDK/CTInAppNotification.h +++ b/CleverTapSDK/CTInAppNotification.h @@ -11,7 +11,6 @@ @property (nonatomic, readonly) NSString *Id; @property (nonatomic, readonly) NSString *campaignId; -@property (nonatomic, copy, readonly) NSString *type; @property (nonatomic, readonly) CTInAppType inAppType; @property (nonatomic, copy, readonly) NSString *html; diff --git a/CleverTapSDK/CTInAppNotification.m b/CleverTapSDK/CTInAppNotification.m index c265e355..e51d44b1 100644 --- a/CleverTapSDK/CTInAppNotification.m +++ b/CleverTapSDK/CTInAppNotification.m @@ -12,7 +12,6 @@ @interface CTInAppNotification() { @property (nonatomic, readwrite) NSString *Id; @property (nonatomic, readwrite) NSString *campaignId; -@property (nonatomic, readwrite) NSString *type; @property (nonatomic, readwrite) CTInAppType inAppType; @property (nonatomic, strong, readwrite) NSURL *imageURL; @@ -107,7 +106,7 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject { if (self.inAppType == CTInAppTypeUnknown) { self.error = @"Unknown InApp Type"; } - + NSUInteger timeToLive = [jsonObject[CLTAP_INAPP_TTL] longValue]; if (timeToLive) { _timeToLive = timeToLive; @@ -126,10 +125,7 @@ - (instancetype)initWithJSON:(NSDictionary *)jsonObject { } - (void)configureFromJSON: (NSDictionary *)jsonObject { - self.type = (NSString*) jsonObject[@"type"]; - if (self.type) { - self.inAppType = [CTInAppUtils inAppTypeFromString:self.type]; - } + self.inAppType = [CTInAppUtils inAppTypeFromString:jsonObject[@"type"]]; self.backgroundColor = jsonObject[@"bg"]; self.title = (NSString*) jsonObject[@"title"][@"text"]; self.titleColor = (NSString*) jsonObject[@"title"][@"color"]; diff --git a/CleverTapSDK/CTInAppNotificationDisplayDelegate.h b/CleverTapSDK/CTInAppNotificationDisplayDelegate.h index e0346f68..04c079d4 100644 --- a/CleverTapSDK/CTInAppNotificationDisplayDelegate.h +++ b/CleverTapSDK/CTInAppNotificationDisplayDelegate.h @@ -10,12 +10,14 @@ #define CTInAppNotificationDisplayDelegate_h @class CTInAppDisplayViewController; +@class CTInAppNotification; +@class CTNotificationAction; @protocol CTInAppNotificationDisplayDelegate - (void)notificationDidShow:(CTInAppNotification *)notification; -- (void)handleNotificationCTA:(NSURL *)ctaURL buttonCustomExtras:(NSDictionary *)buttonCustomExtras forNotification:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller withExtras:(NSDictionary *)extras; +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras; - (void)notificationDidDismiss:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller; diff --git a/CleverTapSDK/CTInAppUtils.h b/CleverTapSDK/CTInAppUtils.h index dc75b6ff..6c67df35 100644 --- a/CleverTapSDK/CTInAppUtils.h +++ b/CleverTapSDK/CTInAppUtils.h @@ -28,6 +28,7 @@ typedef NS_ENUM(NSUInteger, CTInAppActionType){ @interface CTInAppUtils : NSObject + (CTInAppType)inAppTypeFromString:(NSString *_Nonnull)type; ++ (NSString * _Nonnull)inAppTypeString:(CTInAppType)type; + (CTInAppActionType)inAppActionTypeFromString:(NSString *_Nonnull)type; + (NSString * _Nonnull)inAppActionTypeString:(CTInAppActionType)type; + (NSBundle *_Nullable)bundle; diff --git a/CleverTapSDK/CTInAppUtils.m b/CleverTapSDK/CTInAppUtils.m index adcbb907..8fab2616 100644 --- a/CleverTapSDK/CTInAppUtils.m +++ b/CleverTapSDK/CTInAppUtils.m @@ -7,12 +7,13 @@ #endif static NSDictionary *_inAppTypeMap; +static NSDictionary *_inAppTypeToStringMap; static NSDictionary *_inAppActionTypeStringToTypeMap; static NSDictionary *_inAppActionTypeTypeToStringMap; @implementation CTInAppUtils -+ (CTInAppType)inAppTypeFromString:(NSString*)type { ++ (NSDictionary *)inAppTypeStringToTypeMap { if (_inAppTypeMap == nil) { _inAppTypeMap = @{ CLTAP_INAPP_HTML_TYPE: @(CTInAppTypeHTML), @@ -28,14 +29,33 @@ + (CTInAppType)inAppTypeFromString:(NSString*)type { @"custom-code": @(CTInAppTypeCustom) }; } - - NSNumber *_type = type != nil ? _inAppTypeMap[type] : @(CTInAppTypeUnknown); + return _inAppTypeMap; +} + ++ (NSDictionary *)inAppTypeTypeToStringMap { + if (_inAppTypeToStringMap == nil) { + NSDictionary *dict = [self inAppTypeStringToTypeMap]; + NSMutableDictionary *swapped = [NSMutableDictionary new]; + [dict enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + swapped[value] = key; + }]; + _inAppTypeToStringMap = [swapped copy]; + } + return _inAppTypeToStringMap; +} + ++ (CTInAppType)inAppTypeFromString:(NSString*)type { + NSNumber *_type = type != nil ? [self inAppTypeStringToTypeMap][type] : @(CTInAppTypeUnknown); if (_type == nil) { _type = @(CTInAppTypeUnknown); } return [_type integerValue]; } ++ (NSString * _Nonnull)inAppTypeString:(CTInAppType)type { + return self.inAppTypeTypeToStringMap[@(type)]; +} + + (NSDictionary *)inAppActionTypeTypeToStringMap { if (_inAppActionTypeTypeToStringMap == nil) { NSDictionary *dict = [self inAppActionTypeStringToTypeMap]; diff --git a/CleverTapSDK/CTNotificationAction.h b/CleverTapSDK/CTNotificationAction.h index 02ded017..128e6f98 100644 --- a/CleverTapSDK/CTNotificationAction.h +++ b/CleverTapSDK/CTNotificationAction.h @@ -24,7 +24,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; #if !CLEVERTAP_NO_INAPP_SUPPORT -- (instancetype)initWithJSON:(NSDictionary*)json; +- (instancetype)initWithJSON:(NSDictionary *)json; +- (instancetype)initWithOpenURL:(NSURL *)url; #endif @end diff --git a/CleverTapSDK/CTNotificationAction.m b/CleverTapSDK/CTNotificationAction.m index 4a0b7297..6eea6643 100644 --- a/CleverTapSDK/CTNotificationAction.m +++ b/CleverTapSDK/CTNotificationAction.m @@ -47,4 +47,12 @@ - (nonnull instancetype)initWithJSON:(nonnull NSDictionary *)json { return self; } +- (nonnull instancetype)initWithOpenURL:(nonnull NSURL *)url { + if (self = [super init]) { + self.type = CTInAppActionTypeOpenURL; + self.actionURL = url; + } + return self; +} + @end diff --git a/CleverTapSDK/InApps/CTInAppDisplayManager.m b/CleverTapSDK/InApps/CTInAppDisplayManager.m index ce1b5f86..ef6cca34 100644 --- a/CleverTapSDK/InApps/CTInAppDisplayManager.m +++ b/CleverTapSDK/InApps/CTInAppDisplayManager.m @@ -36,8 +36,8 @@ #import "CleverTap+PushPermission.h" #import "CleverTapJSInterfacePrivate.h" #import "CTInAppImagePrefetchManager.h" - #import "CTCustomTemplatesManager-Internal.h" +#import "CTCustomTemplateInAppData-Internal.h" #endif #if !(TARGET_OS_TV) @@ -169,6 +169,21 @@ - (void)_addInAppNotificationsToQueue:(NSArray *)inappNotifs { } } +- (void)_addInAppNotificationInFrontOfQueue:(CTInAppNotification *)inappNotif { + @try { + NSString *templateName = inappNotif.customTemplateInAppData.templateName; + if ([self.templatesManager isRegisteredTemplateWithName:templateName]) { + [self.inAppStore insertInFrontInApp:inappNotif.jsonDescription]; + // Fire the first notification, if any + [self.dispatchQueueManager runOnNotificationQueue:^{ + [self _showNotificationIfAvailable]; + }]; + } + } @catch (NSException *e) { + CleverTapLogInternal(self.config.logLevel, @"%@: InApp notification handling error: %@", self, e.debugDescription); + } +} + - (NSArray *)filterNonRegisteredTemplates:(NSArray *)inappNotifs { NSMutableArray *filteredInAppNotifs = [NSMutableArray new]; for (NSDictionary *inAppJSON in inappNotifs) { @@ -447,8 +462,12 @@ - (void)displayNotification:(CTInAppNotification*)notification { controller = [[CTCoverImageViewController alloc] initWithNotification:notification]; break; case CTInAppTypeCustom: - currentlyDisplayingNotification = notification; - [self.templatesManager presentNotification:notification withDelegate:self]; + if ([self.templatesManager presentNotification:notification withDelegate:self]) { + currentlyDisplayingNotification = notification; + } else { + errorString = [NSString stringWithFormat:@"Cannot present custom notification with template name: %@.", + notification.customTemplateInAppData.templateName]; + } break; default: errorString = [NSString stringWithFormat:@"Unhandled notification type: %lu", (unsigned long)notification.inAppType]; @@ -546,18 +565,45 @@ - (void)notifyNotificationButtonTappedWithCustomExtras:(NSDictionary *)customExt } } -- (void)handleNotificationCTA:(NSURL *)ctaURL buttonCustomExtras:(NSDictionary *)buttonCustomExtras forNotification:(CTInAppNotification*)notification fromViewController:(CTInAppDisplayViewController*)controller withExtras:(NSDictionary*)extras { - CleverTapLogInternal(self.config.logLevel, @"%@: handle InApp cta: %@ button custom extras: %@ with options:%@", self, ctaURL.absoluteString, buttonCustomExtras, extras); +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras { + CleverTapLogInternal(self.config.logLevel, @"%@: handle InApp action type:%@ with cta: %@ button custom extras: %@ with options:%@", self, [CTInAppUtils inAppActionTypeString:action.type], action.actionURL.absoluteString, action.keyValues, extras); + // record the notification clicked event [self.instance recordInAppNotificationStateEvent:YES forNotification:notification andQueryParameters:extras]; + + // add the action extras so they can be passed to the dismissedWithExtras delegate if (extras) { notification.actionExtras = extras; } - if (buttonCustomExtras && buttonCustomExtras.count > 0) { - CleverTapLogDebug(self.config.logLevel, @"%@: InApp: button tapped with custom extras: %@", self, buttonCustomExtras); - [self notifyNotificationButtonTappedWithCustomExtras:buttonCustomExtras]; + + switch (action.type) { + case CTInAppActionTypeUnknown: + CleverTapLogDebug(self.config.logLevel, @"%@: Triggered in-app action with unknown type.", self); + break; + case CTInAppActionTypeClose: + // SDK in-apps are dismissed in CTInAppDisplayViewController buttonTapped: or tappedDismiss + if (notification.inAppType == CTInAppTypeCustom) { + [self.templatesManager closeNotification:notification]; + } + break; + case CTInAppActionTypeOpenURL: + [self handleCTAOpenURL:action.actionURL]; + break; + case CTInAppActionTypeKeyValues: + if (action.keyValues && action.keyValues.count > 0) { + CleverTapLogDebug(self.config.logLevel, @"%@: InApp: button tapped with custom extras: %@", self, action.keyValues); + [self notifyNotificationButtonTappedWithCustomExtras:action.keyValues]; + } + break; + case CTInAppActionTypeCustom: + [self triggerCustomTemplateAction:action.customTemplateInAppData forNotification:notification]; + break; + case CTInAppActionTypeRequestForPermission: + // Handled in CTInAppDisplayViewController handleButtonClickFromIndex: + break; } - else if (ctaURL) { - +} + +- (void)handleCTAOpenURL:(NSURL *)ctaURL { #if !CLEVERTAP_NO_INAPP_SUPPORT if (self.instance.urlDelegate) { // URL DELEGATE FOUND. OPEN DEEP LINKS ONLY IF USER ALLOWS IT @@ -574,8 +620,40 @@ - (void)handleNotificationCTA:(NSURL *)ctaURL buttonCustomExtras:(NSDictionary * }]; } #endif +} + +- (void)triggerCustomTemplateAction:(CTCustomTemplateInAppData *)actionData forNotification:(CTInAppNotification *)notification { + if (actionData && actionData.templateName) { + if ([self.templatesManager isRegisteredTemplateWithName:actionData.templateName]) { + CTCustomTemplateInAppData *inAppData = [actionData copy]; + [inAppData setIsAction:YES]; + CTInAppNotification *notificationFromAction = [self createNotificationForAction:inAppData andParentNotification:notification]; + + if ([self.templatesManager isVisualTemplateWithName:inAppData.templateName]) { + [self _addInAppNotificationInFrontOfQueue:notificationFromAction]; + } else { + [self.templatesManager presentNotification:notificationFromAction withDelegate:self]; + } + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: Cannot trigger non-registered template with name: %@", [self class], actionData.templateName); + } + } else { + CleverTapLogDebug(self.config.logLevel, @"%@: Cannot trigger action without template name.", [self class]); } - [controller hide:true]; +} + +- (CTInAppNotification *)createNotificationForAction:(CTCustomTemplateInAppData *)inAppData andParentNotification:(CTInAppNotification *)notification { + NSMutableDictionary *json = [@{ + CLTAP_INAPP_ID: notification.Id ? notification.Id : @"", + CLTAP_INAPP_EXCLUDE_GLOBAL_CAPS: @(YES), + CLTAP_INAPP_TYPE: [CTInAppUtils inAppTypeString:CTInAppTypeCustom], + CLTAP_PROP_WZRK_ID: notification.campaignId ? notification.campaignId : @"", + CLTAP_INAPP_TTL: @(notification.timeToLive) + } mutableCopy]; + + [json addEntriesFromDictionary:inAppData.json]; + + return [[CTInAppNotification alloc] initWithJSON:json]; } - (void)handleInAppPushPrimer:(CTInAppNotification *)notification diff --git a/CleverTapSDK/InApps/CTInAppHTMLViewController.m b/CleverTapSDK/InApps/CTInAppHTMLViewController.m index ac8961a8..48713505 100644 --- a/CleverTapSDK/InApps/CTInAppHTMLViewController.m +++ b/CleverTapSDK/InApps/CTInAppHTMLViewController.m @@ -254,11 +254,11 @@ - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigati } } } - if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationCTA:buttonCustomExtras:forNotification:fromViewController:withExtras:)]) { - [self.delegate handleNotificationCTA:dl buttonCustomExtras:nil forNotification:self.notification fromViewController:self withExtras:mutableParams]; - } else { - [self hide:YES]; + if (self.delegate && [self.delegate respondsToSelector:@selector(handleNotificationAction:forNotification:withExtras:)]) { + CTNotificationAction *action = [[CTNotificationAction alloc] initWithOpenURL:dl]; + [self.delegate handleNotificationAction:action forNotification:self.notification withExtras:mutableParams]; } + [self hide:YES]; decisionHandler(WKNavigationActionPolicyCancel); } diff --git a/CleverTapSDK/InApps/CTInAppStore.h b/CleverTapSDK/InApps/CTInAppStore.h index d5517554..a7b146cc 100644 --- a/CleverTapSDK/InApps/CTInAppStore.h +++ b/CleverTapSDK/InApps/CTInAppStore.h @@ -33,6 +33,7 @@ - (NSArray * _Nonnull)inAppsQueue; - (void)storeInApps:(NSArray * _Nullable)inApps; - (void)enqueueInApps:(NSArray * _Nullable)inAppNotifs; +- (void)insertInFrontInApp:(NSDictionary * _Nullable)inAppNotif; - (NSDictionary * _Nullable)peekInApp; - (NSDictionary * _Nullable)dequeueInApp; diff --git a/CleverTapSDK/InApps/CTInAppStore.m b/CleverTapSDK/InApps/CTInAppStore.m index 30da70ff..809e9a99 100644 --- a/CleverTapSDK/InApps/CTInAppStore.m +++ b/CleverTapSDK/InApps/CTInAppStore.m @@ -131,6 +131,16 @@ - (void)enqueueInApps:(NSArray *)inAppNotifs { } } +- (void)insertInFrontInApp:(NSDictionary *)inAppNotif { + if (!inAppNotif) return; + + @synchronized(self) { + NSMutableArray *inAppsQueue = [[NSMutableArray alloc] initWithArray:[self inAppsQueue]]; + [inAppsQueue insertObject:inAppNotif atIndex:0]; + [self storeInApps:inAppsQueue]; + } +} + - (NSDictionary *)peekInApp { @synchronized(self) { NSArray *inApps = [self inAppsQueue]; diff --git a/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m index 14570701..4657c600 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTAppFunctionBuilder.m @@ -14,7 +14,7 @@ @implementation CTAppFunctionBuilder - (nonnull instancetype)initWithIsVisual:(BOOL)isVisual { - self = [super initWithType:@"function" isVisual:isVisual allowHierarchicalNames:NO]; + self = [super initWithType:FUNCTION_TYPE isVisual:isVisual allowHierarchicalNames:NO]; return self; } diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h index c191a157..0cee8cf6 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate-Internal.h @@ -21,6 +21,7 @@ - (instancetype)initWithTemplateName:(NSString *)templateName templateType:(NSString *)templateType + isVisual:(BOOL)isVisual arguments:(NSArray *)arguments presenter:(id)presenter; diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h index 5e926a70..2fd46e65 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.h @@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN @interface CTCustomTemplate : NSObject @property (nonatomic, strong, readonly) NSString *name; +@property (nonatomic, readonly) BOOL isVisual; - (instancetype)init NS_UNAVAILABLE; diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m index d5a58b73..42d76160 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplate.m @@ -12,6 +12,7 @@ @interface CTCustomTemplate () @property (nonatomic, strong) NSString *name; +@property (nonatomic) BOOL isVisual; @property (nonatomic, strong) NSString *templateType; @property (nonatomic, strong) NSArray *arguments; @property (nonatomic, strong) id presenter; @@ -22,11 +23,13 @@ @implementation CTCustomTemplate - (instancetype)initWithTemplateName:(NSString *)templateName templateType:(NSString *)templateType + isVisual:(BOOL)isVisual arguments:(NSArray *)arguments presenter:(id)presenter { if (self = [super init]) { _name = [templateName copy]; _templateType = [templateType copy]; + _isVisual = isVisual; _arguments = arguments; _presenter = presenter; } diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h index 1fc59fe5..d4984938 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.h @@ -10,6 +10,9 @@ #import "CTTemplatePresenter.h" #import "CTCustomTemplate.h" +#define TEMPLATE_TYPE @"template" +#define FUNCTION_TYPE @"function" + NS_ASSUME_NONNULL_BEGIN @interface CTCustomTemplateBuilder : NSObject diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m index b102695f..7d6c9ae4 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateBuilder.m @@ -166,6 +166,7 @@ - (CTCustomTemplate *)build { return [[CTCustomTemplate alloc] initWithTemplateName:self.name templateType:self.templateType + isVisual:self.isVisual arguments:self.arguments presenter:self.presenter]; } diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h new file mode 100644 index 00000000..7bc538bd --- /dev/null +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData-Internal.h @@ -0,0 +1,20 @@ +// +// CTCustomTemplateInAppData-Internal.h +// CleverTapSDK +// +// Created by Nikola Zagorchev on 6.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#ifndef CTCustomTemplateInAppData_Internal_h +#define CTCustomTemplateInAppData_Internal_h + +#import "CTCustomTemplateInAppData.h" + +@interface CTCustomTemplateInAppData (Internal) + +@property (nonatomic, readwrite) BOOL isAction; + +@end + +#endif /* CTCustomTemplateInAppData_Internal_h */ diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h index 32f7e2b9..ebd40d1d 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.h @@ -16,6 +16,8 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, copy, readonly) NSString *templateId; @property (nonatomic, copy, readonly) NSString *templateDescription; @property (nonatomic, strong, readonly) NSDictionary *args; +@property (nonatomic, readonly) BOOL isAction; +@property (nonatomic, strong, readonly) NSDictionary *json; - (instancetype)init NS_UNAVAILABLE; #if !CLEVERTAP_NO_INAPP_SUPPORT diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m index e91ea082..f1e2bd70 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplateInAppData.m @@ -17,6 +17,8 @@ @interface CTCustomTemplateInAppData() @property (nonatomic, copy, readwrite) NSString *templateDescription; @property (nonatomic, strong, readwrite) NSDictionary *args; +@property (nonatomic, strong, readwrite) NSDictionary *json; + @end @implementation CTCustomTemplateInAppData @@ -24,9 +26,16 @@ @implementation CTCustomTemplateInAppData - (nonnull instancetype)initWithJSON:(nonnull NSDictionary *)json { if (self = [super init]) { @try { + self.json = json; self.templateName = json[CLTAP_INAPP_TEMPLATE_NAME]; self.templateId = json[CLTAP_INAPP_TEMPLATE_ID]; self.templateDescription = json[CLTAP_INAPP_TEMPLATE_DESCRIPTION]; + id isAction = json[@"is_action"]; + + if (isAction && [isAction isKindOfClass:[NSNumber class]]) { + self.isAction = [isAction boolValue]; + } + id vars = json[CLTAP_INAPP_VARS]; if ([vars isKindOfClass:[NSDictionary class]]) { self.args = vars; @@ -46,4 +55,24 @@ + (instancetype)createWithJSON:(nonnull NSDictionary *)json { return nil; } +- (void)setIsAction:(BOOL)isAction { + _isAction = isAction; + NSMutableDictionary *jsonMutable = [self.json mutableCopy]; + jsonMutable[@"is_action"] = @(isAction); + self.json = jsonMutable; +} + +- (id)copyWithZone:(NSZone *)zone { + CTCustomTemplateInAppData *copy = [[[self class] allocWithZone:zone] init]; + if (copy) { + copy->_templateName = [_templateName copyWithZone:zone]; + copy->_templateId = [_templateId copyWithZone:zone]; + copy->_templateDescription = [_templateDescription copyWithZone:zone]; + copy->_args = [[NSDictionary allocWithZone:zone] initWithDictionary:self.args copyItems:YES]; + copy->_json = [[NSDictionary allocWithZone:zone] initWithDictionary:self.json copyItems:YES]; + copy->_isAction = _isAction; + } + return copy; +} + @end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h index c04f6807..646b35ca 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager-Internal.h @@ -18,7 +18,10 @@ - (instancetype)initWithConfig:(CleverTapInstanceConfig *)instanceConfig; -- (void)presentNotification:(CTInAppNotification *)notification withDelegate:(id)delegate; +- (BOOL)presentNotification:(CTInAppNotification *)notification + withDelegate:(id)delegate; + +- (void)closeNotification:(CTInAppNotification *)notification; @end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h index d9a632df..9334a030 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.h @@ -8,6 +8,7 @@ #import #import "CTTemplateProducer.h" +#import "CTTemplateContext.h" NS_ASSUME_NONNULL_BEGIN @@ -18,6 +19,8 @@ NS_ASSUME_NONNULL_BEGIN - (instancetype)init NS_UNAVAILABLE; - (BOOL)isRegisteredTemplateWithName:(NSString *)name; +- (BOOL)isVisualTemplateWithName:(nonnull NSString *)name; +- (CTTemplateContext *)activeContextForTemplate:(NSString *)templateName; - (NSDictionary*)syncPayload; diff --git a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m index 049e6f25..c1407e2e 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTCustomTemplatesManager.m @@ -13,9 +13,10 @@ #import "CTInAppNotification.h" #import "CTTemplateContext-Internal.h" -@interface CTCustomTemplatesManager () +@interface CTCustomTemplatesManager () @property (nonatomic, strong) NSMutableDictionary *templates; +@property (nonatomic, strong) NSMutableDictionary *activeContexts; @end @@ -41,6 +42,7 @@ + (void)clearTemplateProducers { - (instancetype)initWithConfig:(CleverTapInstanceConfig *)instanceConfig { self = [super init]; if (self) { + self.activeContexts = [NSMutableDictionary dictionary]; self.templates = [NSMutableDictionary dictionary]; for (id producer in templateProducers) { NSSet *customTemplates = [producer defineTemplates:instanceConfig]; @@ -63,16 +65,60 @@ - (BOOL)isRegisteredTemplateWithName:(nonnull NSString *)name { return self.templates[name]; } -- (void)presentNotification:(CTInAppNotification *)notification withDelegate:(id)delegate { +- (BOOL)isVisualTemplateWithName:(nonnull NSString *)name { + return self.templates[name].isVisual; +} + +- (CTTemplateContext *)activeContextForTemplate:(NSString *)templateName { + return self.activeContexts[templateName]; +} + +- (void)onDismissContext:(CTTemplateContext *)context { + [self.activeContexts removeObjectForKey:context.templateName]; +} + +- (BOOL)presentNotification:(CTInAppNotification *)notification withDelegate:(id)delegate { CTCustomTemplate *template = self.templates[notification.customTemplateInAppData.templateName]; if (!template) { - CleverTapLogStaticDebug("%@: Template with name:%@ not registered.", self, notification.customTemplateInAppData.templateName); + CleverTapLogStaticDebug("%@: Template with name: %@ not registered.", self, notification.customTemplateInAppData.templateName); + return NO; + } + + CTTemplateContext *context = [self createTemplateContext:template withNotification:notification andDelegate:delegate]; + self.activeContexts[template.name] = context; + [template.presenter onPresent:context]; + return YES; +} + +- (CTTemplateContext *)createTemplateContext:(CTCustomTemplate *)template withNotification:(CTInAppNotification *)notification andDelegate:(id)delegate { + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:template andNotification:notification]; + [context setNotificationDelegate:delegate]; + [context setDismissDelegate:self]; + return context; +} + +- (void)closeNotification:(CTInAppNotification *)notification { + NSString *templateName = notification.customTemplateInAppData.templateName; + if (!templateName) { + CleverTapLogStaticDebug("%@: No template name set in the notification template data.", [self class]); + return; + } + + CTCustomTemplate *template = self.templates[templateName]; + if (!template) { + CleverTapLogStaticDebug("%@: Template with name: %@ not registered.", [self class], templateName); return; } - CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:template andNotification:notification]; - context.delegate = delegate; - [template.presenter onPresent:context]; + CTTemplateContext *context = [self activeContextForTemplate:templateName]; + if (!context) { + CleverTapLogStaticDebug("%@: Cannot find active context for template: %@.", [self class], templateName); + return; + } + + if (template.presenter) { + [template.presenter onCloseClicked:context]; + } } - (NSDictionary*)syncPayload { diff --git a/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m index 0cadeac0..bc0f1493 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTInAppTemplateBuilder.m @@ -13,7 +13,7 @@ @implementation CTInAppTemplateBuilder - (instancetype)init { NSSet *nullableTypes = [NSSet setWithObjects:@(CTTemplateArgumentTypeAction), nil]; - self = [super initWithType:@"template" isVisual:YES allowHierarchicalNames:YES nullableArgumentTypes:nullableTypes]; + self = [super initWithType:TEMPLATE_TYPE isVisual:YES allowHierarchicalNames:YES nullableArgumentTypes:nullableTypes]; return self; } diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h index f152c297..44646562 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext-Internal.h @@ -14,11 +14,19 @@ #import "CTTemplateContext.h" #import "CTInAppNotificationDisplayDelegate.h" +@protocol CTTemplateContextDismissDelegate + +- (void)onDismissContext:(CTTemplateContext *)context; + +@end + @interface CTTemplateContext (Internal) - (instancetype)initWithTemplate:(CTCustomTemplate *)customTemplate andNotification:(CTInAppNotification *)notification; -- (void)setDelegate:(id)delegate; +- (void)setNotificationDelegate:(id)delegate; + +- (void)setDismissDelegate:(id)delegate; @end diff --git a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m index 56096894..51453797 100644 --- a/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m +++ b/CleverTapSDK/InApps/CustomTemplates/CTTemplateContext.m @@ -12,13 +12,16 @@ #import "CTCustomTemplate-Internal.h" #import "CTNotificationAction.h" #import "CTConstants.h" +#import "CTCustomTemplateBuilder.h" @interface CTTemplateContext () @property (nonatomic) CTCustomTemplate *template; @property (nonatomic) CTInAppNotification *notification; @property (nonatomic, strong) NSDictionary *argumentValues; -@property (nonatomic) id delegate; +@property (nonatomic) id notificationDelegate; +@property (nonatomic) id dismissDelegate; +@property (nonatomic) BOOL isAction; @end @@ -30,6 +33,7 @@ - (instancetype)initWithTemplate:(CTCustomTemplate *)customTemplate andNotificat if (self = [super init]) { self.notification = notification; self.template = customTemplate; + self.isAction = notification.customTemplateInAppData.isAction; } return self; } @@ -142,25 +146,55 @@ - (NSString *)fileNamed:(NSString *)name { } - (void)presented { - if (self.delegate) { - [self.delegate notificationDidShow:self.notification]; + if (self.isAction) { + return; + } + + if (self.notificationDelegate) { + [self.notificationDelegate notificationDidShow:self.notification]; } else { CleverTapLogStaticDebug(@"%@: Cannot set template as presented.", [self class]) } } - (void)triggerActionNamed:(NSString *)name { - // TODO: add when implementing action handling + if ([self.template.templateType isEqualToString:FUNCTION_TYPE]) { + return; + } + id action = self.argumentValues[name]; - if ([action isKindOfClass:[CTNotificationAction class]]) { - + if (![action isKindOfClass:[CTNotificationAction class]]) { + CleverTapLogStaticDebug(@"%@: No argument of type action with name %@ for template %@.", + [self class], name, self.templateName); + return; + } + + if (self.notificationDelegate) { + CTNotificationAction *notificationAction = action; + NSString *campaignId = self.notification.campaignId ? self.notification.campaignId : @""; + NSString *cta = notificationAction.customTemplateInAppData.templateName ? notificationAction.customTemplateInAppData.templateName : name; + NSDictionary *extras = @{CLTAP_NOTIFICATION_ID_TAG:campaignId, @"wzrk_c2a": cta}; + [self.notificationDelegate handleNotificationAction:notificationAction forNotification:self.notification withExtras:extras]; } } - (void)dismissed { - if (self.delegate) { - [self.delegate notificationDidDismiss:self.notification fromViewController:nil]; - self.delegate = nil; + if (self.dismissDelegate) { + [self.dismissDelegate onDismissContext:self]; + self.dismissDelegate = nil; + } + + // If the context is an action and visual:false, + // it does not go through the in-app queue, so the dismiss is NOOP. + // If the context is not an action, then it goes through the in-app queue no matter + // the visual property i.e standalone function + if (self.isAction && !self.template.isVisual) { + return; + } + + if (self.notificationDelegate) { + [self.notificationDelegate notificationDidDismiss:self.notification fromViewController:nil]; + self.notificationDelegate = nil; } else { CleverTapLogStaticDebug(@"%@: Cannot set template as dismissed.", [self class]) } diff --git a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m index 3ef9948c..c668151c 100644 --- a/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m +++ b/CleverTapSDKTests/InApps/CTInAppEvaluationManagerTest.m @@ -8,7 +8,7 @@ #import #import -#import +#import "CTTemplatePresenterMock.h" #import "CTInAppEvaluationManager.h" #import "CTEventAdapter.h" #import "BaseTestCase.h" @@ -629,7 +629,7 @@ - (void)testEvaluateOnAppLaunchedServerSide { - (void)testEvaluateCustomInApps { NSMutableSet *templates = [NSMutableSet set]; - id templatePresenter = OCMProtocolMock(@protocol(CTTemplatePresenter)); + CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; [templateBuilder setName:@"Template 1"]; [templateBuilder setPresenter:templatePresenter]; diff --git a/CleverTapSDKTests/InApps/CTNotificationActionTest.m b/CleverTapSDKTests/InApps/CTNotificationActionTest.m index 442f9a48..8050bfc5 100644 --- a/CleverTapSDKTests/InApps/CTNotificationActionTest.m +++ b/CleverTapSDKTests/InApps/CTNotificationActionTest.m @@ -126,4 +126,12 @@ - (void)testInitWithNotificationButton { })); } +- (void)testInitWithOpenURL { + NSURL *url = [[NSURL alloc] initWithString:@"https://example.com/"]; + CTNotificationAction *notificationAction = [[CTNotificationAction alloc] initWithOpenURL:url]; + + XCTAssertEqual(notificationAction.type, CTInAppActionTypeOpenURL); + XCTAssertEqualObjects(notificationAction.actionURL, url); +} + @end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m index ceae4a30..54a4d483 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateInAppDataTest.m @@ -8,6 +8,7 @@ #import #import "CTCustomTemplateInAppData.h" +#import "CTCustomTemplateInAppData-Internal.h" #import "CTInAppNotification.h" #import "CTConstants.h" @@ -17,8 +18,8 @@ @interface CTCustomTemplateInAppDataTest : XCTestCase @implementation CTCustomTemplateInAppDataTest -- (void)testCreateWithJSON { - NSDictionary *json = @{ +- (NSDictionary *)jsonCustomCode { + return @{ CLTAP_INAPP_TEMPLATE_ID: @"templateId", CLTAP_INAPP_TEMPLATE_NAME: @"templateName", CLTAP_INAPP_TYPE: @"custom-code", @@ -28,7 +29,10 @@ - (void)testCreateWithJSON { @"key2": @"value2" } }; - CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:json]; +} + +- (void)testCreateWithJSON { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; XCTAssertEqualObjects(customTemplate.templateName, @"templateName"); XCTAssertEqualObjects(customTemplate.templateId, @"templateId"); @@ -93,4 +97,31 @@ - (void)testCreateFromInAppNotification { })); } +- (void)testSetIsAction { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; + [customTemplate setIsAction:YES]; + + XCTAssertEqual(customTemplate.json[@"is_action"], @(YES)); +} + +- (void)testCopy { + CTCustomTemplateInAppData *customTemplate = [CTCustomTemplateInAppData createWithJSON:self.jsonCustomCode]; + + CTCustomTemplateInAppData *copy = [customTemplate copy]; + // Verify not the same instance + XCTAssertNotEqual(customTemplate, copy); + + // Verify property values match + XCTAssertEqualObjects(customTemplate.templateId, copy.templateId); + XCTAssertEqualObjects(customTemplate.templateName, copy.templateName); + XCTAssertEqualObjects(customTemplate.templateDescription, copy.templateDescription); + XCTAssertEqualObjects(customTemplate.args, copy.args); + XCTAssertEqualObjects(customTemplate.json, copy.json); + XCTAssertEqual(customTemplate.isAction, copy.isAction); + + // Verify copied properties are not the same instance (strings are immutable) + XCTAssertNotEqual(customTemplate.args, copy.args); + XCTAssertNotEqual(customTemplate.json, copy.json); +} + @end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m index 974677fc..e546264b 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplateTest.m @@ -9,6 +9,7 @@ #import #import #import "CTCustomTemplate-Internal.h" +#import "CTCustomTemplateBuilder.h" @interface CTCustomTemplateTest : XCTestCase @@ -17,10 +18,10 @@ @interface CTCustomTemplateTest : XCTestCase @implementation CTCustomTemplateTest - (void)testEqual { - CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"template" arguments:@[] presenter:nil]; - CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"template" arguments:@[] presenter:nil]; - CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"function" arguments:@[] presenter:nil]; - CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:@"template" arguments:@[] presenter:nil]; + CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:FUNCTION_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; XCTAssertEqualObjects(template, template); XCTAssertEqualObjects(template, sameTemplate); XCTAssertEqualObjects(template, sameName); @@ -29,10 +30,10 @@ - (void)testEqual { } - (void)testHash { - CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"template" arguments:@[] presenter:nil]; - CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"template" arguments:@[] presenter:nil]; - CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:@"function" arguments:@[] presenter:nil]; - CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:@"template" arguments:@[] presenter:nil]; + CTCustomTemplate *template = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameTemplate = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *sameName = [[CTCustomTemplate alloc] initWithTemplateName:@"template" templateType:FUNCTION_TYPE isVisual:true arguments:@[] presenter:nil]; + CTCustomTemplate *differentName = [[CTCustomTemplate alloc] initWithTemplateName:@"template1" templateType:TEMPLATE_TYPE isVisual:true arguments:@[] presenter:nil]; XCTAssertEqual([template hash], [sameTemplate hash]); XCTAssertEqual([template hash], [sameName hash]); XCTAssertNotEqual([template hash], [differentName hash]); diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m index 890a846c..11c404cd 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTCustomTemplatesManagerTest.m @@ -8,13 +8,13 @@ #import #import -#import #import "CTCustomTemplatesManager-Internal.h" #import "CTCustomTemplatesManager+Tests.h" #import "CTInAppTemplateBuilder.h" #import "CTAppFunctionBuilder.h" #import "CTTemplatePresenterMock.h" #import "CTTestTemplateProducer.h" +#import "CTInAppNotificationDisplayDelegateMock.h" @interface CTCustomTemplatesManagerTest : XCTestCase @@ -75,15 +75,14 @@ - (void)testSyncPayloadComplex { CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; [CTCustomTemplatesManager registerTemplateProducer:producer]; - CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; - CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:config]; + CTCustomTemplatesManager *manager = [self templatesManager]; NSDictionary *syncPayload = [manager syncPayload]; NSDictionary *expectedPayload = @{ @"definitions": @{ @"Function 1": @{ - @"type": @"function", + @"type": FUNCTION_TYPE, @"vars": @{ @"b": @{ @"defaultValue": @0, @@ -98,7 +97,7 @@ - (void)testSyncPayloadComplex { } }, @"Template 1": @{ - @"type": @"template", + @"type": TEMPLATE_TYPE, @"vars": @{ @"a.m": @{ @"defaultValue": @"11 string", @@ -168,7 +167,7 @@ - (void)testSyncPayloadComplex { } }, @"Template 2": @{ - @"type": @"template", + @"type": TEMPLATE_TYPE, @"vars": @{ @"b": @{ @"defaultValue": @0, @@ -227,16 +226,14 @@ - (void)testSyncPayload { CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; [CTCustomTemplatesManager registerTemplateProducer:producer]; - CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; - CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:config]; + CTCustomTemplatesManager *manager = [self templatesManager]; NSDictionary *syncPayload = [manager syncPayload]; - NSDictionary *expectedPayload = @{ @"type": @"templatePayload", @"definitions": @{ @"Template 1": @{ - @"type": @"template", + @"type": TEMPLATE_TYPE, @"vars": @{ @"boolean": @{ @"defaultValue": @0, @@ -306,8 +303,7 @@ - (void)testTemplatesRegistered { CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; [CTCustomTemplatesManager registerTemplateProducer:producer]; - CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; - CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:config]; + CTCustomTemplatesManager *manager = [self templatesManager]; XCTAssertTrue([manager isRegisteredTemplateWithName:templateName1]); XCTAssertTrue([manager isRegisteredTemplateWithName:templateName2]); @@ -330,7 +326,7 @@ - (void)testTemplatesRegistered { - (void)testDuplicateTemplateNameThrows { NSMutableSet *templates = [NSMutableSet set]; CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; - [templateBuilder setName:@"Template 1"]; + [templateBuilder setName:TEMPLATE_NAME]; [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; [templates addObject:[templateBuilder build]]; CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; @@ -346,59 +342,124 @@ - (void)testPresenterOnPresent { NSMutableSet *templates = [NSMutableSet set]; CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; - [templateBuilder setName:@"Template 1"]; + [templateBuilder setName:TEMPLATE_NAME]; [templateBuilder setPresenter:templatePresenter]; [templates addObject:[templateBuilder build]]; CTTemplatePresenterMock *functionPresenter = [CTTemplatePresenterMock new]; CTInAppTemplateBuilder *functionBuilder = [CTInAppTemplateBuilder new]; - [functionBuilder setName:@"Function 1"]; + [functionBuilder setName:FUNCTION_NAME]; [functionBuilder setPresenter:functionPresenter]; [templates addObject:[functionBuilder build]]; CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; [CTCustomTemplatesManager registerTemplateProducer:producer]; - CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; - CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:config]; + CTCustomTemplatesManager *manager = [self templatesManager]; CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; - id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); - + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + [manager presentNotification:notificaton withDelegate:delegate]; XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); - XCTAssertEqual(@"Template 1", templatePresenter.onPresentContext.templateName); - + XCTAssertEqual(TEMPLATE_NAME, templatePresenter.onPresentContext.templateName); + CTInAppNotification *functionNotificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; [manager presentNotification:functionNotificaton withDelegate:delegate]; XCTAssertEqual(1, functionPresenter.onPresentInvocationsCount); - XCTAssertEqual(@"Function 1", functionPresenter.onPresentContext.templateName); + XCTAssertEqual(FUNCTION_NAME, functionPresenter.onPresentContext.templateName); } - (void)testPresenterOnPresentNonRegisteredTemplate { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + // Use the simpleFunctionNotificationJson which is not registered + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate]; + XCTAssertEqual(0, templatePresenter.onPresentInvocationsCount); +} + +- (void)testActiveContextForTemplate { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate]; + XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); + CTTemplateContext *context = [manager activeContextForTemplate:TEMPLATE_NAME]; + XCTAssertEqual(templatePresenter.onPresentContext, context); + + [context dismissed]; + XCTAssertNil([manager activeContextForTemplate:TEMPLATE_NAME]); +} + +- (void)testActiveContextForInactiveTemplate { + [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + XCTAssertNil([manager activeContextForTemplate:TEMPLATE_NAME]); +} + +- (void)testOnClose { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + id delegate = [CTInAppNotificationDisplayDelegateMock new]; + + [manager presentNotification:notificaton withDelegate:delegate]; + XCTAssertEqual(1, templatePresenter.onPresentInvocationsCount); + CTTemplateContext *context = [manager activeContextForTemplate:TEMPLATE_NAME]; + XCTAssertEqual(templatePresenter.onPresentContext, context); + + [manager closeNotification:notificaton]; + XCTAssertEqual(1, templatePresenter.onCloseInvocationsCount); + XCTAssertEqual(templatePresenter.onCloseContext, context); +} + +- (void)testOnCloseNotActiveContext { + CTTemplatePresenterMock *templatePresenter = [self registerTemplate]; + CTCustomTemplatesManager *manager = [self templatesManager]; + + // Not active context + CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleTemplateNotificationJson]]; + [manager closeNotification:notificaton]; + XCTAssertEqual(0, templatePresenter.onCloseInvocationsCount); + + // Not registered template + CTInAppNotification *notificatonNotRegistered = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; + [manager closeNotification:notificatonNotRegistered]; + XCTAssertEqual(0, templatePresenter.onCloseInvocationsCount); +} + +- (CTTemplatePresenterMock *)registerTemplate { NSMutableSet *templates = [NSMutableSet set]; CTTemplatePresenterMock *templatePresenter = [CTTemplatePresenterMock new]; CTInAppTemplateBuilder *templateBuilder = [CTInAppTemplateBuilder new]; - [templateBuilder setName:@"Template 1"]; + [templateBuilder setName:TEMPLATE_NAME]; [templateBuilder setPresenter:templatePresenter]; [templates addObject:[templateBuilder build]]; CTTestTemplateProducer *producer = [[CTTestTemplateProducer alloc] initWithTemplates:templates]; [CTCustomTemplatesManager registerTemplateProducer:producer]; + return templatePresenter; +} + +- (CTCustomTemplatesManager *)templatesManager { CleverTapInstanceConfig *config = [[CleverTapInstanceConfig alloc] initWithAccountId:@"testAccountId" accountToken:@"testAccountToken"]; CTCustomTemplatesManager *manager = [[CTCustomTemplatesManager alloc] initWithConfig:config]; - - CTInAppNotification *notificaton = [[CTInAppNotification alloc] initWithJSON:[self simpleFunctionNotificationJson]]; - id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); - - [manager presentNotification:notificaton withDelegate:delegate]; - XCTAssertEqual(0, templatePresenter.onPresentInvocationsCount); + return manager; } - (NSDictionary *)simpleTemplateNotificationJson { return @{ - @"templateName": @"Template 1", + @"templateName": TEMPLATE_NAME, @"type": @"custom-code", @"vars": @{} }; @@ -406,10 +467,13 @@ - (NSDictionary *)simpleTemplateNotificationJson { - (NSDictionary *)simpleFunctionNotificationJson { return @{ - @"templateName": @"Function 1", + @"templateName": FUNCTION_NAME, @"type": @"custom-code", @"vars": @{} }; } +static NSString * const TEMPLATE_NAME = @"Template 1"; +static NSString * const FUNCTION_NAME = @"Function 1"; + @end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h new file mode 100644 index 00000000..888df7cd --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.h @@ -0,0 +1,23 @@ +// +// CTInAppNotificationDisplayDelegateMock.h +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 5.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import +#import "CTInAppNotificationDisplayDelegate.h" +#import "CTInAppNotification.h" +#import "CTNotificationAction.h" + +NS_ASSUME_NONNULL_BEGIN + +@interface CTInAppNotificationDisplayDelegateMock : NSObject + +@property (nonatomic) void (^handleNotificationAction)(CTNotificationAction *, CTInAppNotification *, NSDictionary *); +@property (nonatomic) int handleNotificationActionInvocations; + +@end + +NS_ASSUME_NONNULL_END diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m new file mode 100644 index 00000000..e32d3316 --- /dev/null +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTInAppNotificationDisplayDelegateMock.m @@ -0,0 +1,32 @@ +// +// CTInAppNotificationDisplayDelegateMock.m +// CleverTapSDKTests +// +// Created by Nikola Zagorchev on 5.06.24. +// Copyright © 2024 CleverTap. All rights reserved. +// + +#import "CTInAppNotificationDisplayDelegateMock.h" + +@implementation CTInAppNotificationDisplayDelegateMock + +- (void)handleNotificationAction:(CTNotificationAction *)action forNotification:(CTInAppNotification *)notification withExtras:(NSDictionary *)extras { + self.handleNotificationActionInvocations++; + if (self.handleNotificationAction) { + self.handleNotificationAction(action, notification, extras); + } +} + +- (void)notificationDidDismiss:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller { +} + +- (void)notificationDidShow:(CTInAppNotification *)notification { +} + +- (void)handleInAppPushPrimer:(CTInAppNotification *)notification fromViewController:(CTInAppDisplayViewController *)controller withFallbackToSettings:(BOOL)isFallbackToSettings { +} + +- (void)inAppPushPrimerDidDismissed { +} + +@end diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m index 2fc644b4..8f7caff0 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplateContextTest.m @@ -13,10 +13,12 @@ #import "CTAppFunctionBuilder.h" #import "CTTemplatePresenterMock.h" #import "CTTemplateContext-Internal.h" +#import "CTCustomTemplateInAppData-Internal.h" +#import "CTInAppNotificationDisplayDelegateMock.h" @interface CTTemplateContext (Tests) -@property (nonatomic) id delegate; +@property (nonatomic) id notificationDelegate; @end @@ -29,7 +31,7 @@ @implementation CTTemplateContextTest - (void)testDismissedShouldCallDelegateDismiss { CTTemplateContext *context = self.templateContext; id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); - [context setDelegate:delegate]; + [context setNotificationDelegate:delegate]; [context dismissed]; [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; @@ -38,10 +40,22 @@ - (void)testDismissedShouldCallDelegateDismiss { [[delegate reject] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; } +- (void)testDismissedShouldCallDelegateDismiss2 { + CTTemplateContext *context = self.templateContext; + id delegate = OCMProtocolMock(@protocol(CTTemplateContextDismissDelegate)); + [context setDismissDelegate:delegate]; + [context dismissed]; + [[delegate verify] onDismissContext:[OCMArg any]]; + + // should call delegate dismiss only once + [context dismissed]; + [[delegate reject] onDismissContext:[OCMArg any]]; +} + - (void)testPresentedShouldCallDelegateShow { CTTemplateContext *context = self.templateContext; id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); - [context setDelegate:delegate]; + [context setNotificationDelegate:delegate]; [context presented]; [[delegate verify] notificationDidShow:[OCMArg any]]; } @@ -49,15 +63,117 @@ - (void)testPresentedShouldCallDelegateShow { - (void)testDismissClearsDelegate { CTTemplateContext *context = self.templateContext; id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); - [context setDelegate:delegate]; + [context setNotificationDelegate:delegate]; [context dismissed]; [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; - XCTAssertNil([context delegate]); + XCTAssertNil([context notificationDelegate]); + [context presented]; + [[delegate reject] notificationDidShow:[OCMArg any]]; +} + +- (void)testTriggerAction { + CTTemplateContext *context = self.templateContext; + CTInAppNotificationDisplayDelegateMock *delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeClose); + XCTAssertEqualObjects(extras[@"wzrk_c2a"], @"map.actions.close"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.close"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeCustom); + XCTAssertEqualObjects(action.customTemplateInAppData.templateName, VARS_ACTION_FUNCTION_NAME); + XCTAssertEqualObjects(extras[@"wzrk_c2a"], VARS_ACTION_FUNCTION_NAME); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.function"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeOpenURL); + XCTAssertEqualObjects(action.actionURL, [[NSURL alloc] initWithString:VARS_ACTION_OPEN_URL_ADDRESS]); + XCTAssertEqualObjects(extras[@"wzrk_c2a"], @"map.actions.openUrl"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.openUrl"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTAssertEqual(action.type, CTInAppActionTypeKeyValues); + XCTAssertEqualObjects(action.keyValues, @{ + @"key1": @"value1" + }); + XCTAssertEqualObjects(extras[@"wzrk_c2a"], @"map.actions.kv"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"map.actions.kv"]; + XCTAssertEqual(1, delegate.handleNotificationActionInvocations); + + context = self.templateContext; + delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [delegate setHandleNotificationAction:^(CTNotificationAction *action, CTInAppNotification *notification, NSDictionary *extras) { + XCTFail(@"handleNotificationAction called for non-existent action arguments"); + }]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"nonexistent"]; + XCTAssertEqual(0, delegate.handleNotificationActionInvocations); +} + +- (void)testTriggerActionNOOPForFunction { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function andNotification:notification]; + CTInAppNotificationDisplayDelegateMock *delegate = [[CTInAppNotificationDisplayDelegateMock alloc] init]; + [context setNotificationDelegate:delegate]; + [context triggerActionNamed:@"action"]; + XCTAssertEqual(0, delegate.handleNotificationActionInvocations); +} + +- (void)testDidShowNotCalledForActions { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function andNotification:notification]; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; [context presented]; [[delegate reject] notificationDidShow:[OCMArg any]]; } +- (void)testDidDismissNotCalledForActionsNotVisual { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.function andNotification:notification]; + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + id dismissDelegate = OCMProtocolMock(@protocol(CTTemplateContextDismissDelegate)); + [context setNotificationDelegate:delegate]; + [context setDismissDelegate:dismissDelegate]; + [context dismissed]; + [[dismissDelegate verify] onDismissContext:[OCMArg any]]; + [[delegate reject] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; +} + +- (void)testDidDismissCalledForActionsVisual { + CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.functionNotificationJson]; + notification.customTemplateInAppData.isAction = YES; + + CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.functionVisual andNotification:notification]; + + id delegate = OCMProtocolMock(@protocol(CTInAppNotificationDisplayDelegate)); + [context setNotificationDelegate:delegate]; + [context dismissed]; + [[delegate verify] notificationDidDismiss:[OCMArg any] fromViewController:[OCMArg any]]; +} + - (void)testTemplateName { CTInAppNotification *notification = [[CTInAppNotification alloc] initWithJSON:self.templateNotificationJson]; CTTemplateContext *context = [[CTTemplateContext alloc] initWithTemplate:self.template andNotification:notification]; @@ -168,7 +284,7 @@ - (void)verifyInnermostMap:(NSDictionary *)vars map:(NSDictionary *)map { - (CTCustomTemplate *)simpleTemplate { CTInAppTemplateBuilder *builder = [[CTInAppTemplateBuilder alloc] init]; - [builder setName:TEMPLATE_NAME]; + [builder setName:SIMPLE_TEMPLATE_NAME]; [builder addArgument:@"a.b.c" withBool:VARS_DEFAULT_BOOLEAN]; [builder addArgument:@"a.b.d" withString:VARS_DEFAULT_STRING]; [builder addArgument:@"a.b.e.f" withNumber:@(VARS_DEFAULT_LONG)]; @@ -181,7 +297,7 @@ - (CTCustomTemplate *)simpleTemplate { - (NSDictionary *)simpleTemplateNotificationJson { return @{ - @"templateName": TEMPLATE_NAME, + @"templateName": SIMPLE_TEMPLATE_NAME, @"type": @"custom-code", @"vars": @{ @"a.b.c": @(VARS_OVERRIDE_BOOLEAN), @@ -232,6 +348,7 @@ - (CTCustomTemplate *)template { [templateBuilder addActionArgument:@"map.actions.function"]; [templateBuilder addActionArgument:@"map.actions.close"]; [templateBuilder addActionArgument:@"map.actions.openUrl"]; + [templateBuilder addActionArgument:@"map.actions.kv"]; [templateBuilder setPresenter:[CTTemplatePresenterMock new]]; return [templateBuilder build]; } @@ -269,6 +386,14 @@ - (NSDictionary *)templateNotificationJson { @"ios": VARS_ACTION_OPEN_URL_ADDRESS } }, + @"map.actions.kv": @{ + @"actions": @{ + @"type": @"kv", + @"kv": @{ + @"key1": @"value1" + }, + } + }, @"map.int": @123, @"map.float": @15.6f, @"map.innerMap.boolean": @YES, @@ -284,9 +409,35 @@ - (NSDictionary *)templateNotificationJson { }; } -static NSString * const TEMPLATE_NAME = @"Template"; +- (CTCustomTemplate *)function { + CTAppFunctionBuilder *bulder = [[CTAppFunctionBuilder alloc] initWithIsVisual:NO]; + [bulder setName:FUNCTION_NAME]; + [bulder addArgument:@"string" withString:VARS_DEFAULT_STRING]; + [bulder setPresenter:[CTTemplatePresenterMock new]]; + return [bulder build]; +} + +- (CTCustomTemplate *)functionVisual { + CTAppFunctionBuilder *bulder = [[CTAppFunctionBuilder alloc] initWithIsVisual:YES]; + [bulder setName:FUNCTION_NAME]; + [bulder addArgument:@"string" withString:VARS_DEFAULT_STRING]; + [bulder setPresenter:[CTTemplatePresenterMock new]]; + return [bulder build]; +} + +- (NSDictionary *)functionNotificationJson { + return @{ + @"templateName": FUNCTION_NAME, + @"type": @"custom-code", + @"vars": @{ + @"string": VARS_OVERRIDE_STRING + } + }; +} + +static NSString * const SIMPLE_TEMPLATE_NAME = @"Template"; static NSString * const TEMPLATE_NAME_NESTED = @"TemplateNestedArgs"; -static NSString * const FUNCTION_NAME_TOP_LEVEL = @"FunctionTopLevel"; +static NSString * const FUNCTION_NAME = @"Function"; static BOOL const VARS_OVERRIDE_BOOLEAN = YES; static NSString * const VARS_OVERRIDE_STRING = @"Text"; diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h index 4d0c175c..0e7bb9cc 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.h @@ -13,6 +13,9 @@ NS_ASSUME_NONNULL_BEGIN @interface CTTemplatePresenterMock : NSObject +@property (nonatomic) int onCloseInvocationsCount; +@property (nonatomic) CTTemplateContext *onCloseContext; + @property (nonatomic) int onPresentInvocationsCount; @property (nonatomic) CTTemplateContext *onPresentContext; diff --git a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m index fc744f93..db7dad78 100644 --- a/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m +++ b/CleverTapSDKTests/InApps/CustomTemplates/CTTemplatePresenterMock.m @@ -11,6 +11,8 @@ @implementation CTTemplatePresenterMock - (void)onCloseClicked:(CTTemplateContext *)context { + self.onCloseInvocationsCount++; + self.onCloseContext = context; } - (void)onPresent:(CTTemplateContext *)context {