FIRIAMDisplayExecutor.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <FirebaseCore/FIRLogger.h>
  17. #import "FIRCore+InAppMessaging.h"
  18. #import "FIRIAMActivityLogger.h"
  19. #import "FIRIAMDisplayExecutor.h"
  20. #import "FIRIAMMessageContentData.h"
  21. #import "FIRIAMMessageDefinition.h"
  22. #import "FIRIAMSDKRuntimeErrorCodes.h"
  23. @implementation FIRIAMDisplaySetting
  24. @end
  25. @interface FIRIAMDisplayExecutor () <FIRInAppMessagingDisplayDelegate>
  26. @property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
  27. // YES if a message is being rendered at this time
  28. @property(nonatomic) BOOL isMsgBeingDisplayed;
  29. @property(nonatomic) NSTimeInterval lastDisplayTime;
  30. @property(nonatomic, nonnull, readonly) FIRIAMDisplaySetting *setting;
  31. @property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache;
  32. @property(nonatomic, nonnull, readonly) id<FIRIAMBookKeeper> displayBookKeeper;
  33. @property(nonatomic) BOOL impressionRecorded;
  34. @property(nonatomic, nonnull, readonly) id<FIRIAMAnalyticsEventLogger> analyticsEventLogger;
  35. @property(nonatomic, nonnull, readonly) FIRIAMActionURLFollower *actionURLFollower;
  36. @end
  37. @implementation FIRIAMDisplayExecutor {
  38. FIRIAMMessageDefinition *_currentMsgBeingDisplayed;
  39. }
  40. #pragma mark - FIRInAppMessagingDisplayDelegate methods
  41. - (void)messageClicked {
  42. self.isMsgBeingDisplayed = NO;
  43. if (!_currentMsgBeingDisplayed.renderData.messageID) {
  44. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400030",
  45. @"messageClicked called but "
  46. "there is no current message ID.");
  47. return;
  48. }
  49. if (_currentMsgBeingDisplayed.isTestMessage) {
  50. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400031",
  51. @"A test message clicked. Do test event impression/click analytics logging");
  52. [self.analyticsEventLogger
  53. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  54. forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
  55. withCampaignName:_currentMsgBeingDisplayed.renderData.name
  56. eventTimeInMs:nil
  57. completion:^(BOOL success) {
  58. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400036",
  59. @"Logging analytics event for url following %@",
  60. success ? @"succeeded" : @"failed");
  61. }];
  62. [self.analyticsEventLogger
  63. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
  64. forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
  65. withCampaignName:_currentMsgBeingDisplayed.renderData.name
  66. eventTimeInMs:nil
  67. completion:^(BOOL success) {
  68. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400039",
  69. @"Logging analytics event for url following %@",
  70. success ? @"succeeded" : @"failed");
  71. }];
  72. } else {
  73. // Logging the impression
  74. [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
  75. withMessageName:_currentMsgBeingDisplayed.renderData.name];
  76. [self.analyticsEventLogger
  77. logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
  78. forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
  79. withCampaignName:_currentMsgBeingDisplayed.renderData.name
  80. eventTimeInMs:nil
  81. completion:^(BOOL success) {
  82. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400032",
  83. @"Logging analytics event for url following %@",
  84. success ? @"succeeded" : @"failed");
  85. }];
  86. }
  87. NSURL *actionURL = _currentMsgBeingDisplayed.renderData.contentData.actionURL;
  88. if (!actionURL) {
  89. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400033",
  90. @"messageClicked called but "
  91. "there is no action url specified in the message data.");
  92. // it's equivalent to closing the message with no further action
  93. return;
  94. } else {
  95. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400037", @"Following action url %@",
  96. actionURL.absoluteString);
  97. @try {
  98. [self.actionURLFollower
  99. followActionURL:actionURL
  100. withCompletionBlock:^(BOOL success) {
  101. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400034",
  102. @"Seeing %@ from following action URL", success ? @"success" : @"error");
  103. }];
  104. } @catch (NSException *e) {
  105. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400035",
  106. @"Exception encountered in following "
  107. "action url (%@): %@ ",
  108. actionURL, e.description);
  109. @throw;
  110. }
  111. }
  112. }
  113. - (void)messageDismissedWithType:(FIRInAppMessagingDismissType)dismissType {
  114. self.isMsgBeingDisplayed = NO;
  115. if (!_currentMsgBeingDisplayed.renderData.messageID) {
  116. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400014",
  117. @"messageDismissedWithType called but "
  118. "there is no current message ID.");
  119. return;
  120. }
  121. if (_currentMsgBeingDisplayed.isTestMessage) {
  122. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400020",
  123. @"A test message dismissed. Record the impression event.");
  124. [self.analyticsEventLogger
  125. logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
  126. forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
  127. withCampaignName:_currentMsgBeingDisplayed.renderData.name
  128. eventTimeInMs:nil
  129. completion:^(BOOL success) {
  130. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400038",
  131. @"Logging analytics event for url following %@",
  132. success ? @"succeeded" : @"failed");
  133. }];
  134. return;
  135. }
  136. // Logging the impression
  137. [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
  138. withMessageName:_currentMsgBeingDisplayed.renderData.name];
  139. FIRIAMAnalyticsLogEventType logEventType = dismissType == FIRInAppMessagingDismissTypeAuto
  140. ? FIRIAMAnalyticsEventMessageDismissAuto
  141. : FIRIAMAnalyticsEventMessageDismissClick;
  142. [self.analyticsEventLogger
  143. logAnalyticsEventForType:logEventType
  144. forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
  145. withCampaignName:_currentMsgBeingDisplayed.renderData.name
  146. eventTimeInMs:nil
  147. completion:^(BOOL success) {
  148. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400004",
  149. @"Logging analytics event for message dismiss %@",
  150. success ? @"succeeded" : @"failed");
  151. }];
  152. }
  153. - (void)impressionDetected {
  154. if (!_currentMsgBeingDisplayed.renderData.messageID) {
  155. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400022",
  156. @"impressionDetected called but "
  157. "there is no current message ID.");
  158. return;
  159. }
  160. if (!_currentMsgBeingDisplayed.isTestMessage) {
  161. // Displayed long enough to be a valid impression.
  162. [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
  163. withMessageName:_currentMsgBeingDisplayed.renderData.name];
  164. } else {
  165. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400011",
  166. @"A test message. Record the test message impression event.");
  167. return;
  168. }
  169. }
  170. - (void)displayErrorEncountered:(NSError *)error {
  171. self.isMsgBeingDisplayed = NO;
  172. if (!_currentMsgBeingDisplayed.renderData.messageID) {
  173. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400017",
  174. @"displayErrorEncountered called but "
  175. "there is no current message ID.");
  176. return;
  177. }
  178. NSString *messageID = _currentMsgBeingDisplayed.renderData.messageID;
  179. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400009",
  180. @"Display ran into error for message %@: %@", messageID, error);
  181. if (_currentMsgBeingDisplayed.isTestMessage) {
  182. [self displayMessageLoadError:error];
  183. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400012",
  184. @"A test message. No analytics tracking "
  185. "from image data loading failure");
  186. return;
  187. }
  188. // we remove the message from the client side cache so that it won't be retried until next time
  189. // it's fetched again from server.
  190. [self.messageCache removeMessageWithId:messageID];
  191. NSString *messageName = _currentMsgBeingDisplayed.renderData.name;
  192. if ([error.domain isEqualToString:NSURLErrorDomain]) {
  193. [self.analyticsEventLogger
  194. logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError
  195. forCampaignID:messageID
  196. withCampaignName:messageName
  197. eventTimeInMs:nil
  198. completion:^(BOOL success) {
  199. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400010",
  200. @"Logging analytics event for image fetch error %@",
  201. success ? @"succeeded" : @"failed");
  202. }];
  203. } else if (error.code == FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL) {
  204. [self.analyticsEventLogger
  205. logAnalyticsEventForType:FIRIAMAnalyticsEventImageFormatUnsupported
  206. forCampaignID:messageID
  207. withCampaignName:messageName
  208. eventTimeInMs:nil
  209. completion:^(BOOL success) {
  210. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400013",
  211. @"Logging analytics event for image format error %@",
  212. success ? @"succeeded" : @"failed");
  213. }];
  214. }
  215. }
  216. - (void)recordValidImpression:(NSString *)messageID withMessageName:(NSString *)messageName {
  217. if (!self.impressionRecorded) {
  218. [self.displayBookKeeper recordNewImpressionForMessage:messageID
  219. withStartTimestampInSeconds:self.lastDisplayTime];
  220. self.impressionRecorded = YES;
  221. [self.messageCache removeMessageWithId:messageID];
  222. // Log an impression analytics event as well.
  223. [self.analyticsEventLogger
  224. logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
  225. forCampaignID:messageID
  226. withCampaignName:messageName
  227. eventTimeInMs:nil
  228. completion:^(BOOL success) {
  229. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400007",
  230. @"Logging analytics event for impression %@",
  231. success ? @"succeeded" : @"failed");
  232. }];
  233. }
  234. }
  235. - (void)displayMessageLoadError:(NSError *)error {
  236. NSString *errorMsg = error.userInfo[NSLocalizedDescriptionKey]
  237. ? error.userInfo[NSLocalizedDescriptionKey]
  238. : @"Message loading failed";
  239. UIAlertController *alert = [UIAlertController
  240. alertControllerWithTitle:@"Firebase InAppMessaging fail to load a test message"
  241. message:errorMsg
  242. preferredStyle:UIAlertControllerStyleAlert];
  243. UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK"
  244. style:UIAlertActionStyleDefault
  245. handler:^(UIAlertAction *action){
  246. }];
  247. [alert addAction:defaultAction];
  248. [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert
  249. animated:YES
  250. completion:nil];
  251. }
  252. - (instancetype)initWithSetting:(FIRIAMDisplaySetting *)setting
  253. messageCache:(FIRIAMMessageClientCache *)cache
  254. timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
  255. bookKeeper:(id<FIRIAMBookKeeper>)displayBookKeeper
  256. actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower
  257. activityLogger:(FIRIAMActivityLogger *)activityLogger
  258. analyticsEventLogger:(id<FIRIAMAnalyticsEventLogger>)analyticsEventLogger {
  259. if (self = [super init]) {
  260. _timeFetcher = timeFetcher;
  261. _lastDisplayTime = displayBookKeeper.lastDisplayTime;
  262. _setting = setting;
  263. _messageCache = cache;
  264. _displayBookKeeper = displayBookKeeper;
  265. _isMsgBeingDisplayed = NO;
  266. _analyticsEventLogger = analyticsEventLogger;
  267. _actionURLFollower = actionURLFollower;
  268. _suppressMessageDisplay = NO; // always allow message display on startup
  269. }
  270. return self;
  271. }
  272. - (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName {
  273. // synchronizing on self so that we won't potentially enter the render flow from two
  274. // threads: example like showing analytics triggered message and a regular app open
  275. // triggered message
  276. @synchronized(self) {
  277. if (self.suppressMessageDisplay) {
  278. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400015",
  279. @"Message display is being suppressed. No contextual message rendering.");
  280. return;
  281. }
  282. if (!self.messageDisplayComponent) {
  283. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400026",
  284. @"Message display component is not present yet. No display should happen.");
  285. return;
  286. }
  287. if (self.isMsgBeingDisplayed) {
  288. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400008",
  289. @"An in-app message display is in progress, do not check analytics event "
  290. "based message for now.");
  291. return;
  292. }
  293. // Pop up next analytics event based message to be displayed
  294. FIRIAMMessageDefinition *nextAnalyticsBasedMessage =
  295. [self.messageCache nextOnFirebaseAnalyticEventDisplayMsg:eventName];
  296. if (nextAnalyticsBasedMessage) {
  297. [self displayForMessage:nextAnalyticsBasedMessage];
  298. }
  299. }
  300. }
  301. - (FIRInAppMessagingBannerDisplay *)
  302. bannerMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
  303. imageData:(FIRInAppMessagingImageData *)imageData {
  304. NSString *title = definition.renderData.contentData.titleText;
  305. NSString *body = definition.renderData.contentData.bodyText;
  306. FIRInAppMessagingBannerDisplay *bannerMessage = [[FIRInAppMessagingBannerDisplay alloc]
  307. initWithMessageID:definition.renderData.messageID
  308. renderAsTestMessage:definition.isTestMessage
  309. titleText:title
  310. bodyText:body
  311. textColor:definition.renderData.renderingEffectSettings.textColor
  312. backgroundColor:definition.renderData.renderingEffectSettings.displayBGColor
  313. imageData:imageData];
  314. return bannerMessage;
  315. }
  316. - (FIRInAppMessagingImageOnlyDisplay *)
  317. imageOnlyMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
  318. imageData:(FIRInAppMessagingImageData *)imageData {
  319. FIRInAppMessagingImageOnlyDisplay *imageOnlyMessage =
  320. [[FIRInAppMessagingImageOnlyDisplay alloc] initWithMessageID:definition.renderData.messageID
  321. renderAsTestMessage:definition.isTestMessage
  322. imageData:imageData];
  323. return imageOnlyMessage;
  324. }
  325. - (FIRInAppMessagingModalDisplay *)
  326. modalViewMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
  327. imageData:(FIRInAppMessagingImageData *)imageData {
  328. // For easier reference in this method.
  329. FIRIAMMessageRenderData *renderData = definition.renderData;
  330. NSString *title = renderData.contentData.titleText;
  331. NSString *body = renderData.contentData.bodyText;
  332. FIRInAppMessagingActionButton *actionButton = nil;
  333. if (definition.renderData.contentData.actionButtonText) {
  334. actionButton = [[FIRInAppMessagingActionButton alloc]
  335. initWithButtonText:renderData.contentData.actionButtonText
  336. buttonTextColor:renderData.renderingEffectSettings.btnTextColor
  337. backgroundColor:renderData.renderingEffectSettings.btnBGColor];
  338. }
  339. FIRInAppMessagingModalDisplay *modalViewMessage = [[FIRInAppMessagingModalDisplay alloc]
  340. initWithMessageID:definition.renderData.messageID
  341. renderAsTestMessage:definition.isTestMessage
  342. titleText:title
  343. bodyText:body
  344. textColor:renderData.renderingEffectSettings.textColor
  345. backgroundColor:renderData.renderingEffectSettings.displayBGColor
  346. imageData:imageData
  347. actionButton:actionButton];
  348. return modalViewMessage;
  349. }
  350. - (FIRInAppMessagingDisplayMessageBase *)
  351. displayMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
  352. imageData:(FIRInAppMessagingImageData *)imageData {
  353. switch (definition.renderData.renderingEffectSettings.viewMode) {
  354. case FIRIAMRenderAsBannerView:
  355. return [self bannerMessageWithMessageDefinition:definition imageData:imageData];
  356. case FIRIAMRenderAsModalView:
  357. return [self modalViewMessageWithMessageDefinition:definition imageData:imageData];
  358. case FIRIAMRenderAsImageOnlyView:
  359. return [self imageOnlyMessageWithMessageDefinition:definition imageData:imageData];
  360. default:
  361. return nil;
  362. }
  363. }
  364. - (void)displayForMessage:(FIRIAMMessageDefinition *)message {
  365. _currentMsgBeingDisplayed = message;
  366. [message.renderData.contentData
  367. loadImageDataWithBlock:^(NSData *_Nullable imageNSData, NSError *error) {
  368. FIRInAppMessagingImageData *imageData = nil;
  369. if (error) {
  370. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400019",
  371. @"Error in loading image data for the message.");
  372. // short-circuit to display error handling
  373. [self displayErrorEncountered:error];
  374. return;
  375. } else if (imageNSData != nil) {
  376. imageData = [[FIRInAppMessagingImageData alloc]
  377. initWithImageURL:message.renderData.contentData.imageURL.absoluteString
  378. imageData:imageNSData];
  379. }
  380. self.impressionRecorded = NO;
  381. self.isMsgBeingDisplayed = YES;
  382. FIRInAppMessagingDisplayMessageBase *displayMessage =
  383. [self displayMessageWithMessageDefinition:message imageData:imageData];
  384. [self.messageDisplayComponent displayMessage:displayMessage displayDelegate:self];
  385. }];
  386. }
  387. - (BOOL)enoughIntervalFromLastDisplay {
  388. NSTimeInterval intervalFromLastDisplayInSeconds =
  389. [self.timeFetcher currentTimestampInSeconds] - self.lastDisplayTime;
  390. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400005",
  391. @"Interval time from last display is %lf seconds", intervalFromLastDisplayInSeconds);
  392. return intervalFromLastDisplayInSeconds >= self.setting.displayMinIntervalInMinutes * 60.0;
  393. }
  394. - (void)checkAndDisplayNextAppForegroundMessage {
  395. // synchronizing on self so that we won't potentially enter the render flow from two
  396. // threads: example like showing analytics triggered message and a regular app open
  397. // triggered message concurrently
  398. @synchronized(self) {
  399. if (!self.messageDisplayComponent) {
  400. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400027",
  401. @"Message display component is not present yet. No display should happen.");
  402. return;
  403. }
  404. if (self.suppressMessageDisplay) {
  405. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400016",
  406. @"Message display is being suppressed. No regular message rendering.");
  407. return;
  408. }
  409. if (self.isMsgBeingDisplayed) {
  410. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400002",
  411. @"An in-app message display is in progress, do not over-display on top of it.");
  412. return;
  413. }
  414. if ([self.messageCache hasTestMessage] || [self enoughIntervalFromLastDisplay]) {
  415. // We can display test messages anytime or display regular messages when
  416. // the display time interval has been reached
  417. FIRIAMMessageDefinition *nextForegroundMessage = [self.messageCache nextOnAppOpenDisplayMsg];
  418. if (nextForegroundMessage) {
  419. [self displayForMessage:nextForegroundMessage];
  420. self.lastDisplayTime = [self.timeFetcher currentTimestampInSeconds];
  421. } else {
  422. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400001",
  423. @"No appropriate in-app message detected for display.");
  424. }
  425. } else {
  426. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400003",
  427. @"Minimal display interval of %lf seconds has not been reached yet.",
  428. self.setting.displayMinIntervalInMinutes * 60.0);
  429. }
  430. }
  431. }
  432. @end