FIRIAMFetchResponseParser.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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 <TargetConditionals.h>
  17. #if TARGET_OS_IOS || TARGET_OS_TV
  18. #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
  19. #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
  20. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
  21. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentData.h"
  22. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentDataWithImageURL.h"
  23. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h"
  24. #import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h"
  25. #import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h"
  26. #import "FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.h"
  27. #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
  28. @interface FIRIAMFetchResponseParser ()
  29. @property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
  30. @end
  31. @implementation FIRIAMFetchResponseParser
  32. - (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
  33. if (self = [super init]) {
  34. _timeFetcher = timeFetcher;
  35. }
  36. return self;
  37. }
  38. - (NSArray<FIRIAMMessageDefinition *> *)parseAPIResponseDictionary:(NSDictionary *)responseDict
  39. discardedMsgCount:(NSInteger *)discardCount
  40. fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime {
  41. if (fetchWaitTime != nil) {
  42. *fetchWaitTime = nil; // It would be set to non nil value if it's detected in responseDict
  43. if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) {
  44. NSTimeInterval nextFetchTimeInResponse =
  45. [responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000;
  46. NSTimeInterval fetchWaitTimeInSeconds =
  47. nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds];
  48. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005",
  49. @"Detected next fetch epoch time in API response as %f seconds and wait for %f "
  50. "seconds before next fetch.",
  51. nextFetchTimeInResponse, fetchWaitTimeInSeconds);
  52. if (fetchWaitTimeInSeconds > 0.01) {
  53. *fetchWaitTime = @(fetchWaitTimeInSeconds);
  54. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018",
  55. @"Fetch wait time calculated from server response is negative. Discard it.");
  56. }
  57. } else {
  58. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014",
  59. @"No fetch epoch time detected in API response.");
  60. }
  61. }
  62. NSArray<NSDictionary *> *messageArray = responseDict[@"messages"];
  63. NSInteger discarded = 0;
  64. NSMutableArray<FIRIAMMessageDefinition *> *definitions = [[NSMutableArray alloc] init];
  65. for (NSDictionary *nextMsg in messageArray) {
  66. FIRIAMMessageDefinition *nextDefinition =
  67. [self convertToMessageDefinitionWithMessageDict:nextMsg];
  68. if (nextDefinition) {
  69. [definitions addObject:nextDefinition];
  70. } else {
  71. FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001",
  72. @"No definition generated for message node %@", nextMsg);
  73. discarded++;
  74. }
  75. }
  76. FIRLogDebug(
  77. kFIRLoggerInAppMessaging, @"I-IAM900002",
  78. @"%lu message definitions were parsed out successfully and %lu messages are discarded",
  79. (unsigned long)definitions.count, (unsigned long)discarded);
  80. if (discardCount) {
  81. *discardCount = discarded;
  82. }
  83. return [definitions copy];
  84. }
  85. // Return nil if no valid triggering condition can be detected
  86. - (NSArray<FIRIAMDisplayTriggerDefinition *> *)parseTriggeringCondition:
  87. (NSArray<NSDictionary *> *)triggerConditions {
  88. if (triggerConditions == nil || triggerConditions.count == 0) {
  89. return nil;
  90. }
  91. NSMutableArray<FIRIAMDisplayTriggerDefinition *> *triggers = [[NSMutableArray alloc] init];
  92. for (NSDictionary *nextTriggerCondition in triggerConditions) {
  93. // Handle app_launch and on_foreground cases.
  94. if (nextTriggerCondition[@"fiamTrigger"]) {
  95. if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) {
  96. [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]];
  97. } else if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"APP_LAUNCH"]) {
  98. [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppLaunchTrigger]];
  99. }
  100. } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) {
  101. NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"];
  102. if (triggeringEvent[@"name"]) {
  103. [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc]
  104. initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]];
  105. }
  106. }
  107. }
  108. return [triggers copy];
  109. }
  110. // For one element in the restful API response's messages array, convert into
  111. // a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned.
  112. - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode {
  113. @try {
  114. BOOL isTestMessage = NO;
  115. id isTestCampaignNode = messageNode[@"isTestCampaign"];
  116. if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) {
  117. isTestMessage = [isTestCampaignNode boolValue];
  118. }
  119. id payloadNode = messageNode[@"experimentalPayload"] ?: messageNode[@"vanillaPayload"];
  120. if (![payloadNode isKindOfClass:[NSDictionary class]]) {
  121. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012",
  122. @"Message payload does not exist or does not represent a dictionary in "
  123. "message node %@",
  124. messageNode);
  125. return nil;
  126. }
  127. NSString *messageID = payloadNode[@"campaignId"];
  128. if (!messageID) {
  129. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010",
  130. @"messsage id is missing in message node %@", messageNode);
  131. return nil;
  132. }
  133. NSString *messageName = payloadNode[@"campaignName"];
  134. if (!messageName && !isTestMessage) {
  135. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011",
  136. @"campaign name is missing in non-test message node %@", messageNode);
  137. return nil;
  138. }
  139. ABTExperimentPayload *experimentPayload = nil;
  140. NSDictionary *experimentPayloadDictionary = payloadNode[@"experimentPayload"];
  141. if (experimentPayloadDictionary) {
  142. experimentPayload =
  143. [[ABTExperimentPayload alloc] initWithDictionary:experimentPayloadDictionary];
  144. }
  145. NSTimeInterval startTimeInSeconds = 0;
  146. NSTimeInterval endTimeInSeconds = 0;
  147. if (!isTestMessage) {
  148. // Parsing start/end times out of non-test messages. They are strings in the
  149. // json response.
  150. id startTimeNode = payloadNode[@"campaignStartTimeMillis"];
  151. if ([startTimeNode isKindOfClass:[NSString class]]) {
  152. startTimeInSeconds = [startTimeNode doubleValue] / 1000.0;
  153. }
  154. id endTimeNode = payloadNode[@"campaignEndTimeMillis"];
  155. if ([endTimeNode isKindOfClass:[NSString class]]) {
  156. endTimeInSeconds = [endTimeNode doubleValue] / 1000.0;
  157. }
  158. }
  159. id contentNode = messageNode[@"content"];
  160. if (![contentNode isKindOfClass:[NSDictionary class]]) {
  161. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013",
  162. @"content node does not exist or does not represent a dictionary in "
  163. "message node %@",
  164. messageNode);
  165. return nil;
  166. }
  167. NSDictionary *content = (NSDictionary *)contentNode;
  168. FIRIAMRenderingMode mode;
  169. UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
  170. *titleTextColor;
  171. viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
  172. NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
  173. *secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
  174. title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
  175. secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
  176. // TODO: Refactor this giant if-else block into separate parsing methods per message type.
  177. if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
  178. NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
  179. mode = FIRIAMRenderAsBannerView;
  180. title = bannerNode[@"title"][@"text"];
  181. titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]];
  182. body = bannerNode[@"body"][@"text"];
  183. imageURLStr = bannerNode[@"imageUrl"];
  184. actionURLStr = bannerNode[@"action"][@"actionUrl"];
  185. viewCardBackgroundColor =
  186. [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
  187. } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
  188. mode = FIRIAMRenderAsModalView;
  189. NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
  190. title = modalNode[@"title"][@"text"];
  191. titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]];
  192. body = modalNode[@"body"][@"text"];
  193. imageURLStr = modalNode[@"imageUrl"];
  194. actionButtonText = modalNode[@"actionButton"][@"text"][@"text"];
  195. btnBgColor =
  196. [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]];
  197. actionURLStr = modalNode[@"action"][@"actionUrl"];
  198. viewCardBackgroundColor =
  199. [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
  200. } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
  201. mode = FIRIAMRenderAsImageOnlyView;
  202. NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
  203. imageURLStr = imageOnlyNode[@"imageUrl"];
  204. if (!imageURLStr) {
  205. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
  206. @"Image url is missing for image-only message %@", messageNode);
  207. return nil;
  208. }
  209. actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
  210. } else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
  211. mode = FIRIAMRenderAsCardView;
  212. NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
  213. title = cardNode[@"title"][@"text"];
  214. titleTextColor = [UIColor firiam_colorWithHexString:cardNode[@"title"][@"hexColor"]];
  215. body = cardNode[@"body"][@"text"];
  216. imageURLStr = cardNode[@"portraitImageUrl"];
  217. landscapeImageURLStr = cardNode[@"landscapeImageUrl"];
  218. viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
  219. actionButtonText = cardNode[@"primaryActionButton"][@"text"][@"text"];
  220. btnTxtColor = [UIColor
  221. firiam_colorWithHexString:cardNode[@"primaryActionButton"][@"text"][@"hexColor"]];
  222. secondaryActionButtonText = cardNode[@"secondaryActionButton"][@"text"][@"text"];
  223. secondaryBtnTxtColor = [UIColor
  224. firiam_colorWithHexString:cardNode[@"secondaryActionButton"][@"text"][@"hexColor"]];
  225. actionURLStr = cardNode[@"primaryAction"][@"actionUrl"];
  226. secondaryActionURLStr = cardNode[@"secondaryAction"][@"actionUrl"];
  227. } else {
  228. // Unknown message type
  229. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
  230. @"Unknown message type in message node %@", messageNode);
  231. return nil;
  232. }
  233. if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
  234. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
  235. @"Title text is missing in message node %@", messageNode);
  236. return nil;
  237. }
  238. NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr];
  239. NSURL *landscapeImageURL =
  240. (landscapeImageURLStr.length == 0) ? nil : [NSURL URLWithString:landscapeImageURLStr];
  241. NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr];
  242. NSURL *secondaryActionURL =
  243. (secondaryActionURLStr.length == 0) ? nil : [NSURL URLWithString:secondaryActionURLStr];
  244. FIRIAMRenderingEffectSetting *renderEffect =
  245. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  246. renderEffect.viewMode = mode;
  247. if (viewCardBackgroundColor) {
  248. renderEffect.displayBGColor = viewCardBackgroundColor;
  249. }
  250. if (btnBgColor) {
  251. renderEffect.btnBGColor = btnBgColor;
  252. }
  253. if (btnTxtColor) {
  254. renderEffect.btnTextColor = btnTxtColor;
  255. }
  256. if (secondaryBtnTxtColor) {
  257. renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
  258. }
  259. if (titleTextColor) {
  260. renderEffect.textColor = titleTextColor;
  261. }
  262. NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
  263. [self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
  264. if (isTestMessage) {
  265. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
  266. @"A test message with id %@ was parsed successfully.", messageID);
  267. renderEffect.isTestMessage = YES;
  268. } else {
  269. // Triggering definitions should always be present for a non-test message.
  270. if (!triggersDefinition || triggersDefinition.count == 0) {
  271. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
  272. @"No valid triggering condition is detected in message definition"
  273. " with id %@",
  274. messageID);
  275. return nil;
  276. }
  277. }
  278. FIRIAMMessageContentDataWithImageURL *msgData =
  279. [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
  280. messageBody:body
  281. actionButtonText:actionButtonText
  282. secondaryActionButtonText:secondaryActionButtonText
  283. actionURL:actionURL
  284. secondaryActionURL:secondaryActionURL
  285. imageURL:imageURL
  286. landscapeImageURL:landscapeImageURL
  287. usingURLSession:nil];
  288. FIRIAMMessageRenderData *renderData =
  289. [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
  290. messageName:messageName
  291. contentData:msgData
  292. renderingEffect:renderEffect];
  293. NSDictionary *dataBundle = nil;
  294. id dataBundleNode = messageNode[@"dataBundle"];
  295. if ([dataBundleNode isKindOfClass:[NSDictionary class]]) {
  296. dataBundle = dataBundleNode;
  297. }
  298. if (isTestMessage) {
  299. return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData
  300. experimentPayload:experimentPayload];
  301. } else {
  302. return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
  303. startTime:startTimeInSeconds
  304. endTime:endTimeInSeconds
  305. triggerDefinition:triggersDefinition
  306. appData:dataBundle
  307. experimentPayload:experimentPayload
  308. isTestMessage:NO];
  309. }
  310. } @catch (NSException *e) {
  311. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
  312. @"Error in parsing message node %@ "
  313. "with error %@",
  314. messageNode, e);
  315. return nil;
  316. }
  317. }
  318. @end
  319. #endif // TARGET_OS_IOS || TARGET_OS_TV