| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402 |
- /*
- * Copyright 2017 Google
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #import <TargetConditionals.h>
- #if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
- #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
- #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
- #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
- #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentData.h"
- #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentDataWithImageURL.h"
- #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h"
- #import "FirebaseInAppMessaging/Sources/Private/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h"
- #import "FirebaseInAppMessaging/Sources/Private/Util/FIRIAMTimeFetcher.h"
- #import "FirebaseInAppMessaging/Sources/Util/UIColor+FIRIAMHexString.h"
- #import "FirebaseABTesting/Sources/Private/ABTExperimentPayload.h"
- @interface FIRIAMFetchResponseParser ()
- @property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
- @end
- @implementation FIRIAMFetchResponseParser
- - (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
- if (self = [super init]) {
- _timeFetcher = timeFetcher;
- }
- return self;
- }
- - (NSArray<FIRIAMMessageDefinition *> *)parseAPIResponseDictionary:(NSDictionary *)responseDict
- discardedMsgCount:(NSInteger *)discardCount
- fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime {
- if (fetchWaitTime != nil) {
- *fetchWaitTime = nil; // It would be set to non nil value if it's detected in responseDict
- if ([responseDict[@"expirationEpochTimestampMillis"] isKindOfClass:NSString.class]) {
- NSTimeInterval nextFetchTimeInResponse =
- [responseDict[@"expirationEpochTimestampMillis"] doubleValue] / 1000;
- NSTimeInterval fetchWaitTimeInSeconds =
- nextFetchTimeInResponse - [self.timeFetcher currentTimestampInSeconds];
- FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900005",
- @"Detected next fetch epoch time in API response as %f seconds and wait for %f "
- "seconds before next fetch.",
- nextFetchTimeInResponse, fetchWaitTimeInSeconds);
- if (fetchWaitTimeInSeconds > 0.01) {
- *fetchWaitTime = @(fetchWaitTimeInSeconds);
- FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900018",
- @"Fetch wait time calculated from server response is negative. Discard it.");
- }
- } else {
- FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM900014",
- @"No fetch epoch time detected in API response.");
- }
- }
- NSArray<NSDictionary *> *messageArray = responseDict[@"messages"];
- NSInteger discarded = 0;
- NSMutableArray<FIRIAMMessageDefinition *> *definitions = [[NSMutableArray alloc] init];
- for (NSDictionary *nextMsg in messageArray) {
- FIRIAMMessageDefinition *nextDefinition =
- [self convertToMessageDefinitionWithMessageDict:nextMsg];
- if (nextDefinition) {
- [definitions addObject:nextDefinition];
- } else {
- FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM900001",
- @"No definition generated for message node %@", nextMsg);
- discarded++;
- }
- }
- FIRLogDebug(
- kFIRLoggerInAppMessaging, @"I-IAM900002",
- @"%lu message definitions were parsed out successfully and %lu messages are discarded",
- (unsigned long)definitions.count, (unsigned long)discarded);
- if (discardCount) {
- *discardCount = discarded;
- }
- return [definitions copy];
- }
- // Return nil if no valid triggering condition can be detected
- - (NSArray<FIRIAMDisplayTriggerDefinition *> *)parseTriggeringCondition:
- (NSArray<NSDictionary *> *)triggerConditions {
- if (triggerConditions == nil || triggerConditions.count == 0) {
- return nil;
- }
- NSMutableArray<FIRIAMDisplayTriggerDefinition *> *triggers = [[NSMutableArray alloc] init];
- for (NSDictionary *nextTriggerCondition in triggerConditions) {
- // Handle app_launch and on_foreground cases.
- if (nextTriggerCondition[@"fiamTrigger"]) {
- if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"ON_FOREGROUND"]) {
- [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppForegroundTrigger]];
- } else if ([nextTriggerCondition[@"fiamTrigger"] isEqualToString:@"APP_LAUNCH"]) {
- [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc] initForAppLaunchTrigger]];
- }
- } else if ([nextTriggerCondition[@"event"] isKindOfClass:[NSDictionary class]]) {
- NSDictionary *triggeringEvent = (NSDictionary *)nextTriggerCondition[@"event"];
- if (triggeringEvent[@"name"]) {
- [triggers addObject:[[FIRIAMDisplayTriggerDefinition alloc]
- initWithFirebaseAnalyticEvent:triggeringEvent[@"name"]]];
- }
- }
- }
- return [triggers copy];
- }
- // For one element in the restful API response's messages array, convert into
- // a FIRIAMMessageDefinition object. If the conversion fails, a nil is returned.
- - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictionary *)messageNode {
- @try {
- BOOL isTestMessage = NO;
- id isTestCampaignNode = messageNode[@"isTestCampaign"];
- if ([isTestCampaignNode isKindOfClass:[NSNumber class]]) {
- isTestMessage = [isTestCampaignNode boolValue];
- }
- id payloadNode = messageNode[@"experimentalPayload"] ?: messageNode[@"vanillaPayload"];
- if (![payloadNode isKindOfClass:[NSDictionary class]]) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900012",
- @"Message payload does not exist or does not represent a dictionary in "
- "message node %@",
- messageNode);
- return nil;
- }
- NSString *messageID = payloadNode[@"campaignId"];
- if (!messageID) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900010",
- @"messsage id is missing in message node %@", messageNode);
- return nil;
- }
- NSString *messageName = payloadNode[@"campaignName"];
- if (!messageName && !isTestMessage) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900011",
- @"campaign name is missing in non-test message node %@", messageNode);
- return nil;
- }
- ABTExperimentPayload *experimentPayload = nil;
- NSDictionary *experimentPayloadDictionary = payloadNode[@"experimentPayload"];
- if (experimentPayloadDictionary) {
- experimentPayload =
- [[ABTExperimentPayload alloc] initWithDictionary:experimentPayloadDictionary];
- }
- NSTimeInterval startTimeInSeconds = 0;
- NSTimeInterval endTimeInSeconds = 0;
- if (!isTestMessage) {
- // Parsing start/end times out of non-test messages. They are strings in the
- // json response.
- id startTimeNode = payloadNode[@"campaignStartTimeMillis"];
- if ([startTimeNode isKindOfClass:[NSString class]]) {
- startTimeInSeconds = [startTimeNode doubleValue] / 1000.0;
- }
- id endTimeNode = payloadNode[@"campaignEndTimeMillis"];
- if ([endTimeNode isKindOfClass:[NSString class]]) {
- endTimeInSeconds = [endTimeNode doubleValue] / 1000.0;
- }
- }
- id contentNode = messageNode[@"content"];
- if (![contentNode isKindOfClass:[NSDictionary class]]) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900013",
- @"content node does not exist or does not represent a dictionary in "
- "message node %@",
- messageNode);
- return nil;
- }
- NSDictionary *content = (NSDictionary *)contentNode;
- FIRIAMRenderingMode mode;
- UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
- *titleTextColor;
- viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
- NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
- *secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
- title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
- secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
- // TODO: Refactor this giant if-else block into separate parsing methods per message type.
- if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
- NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
- mode = FIRIAMRenderAsBannerView;
- title = bannerNode[@"title"][@"text"];
- titleTextColor = [UIColor firiam_colorWithHexString:bannerNode[@"title"][@"hexColor"]];
- body = bannerNode[@"body"][@"text"];
- imageURLStr = bannerNode[@"imageUrl"];
- actionURLStr = bannerNode[@"action"][@"actionUrl"];
- viewCardBackgroundColor =
- [UIColor firiam_colorWithHexString:bannerNode[@"backgroundHexColor"]];
- } else if ([content[@"modal"] isKindOfClass:[NSDictionary class]]) {
- mode = FIRIAMRenderAsModalView;
- NSDictionary *modalNode = (NSDictionary *)contentNode[@"modal"];
- title = modalNode[@"title"][@"text"];
- titleTextColor = [UIColor firiam_colorWithHexString:modalNode[@"title"][@"hexColor"]];
- body = modalNode[@"body"][@"text"];
- imageURLStr = modalNode[@"imageUrl"];
- actionButtonText = modalNode[@"actionButton"][@"text"][@"text"];
- btnTxtColor =
- [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"text"][@"hexColor"]];
- btnBgColor =
- [UIColor firiam_colorWithHexString:modalNode[@"actionButton"][@"buttonHexColor"]];
- actionURLStr = modalNode[@"action"][@"actionUrl"];
- viewCardBackgroundColor =
- [UIColor firiam_colorWithHexString:modalNode[@"backgroundHexColor"]];
- } else if ([content[@"imageOnly"] isKindOfClass:[NSDictionary class]]) {
- mode = FIRIAMRenderAsImageOnlyView;
- NSDictionary *imageOnlyNode = (NSDictionary *)contentNode[@"imageOnly"];
- imageURLStr = imageOnlyNode[@"imageUrl"];
- if (!imageURLStr) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900007",
- @"Image url is missing for image-only message %@", messageNode);
- return nil;
- }
- actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
- } else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
- mode = FIRIAMRenderAsCardView;
- NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
- title = cardNode[@"title"][@"text"];
- titleTextColor = [UIColor firiam_colorWithHexString:cardNode[@"title"][@"hexColor"]];
- body = cardNode[@"body"][@"text"];
- imageURLStr = cardNode[@"portraitImageUrl"];
- landscapeImageURLStr = cardNode[@"landscapeImageUrl"];
- viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
- actionButtonText = cardNode[@"primaryActionButton"][@"text"][@"text"];
- btnTxtColor = [UIColor
- firiam_colorWithHexString:cardNode[@"primaryActionButton"][@"text"][@"hexColor"]];
- secondaryActionButtonText = cardNode[@"secondaryActionButton"][@"text"][@"text"];
- secondaryBtnTxtColor = [UIColor
- firiam_colorWithHexString:cardNode[@"secondaryActionButton"][@"text"][@"hexColor"]];
- actionURLStr = cardNode[@"primaryAction"][@"actionUrl"];
- secondaryActionURLStr = cardNode[@"secondaryAction"][@"actionUrl"];
- } else {
- // Unknown message type
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
- @"Unknown message type in message node %@", messageNode);
- return nil;
- }
- if (title == nil && mode != FIRIAMRenderAsImageOnlyView) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900004",
- @"Title text is missing in message node %@", messageNode);
- return nil;
- }
- NSURL *imageURL = [self imageURLFromURLString:imageURLStr];
- NSURL *landscapeImageURL = [self imageURLFromURLString:landscapeImageURLStr];
- NSURL *actionURL = [self urlFromURLString:actionURLStr];
- NSURL *secondaryActionURL = [self urlFromURLString:secondaryActionURLStr];
- FIRIAMRenderingEffectSetting *renderEffect =
- [FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
- renderEffect.viewMode = mode;
- if (viewCardBackgroundColor) {
- renderEffect.displayBGColor = viewCardBackgroundColor;
- }
- if (btnBgColor) {
- renderEffect.btnBGColor = btnBgColor;
- }
- if (btnTxtColor) {
- renderEffect.btnTextColor = btnTxtColor;
- }
- if (secondaryBtnTxtColor) {
- renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
- }
- if (titleTextColor) {
- renderEffect.textColor = titleTextColor;
- }
- NSArray<FIRIAMDisplayTriggerDefinition *> *triggersDefinition =
- [self parseTriggeringCondition:messageNode[@"triggeringConditions"]];
- if (isTestMessage) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900008",
- @"A test message with id %@ was parsed successfully.", messageID);
- renderEffect.isTestMessage = YES;
- } else {
- // Triggering definitions should always be present for a non-test message.
- if (!triggersDefinition || triggersDefinition.count == 0) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900009",
- @"No valid triggering condition is detected in message definition"
- " with id %@",
- messageID);
- return nil;
- }
- }
- FIRIAMMessageContentDataWithImageURL *msgData =
- [[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
- messageBody:body
- actionButtonText:actionButtonText
- secondaryActionButtonText:secondaryActionButtonText
- actionURL:actionURL
- secondaryActionURL:secondaryActionURL
- imageURL:imageURL
- landscapeImageURL:landscapeImageURL
- usingURLSession:nil];
- FIRIAMMessageRenderData *renderData =
- [[FIRIAMMessageRenderData alloc] initWithMessageID:messageID
- messageName:messageName
- contentData:msgData
- renderingEffect:renderEffect];
- NSDictionary *dataBundle = nil;
- id dataBundleNode = messageNode[@"dataBundle"];
- if ([dataBundleNode isKindOfClass:[NSDictionary class]]) {
- dataBundle = dataBundleNode;
- }
- if (isTestMessage) {
- return [[FIRIAMMessageDefinition alloc] initTestMessageWithRenderData:renderData
- appData:dataBundle
- experimentPayload:experimentPayload];
- } else {
- return [[FIRIAMMessageDefinition alloc] initWithRenderData:renderData
- startTime:startTimeInSeconds
- endTime:endTimeInSeconds
- triggerDefinition:triggersDefinition
- appData:dataBundle
- experimentPayload:experimentPayload
- isTestMessage:NO];
- }
- } @catch (NSException *e) {
- FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900006",
- @"Error in parsing message node %@ "
- "with error %@",
- messageNode, e);
- return nil;
- }
- }
- - (nullable NSURL *)imageURLFromURLString:(NSString *)string {
- NSURL *url = [self urlFromURLString:string];
- // Image URLs must be valid HTTPS links, according to the Firebase Console.
- if (![url.scheme.lowercaseString isEqualToString:@"https"]) return nil;
- return url;
- }
- - (nullable NSURL *)urlFromURLString:(NSString *)string {
- NSString *sanitizedString = [self sanitizedURLStringFromString:string];
- if (sanitizedString.length == 0) return nil;
- return [NSURL URLWithString:sanitizedString];
- }
- - (NSString *)sanitizedURLStringFromString:(NSString *)string {
- return [string stringByReplacingOccurrencesOfString:@" " withString:@""];
- }
- @end
- #endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
|