FIRIAMFetchResponseParser.m 16 KB

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