FIRIAMFetchResponseParser.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  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. if (nextTriggerCondition[@"fiamTrigger"]) {
  91. if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) {
  92. [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]];
  93. }
  94. } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) {
  95. NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"];
  96. if (triggeringEvent[@"name"]) {
  97. [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc]
  98. initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]];
  99. }
  100. }
  101. }
  102. return [triggers copy];
  103. }
  104. // For one element in the restful API response's messages array, convert into
  105. // a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned.
  106. - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode {
  107. @try {
  108. BOOL isTestMessage = NO;
  109. id isTestCampaignNode = messageNode[@"isTestCampaign"];
  110. if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) {
  111. isTestMessage = [isTestCampaignNode boolValue];
  112. }
  113. id vanillaPayloadNode = messageNode[@"vanillaPayload"];
  114. if (![vanillaPayloadNode isKindOfClass:[NSDictionary class]]) {
  115. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012",
  116. @"vanillaPayload does not exist or does not represent a dictionary in "
  117. "message node %@",
  118. messageNode);
  119. return nil;
  120. }
  121. NSString *messageID = vanillaPayloadNode[@"campaignId"];
  122. if (!messageID) {
  123. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010",
  124. @"messsage id is missing in message node %@", messageNode);
  125. return nil;
  126. }
  127. NSString *messageName = vanillaPayloadNode[@"campaignName"];
  128. if (!messageName && !isTestMessage) {
  129. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011",
  130. @"campaign name is missing in non-test message node %@", messageNode);
  131. return nil;
  132. }
  133. NSTimeInterval startTimeInSeconds = 0;
  134. NSTimeInterval endTimeInSeconds = 0;
  135. if (!isTestMessage) {
  136. // Parsing start/end times out of non-test messages. They are strings in the
  137. // json response.
  138. id startTimeNode = vanillaPayloadNode[@"campaignStartTimeMillis"];
  139. if ([startTimeNode isKindOfClass:[NSString class]]) {
  140. startTimeInSeconds = [startTimeNode doubleValue] / 1000.0;
  141. }
  142. id endTimeNode = vanillaPayloadNode[@"campaignEndTimeMillis"];
  143. if ([endTimeNode isKindOfClass:[NSString class]]) {
  144. endTimeInSeconds = [endTimeNode doubleValue] / 1000.0;
  145. }
  146. }
  147. id contentNode = messageNode[@"content"];
  148. if (![contentNode isKindOfClass:[NSDictionary class]]) {
  149. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013",
  150. @"content node does not exist or does not represent a dictionary in "
  151. "message node %@",
  152. messageNode);
  153. return nil;
  154. }
  155. NSDictionary *content = (NSDictionary *)contentNode;
  156. FIRIAMRenderingMode mode;
  157. UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
  158. *titleTextColor;
  159. viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
  160. NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
  161. *secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
  162. title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
  163. secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
  164. // TODO: Refactor this giant if-else block into separate parsing methods per message type.
  165. if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
  166. NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
  167. mode = FIRIAMRenderAsBannerView;
  168. title = bannerNode[@"title"][@"text"];
  169. titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]];
  170. body = bannerNode[@"body"][@"text"];
  171. imageURLStr = bannerNode[@"imageUrl"];
  172. actionURLStr = bannerNode[@"action"][@"actionUrl"];
  173. viewCardBackgroundColor =
  174. [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
  175. } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
  176. mode = FIRIAMRenderAsModalView;
  177. NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
  178. title = modalNode[@"title"][@"text"];
  179. titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]];
  180. body = modalNode[@"body"][@"text"];
  181. imageURLStr = modalNode[@"imageUrl"];
  182. actionButtonText = modalNode[@"actionButton"][@"text"][@"text"];
  183. btnBgColor =
  184. [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]];
  185. actionURLStr = modalNode[@"action"][@"actionUrl"];
  186. viewCardBackgroundColor =
  187. [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
  188. } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
  189. mode = FIRIAMRenderAsImageOnlyView;
  190. NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
  191. imageURLStr = imageOnlyNode[@"imageUrl"];
  192. if (!imageURLStr) {
  193. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
  194. @"Image url is missing for image-only message %@", messageNode);
  195. return nil;
  196. }
  197. actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
  198. } else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
  199. mode = FIRIAMRenderAsCardView;
  200. NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
  201. title = cardNode[@"title"][@"text"];
  202. titleTextColor = [UIColor firiam_colorWithHexString:cardNode[@"title"][@"hexColor"]];
  203. body = cardNode[@"body"][@"text"];
  204. imageURLStr = cardNode[@"portraitImageUrl"];
  205. landscapeImageURLStr = cardNode[@"landscapeImageUrl"];
  206. viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
  207. actionButtonText = cardNode[@"primaryActionButton"][@"text"][@"text"];
  208. btnTxtColor = [UIColor
  209. firiam_colorWithHexString:cardNode[@"primaryActionButton"][@"text"][@"hexColor"]];
  210. secondaryActionButtonText = cardNode[@"secondaryActionButton"][@"text"][@"text"];
  211. secondaryBtnTxtColor = [UIColor
  212. firiam_colorWithHexString:cardNode[@"secondaryActionButton"][@"text"][@"hexColor"]];
  213. actionURLStr = cardNode[@"primaryAction"][@"actionUrl"];
  214. secondaryActionURLStr = cardNode[@"secondaryAction"][@"actionUrl"];
  215. } else {
  216. // Unknown message type
  217. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
  218. @"Unknown message type in message node %@", messageNode);
  219. return nil;
  220. }
  221. if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
  222. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
  223. @"Title text is missing in message node %@", messageNode);
  224. return nil;
  225. }
  226. NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr];
  227. NSURL *landscapeImageURL =
  228. (landscapeImageURLStr.length == 0) ? nil : [NSURL URLWithString:landscapeImageURLStr];
  229. NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr];
  230. NSURL *secondaryActionURL =
  231. (secondaryActionURLStr.length == 0) ? nil : [NSURL URLWithString:secondaryActionURLStr];
  232. FIRIAMRenderingEffectSetting *renderEffect =
  233. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  234. renderEffect.viewMode = mode;
  235. if (viewCardBackgroundColor) {
  236. renderEffect.displayBGColor = viewCardBackgroundColor;
  237. }
  238. if (btnBgColor) {
  239. renderEffect.btnBGColor = btnBgColor;
  240. }
  241. if (btnTxtColor) {
  242. renderEffect.btnTextColor = btnTxtColor;
  243. }
  244. if (secondaryBtnTxtColor) {
  245. renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
  246. }
  247. if (titleTextColor) {
  248. renderEffect.textColor = titleTextColor;
  249. }
  250. NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
  251. [self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
  252. if (isTestMessage) {
  253. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
  254. @"A test message with id %@ was parsed successfully.", messageID);
  255. renderEffect.isTestMessage = YES;
  256. } else {
  257. // Triggering definitions should always be present for a non-test message.
  258. if (!triggersDefinition || triggersDefinition.count == 0) {
  259. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
  260. @"No valid triggering condition is detected in message definition"
  261. " with id %@",
  262. messageID);
  263. return nil;
  264. }
  265. }
  266. FIRIAMMessageContentDataWithImageURL *msgData =
  267. [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
  268. messageBody:body
  269. actionButtonText:actionButtonText
  270. secondaryActionButtonText:secondaryActionButtonText
  271. actionURL:actionURL
  272. secondaryActionURL:secondaryActionURL
  273. imageURL:imageURL
  274. landscapeImageURL:landscapeImageURL
  275. usingURLSession:nil];
  276. FIRIAMMessageRenderData *renderData =
  277. [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
  278. messageName:messageName
  279. contentData:msgData
  280. renderingEffect:renderEffect];
  281. if (isTestMessage) {
  282. return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData];
  283. } else {
  284. return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
  285. startTime:startTimeInSeconds
  286. endTime:endTimeInSeconds
  287. triggerDefinition:triggersDefinition];
  288. }
  289. } @catch (NSException *e) {
  290. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
  291. @"Error in parsing message node %@ "
  292. "with error %@",
  293. messageNode, e);
  294. return nil;
  295. }
  296. }
  297. @end