FIRIAMFetchResponseParser.m 15 KB

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