FIRIAMFetchResponseParser.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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, *titleTextColor;
  158. viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
  159. NSString *title, *body, *imageURLStr, *actionURLStr, *actionButtonText;
  160. title = body = imageURLStr = actionButtonText = actionURLStr = nil;
  161. if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
  162. NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
  163. mode = FIRIAMRenderAsBannerView;
  164. title = bannerNode[@"title"][@"text"];
  165. titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]];
  166. body = bannerNode[@"body"][@"text"];
  167. imageURLStr = bannerNode[@"imageUrl"];
  168. actionURLStr = bannerNode[@"action"][@"actionUrl"];
  169. viewCardBackgroundColor =
  170. [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
  171. } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
  172. mode = FIRIAMRenderAsModalView;
  173. NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
  174. title = modalNode[@"title"][@"text"];
  175. titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]];
  176. body = modalNode[@"body"][@"text"];
  177. imageURLStr = modalNode[@"imageUrl"];
  178. actionButtonText = modalNode[@"actionButton"][@"text"][@"text"];
  179. btnBgColor =
  180. [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]];
  181. actionURLStr = modalNode[@"action"][@"actionUrl"];
  182. viewCardBackgroundColor =
  183. [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
  184. } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
  185. mode = FIRIAMRenderAsImageOnlyView;
  186. NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
  187. imageURLStr = imageOnlyNode[@"imageUrl"];
  188. if (!imageURLStr) {
  189. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
  190. @"Image url is missing for image-only message %@", messageNode);
  191. return nil;
  192. }
  193. actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
  194. } else {
  195. // Unknown message type
  196. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
  197. @"Unknown message type in message node %@", messageNode);
  198. return nil;
  199. }
  200. if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
  201. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
  202. @"Title text is missing in message node %@", messageNode);
  203. return nil;
  204. }
  205. NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr];
  206. NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr];
  207. FIRIAMRenderingEffectSetting *renderEffect =
  208. [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
  209. renderEffect.viewMode = mode;
  210. if (viewCardBackgroundColor) {
  211. renderEffect.displayBGColor = viewCardBackgroundColor;
  212. }
  213. if (btnBgColor) {
  214. renderEffect.btnBGColor = btnBgColor;
  215. }
  216. if (btnTxtColor) {
  217. renderEffect.btnTextColor = btnTxtColor;
  218. }
  219. if (titleTextColor) {
  220. renderEffect.textColor = titleTextColor;
  221. }
  222. NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
  223. [self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
  224. if (isTestMessage) {
  225. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
  226. @"A test message with id %@ was parsed successfully.", messageID);
  227. renderEffect.isTestMessage = YES;
  228. } else {
  229. // Triggering definitions should always be present for a non-test message.
  230. if (!triggersDefinition || triggersDefinition.count == 0) {
  231. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
  232. @"No valid triggering condition is detected in message definition"
  233. " with id %@",
  234. messageID);
  235. return nil;
  236. }
  237. }
  238. FIRIAMMessageContentDataWithImageURL *msgData =
  239. [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
  240. messageBody:body
  241. actionButtonText:actionButtonText
  242. actionURL:actionURL
  243. imageURL:imageURL
  244. usingURLSession:nil];
  245. FIRIAMMessageRenderData *renderData =
  246. [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
  247. messageName:messageName
  248. contentData:msgData
  249. renderingEffect:renderEffect];
  250. if (isTestMessage) {
  251. return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData];
  252. } else {
  253. return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
  254. startTime:startTimeInSeconds
  255. endTime:endTimeInSeconds
  256. triggerDefinition:triggersDefinition];
  257. }
  258. } @catch (NSException *e) {
  259. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
  260. @"Error in parsing message node %@ "
  261. "with error %@",
  262. messageNode, e);
  263. return nil;
  264. }
  265. }
  266. @end