AutorÃa | Ultima modificación | Ver Log |
/*
* 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
#import "FirebaseCore/Sources/Private/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