Skip to content

Commit 4a33b26

Browse files
authored
Support card template display in FIAM iOS SDK (#2947)
* Add card scene to storyboard, use IBDesignable rounded corners view * Use IBdesignable rounded corners view for modal message * Add card message type * Parse card message type, add new fields for secondary actions and landscape URLs to view model classes * Display a really basic card message * Naive landscape image loading (needs error handling) * Quick bug fix * Wire up Display to show cards * Wire up Display to show cards * Fix tablet width * Pick right image for orientation * Remove placeholder image sizing constraints at build time * Add more test functionality * Better image sizes * Size message image out * Switch to body text view rather than body label * Make the body text view scrollable. Set correct text color on button. * Hook up buttons and text color * Include action URL in messageClicked callback * Fix action URL * Update pod specs * Update message clicked delegate to pass an action object, not just the actionURL * Change podspec versions back, hook up background color * Don't animate scroll to top on scroll view * Fix text scrolling bug * Fix portrait and landscape fetching code. Fix background color bug * Flesh out FIRIAMMessageContentDataWithImageURLTests * Fix up FIRIAMFetchFlowTests * Fix up FIRIAMMessageClientCacheTests * Fix up FIRIAMMessageDisplayForTesting * Fix fetching in FIRIAMMessageContentDataWithImageURL * Tests for message parsing * Fix up view controllers for banner, modal, image only. Add a view controller for testing card * Add required vertical compression resistance for primary and secondary action button * Add UI tests for card view controller * Add back deprecated messageClicked: method, call it if the newer method isn't implemented. Make all display delegate methods optional. * Run scripts/styles.sh * Documentation cleanup * Fix style in FIRIAMMessageContentDataWithImageURLTests.m * Add missing copyright notices * Bump versions of both FIAM SDKS * Remove default.profraw * Address some logging punctuation nits. Cancel landscape image load if portrait image load fails. * Add missing error flow in image fetch. A few comment nits. * Shorten up init method for CardDisplayMessage * Documentation updates * Deintegrate cocoa pods * Include landscape image data in test image load class * Fix Swift naming for InAppMessagingAction in UItests * Remove card message initializer from public API, place into a private header * Mark some properties on the card object as readonly that should've been already, Find a way to initialize a card message in the UI tests. * Bump dependency on InAppMessaging given changes * Bump versions in podspecs for both SDKs * Make all view model properties for message subclasses readonly * Revert changes to other message subclasses * Make card view model properties readonly * Run styles.sh
1 parent 26a3415 commit 4a33b26

File tree

39 files changed

+2093
-410
lines changed

39 files changed

+2093
-410
lines changed

Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,16 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
178178

179179
NSDictionary *content = (NSDictionary *)contentNode;
180180
FIRIAMRenderingMode mode;
181-
UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *titleTextColor;
181+
UIColor *viewCardBackgroundColor, *btnBgColor, *btnTxtColor, *secondaryBtnTxtColor,
182+
*titleTextColor;
182183
viewCardBackgroundColor = btnBgColor = btnTxtColor = titleTextColor = nil;
183184

184-
NSString *title, *body, *imageURLStr, *actionURLStr, *actionButtonText;
185-
title = body = imageURLStr = actionButtonText = actionURLStr = nil;
185+
NSString *title, *body, *imageURLStr, *landscapeImageURLStr, *actionURLStr,
186+
*secondaryActionURLStr, *actionButtonText, *secondaryActionButtonText;
187+
title = body = imageURLStr = landscapeImageURLStr = actionButtonText =
188+
secondaryActionButtonText = actionURLStr = secondaryActionURLStr = nil;
186189

190+
// TODO: Refactor this giant if-else block into separate parsing methods per message type.
187191
if ([content[@"banner"] isKindOfClass:[NSDictionary class]]) {
188192
NSDictionary *bannerNode = (NSDictionary *)contentNode[@"banner"];
189193
mode = FIRIAMRenderAsBannerView;
@@ -227,6 +231,30 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
227231
return nil;
228232
}
229233
actionURLStr = imageOnlyNode[@"action"][@"actionUrl"];
234+
} else if ([content[@"card"] isKindOfClass:[NSDictionary class]]) {
235+
mode = FIRIAMRenderAsCardView;
236+
NSDictionary *cardNode = (NSDictionary *)contentNode[@"card"];
237+
title = cardNode[@"title"][@"text"];
238+
titleTextColor = [UIColor firiam_colorWithHexString:cardNode[@"title"][@"hexColor"]];
239+
240+
body = cardNode[@"body"][@"text"];
241+
242+
imageURLStr = cardNode[@"portraitImageUrl"];
243+
landscapeImageURLStr = cardNode[@"landscapeImageUrl"];
244+
245+
viewCardBackgroundColor = [UIColor firiam_colorWithHexString:cardNode[@"backgroundHexColor"]];
246+
247+
actionButtonText = cardNode[@"primaryActionButton"][@"text"][@"text"];
248+
btnTxtColor = [UIColor
249+
firiam_colorWithHexString:cardNode[@"primaryActionButton"][@"text"][@"hexColor"]];
250+
251+
secondaryActionButtonText = cardNode[@"secondaryActionButton"][@"text"][@"text"];
252+
secondaryBtnTxtColor = [UIColor
253+
firiam_colorWithHexString:cardNode[@"secondaryActionButton"][@"text"][@"hexColor"]];
254+
255+
actionURLStr = cardNode[@"primaryAction"][@"actionUrl"];
256+
secondaryActionURLStr = cardNode[@"secondaryAction"][@"actionUrl"];
257+
230258
} else {
231259
// Unknown message type
232260
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM900003",
@@ -241,7 +269,11 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
241269
}
242270

243271
NSURL *imageURL = (imageURLStr.length == 0) ? nil : [NSURL URLWithString:imageURLStr];
272+
NSURL *landscapeImageURL =
273+
(landscapeImageURLStr.length == 0) ? nil : [NSURL URLWithString:landscapeImageURLStr];
244274
NSURL *actionURL = (actionURLStr.length == 0) ? nil : [NSURL URLWithString:actionURLStr];
275+
NSURL *secondaryActionURL =
276+
(secondaryActionURLStr.length == 0) ? nil : [NSURL URLWithString:secondaryActionURLStr];
245277
FIRIAMRenderingEffectSetting *renderEffect =
246278
[FIRIAMRenderingEffectSetting getDefaultRenderingEffectSetting];
247279
renderEffect.viewMode = mode;
@@ -258,6 +290,10 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
258290
renderEffect.btnTextColor = btnTxtColor;
259291
}
260292

293+
if (secondaryBtnTxtColor) {
294+
renderEffect.secondaryActionBtnTextColor = secondaryBtnTxtColor;
295+
}
296+
261297
if (titleTextColor) {
262298
renderEffect.textColor = titleTextColor;
263299
}
@@ -284,8 +320,11 @@ - (FIRIAMMessageDefinition *)convertToMessageDefinitionWithMessageDict:(NSDictio
284320
[[FIRIAMMessageContentDataWithImageURL alloc] initWithMessageTitle:title
285321
messageBody:body
286322
actionButtonText:actionButtonText
323+
secondaryActionButtonText:secondaryActionButtonText
287324
actionURL:actionURL
325+
secondaryActionURL:secondaryActionURL
288326
imageURL:imageURL
327+
landscapeImageURL:landscapeImageURL
289328
usingURLSession:nil];
290329

291330
FIRIAMMessageRenderData *renderData =

Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,24 @@ NS_ASSUME_NONNULL_BEGIN
2424
@property(nonatomic, readonly, nonnull) NSString *titleText;
2525
@property(nonatomic, readonly, nonnull) NSString *bodyText;
2626
@property(nonatomic, readonly, nullable) NSString *actionButtonText;
27+
@property(nonatomic, readonly, nullable) NSString *secondaryActionButtonText;
2728
@property(nonatomic, readonly, nullable) NSURL *actionURL;
29+
@property(nonatomic, readonly, nullable) NSURL *secondaryActionURL;
2830
@property(nonatomic, readonly, nullable) NSURL *imageURL;
31+
@property(nonatomic, readonly, nullable) NSURL *landscapeImageURL;
2932

30-
// Load image data and report the result in the callback block.
31-
// Expect these cases in the callback block
32-
// If error happens, error parameter will be non-nil.
33-
// If no error happens and imageData parameter is nil, it indicates the case that there
34-
// is no image assoicated with the message.
35-
// If error is nil and imageData is not nil, then the image data is loaded successfully
33+
// Load image data, which can potentially have two images (one for landscape display). If only
34+
// one image URL exists, that image is loaded and its data is passed in the callback block.
35+
//
36+
// If both standard and landscape URLs exist, then both images are fetched asynchronously. If the
37+
// standard image fails to load, an error will be returned in the callback block and both image data
38+
// slots will be empty.
39+
// If only the landscape image fails to load, the standard image will be returned in the callback
40+
// block and the error will be nil.
41+
// If no error happens and the imageData parameter is nil, it indicates the case that there is no
42+
// image associated with the message.
3643
- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
44+
NSData *_Nullable landscapeImageData,
3745
NSError *_Nullable error))block;
3846

3947
// convert to a description string of the content

Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,11 @@ NS_ASSUME_NONNULL_BEGIN
4040
- (instancetype)initWithMessageTitle:(NSString *)title
4141
messageBody:(NSString *)body
4242
actionButtonText:(nullable NSString *)actionButtonText
43+
secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
4344
actionURL:(nullable NSURL *)actionURL
45+
secondaryActionURL:(nullable NSURL *)secondaryActionURL
4446
imageURL:(nullable NSURL *)imageURL
47+
landscapeImageURL:(nullable NSURL *)landscapeImageURL
4548
usingURLSession:(nullable NSURLSession *)URLSession;
4649
@end
4750
NS_ASSUME_NONNULL_END

Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m

Lines changed: 111 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,33 @@ @interface FIRIAMMessageContentDataWithImageURL ()
2828
@property(nonatomic, readwrite, nonnull, copy) NSString *titleText;
2929
@property(nonatomic, readwrite, nonnull, copy) NSString *bodyText;
3030
@property(nonatomic, copy, nullable) NSString *actionButtonText;
31+
@property(nonatomic, copy, nullable) NSString *secondaryActionButtonText;
3132
@property(nonatomic, copy, nullable) NSURL *actionURL;
33+
@property(nonatomic, copy, nullable) NSURL *secondaryActionURL;
3234
@property(nonatomic, nullable, copy) NSURL *imageURL;
35+
@property(nonatomic, nullable, copy) NSURL *landscapeImageURL;
3336
@property(readonly) NSURLSession *URLSession;
3437
@end
3538

3639
@implementation FIRIAMMessageContentDataWithImageURL
3740
- (instancetype)initWithMessageTitle:(NSString *)title
3841
messageBody:(NSString *)body
3942
actionButtonText:(nullable NSString *)actionButtonText
43+
secondaryActionButtonText:(nullable NSString *)secondaryActionButtonText
4044
actionURL:(nullable NSURL *)actionURL
45+
secondaryActionURL:(nullable NSURL *)secondaryActionURL
4146
imageURL:(nullable NSURL *)imageURL
47+
landscapeImageURL:(nullable NSURL *)landscapeImageURL
4248
usingURLSession:(nullable NSURLSession *)URLSession {
4349
if (self = [super init]) {
4450
_titleText = title;
4551
_bodyText = body;
4652
_imageURL = imageURL;
53+
_landscapeImageURL = landscapeImageURL;
4754
_actionButtonText = actionButtonText;
55+
_secondaryActionButtonText = secondaryActionButtonText;
4856
_actionURL = actionURL;
57+
_secondaryActionURL = secondaryActionURL;
4958

5059
if (imageURL) {
5160
_URLSession = URLSession ? URLSession : [NSURLSession sharedSession];
@@ -74,65 +83,123 @@ - (nullable NSString *)getActionButtonText {
7483
return _actionButtonText;
7584
}
7685

77-
- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
86+
- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable standardImageData,
87+
NSData *_Nullable landscapeImageData,
7888
NSError *_Nullable error))block {
7989
if (!block) {
8090
// no need for any further action if block is nil
8191
return;
8292
}
8393

84-
if (!_imageURL) {
94+
if (!_imageURL && !_landscapeImageURL) {
8595
// no image data since image url is nil
86-
block(nil, nil);
96+
block(nil, nil, nil);
97+
} else if (!_landscapeImageURL) {
98+
// Only fetch standard image.
99+
[self fetchImageFromURL:_imageURL
100+
withBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) {
101+
block(imageData, nil, error);
102+
}];
103+
} else if (!_imageURL) {
104+
// Only fetch portrait image.
105+
[self fetchImageFromURL:_landscapeImageURL
106+
withBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) {
107+
block(nil, imageData, error);
108+
}];
87109
} else {
88-
NSURLRequest *imageDataRequest = [NSURLRequest requestWithURL:_imageURL];
89-
NSURLSessionDataTask *task = [_URLSession
90-
dataTaskWithRequest:imageDataRequest
91-
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
92-
if (error) {
93-
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000003",
94-
@"Error in fetching image: %@", error);
95-
block(nil, error);
96-
} else {
97-
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
98-
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
99-
if (httpResponse.statusCode == SuccessHTTPStatusCode) {
100-
if (httpResponse.MIMEType == nil || ![httpResponse.MIMEType hasPrefix:@"image"]) {
101-
NSString *errorDesc =
102-
[NSString stringWithFormat:@"None image MIME type %@"
103-
" detected for url %@",
104-
httpResponse.MIMEType, self.imageURL];
105-
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000004", @"%@", errorDesc);
106-
107-
NSError *error =
108-
[NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
109-
code:FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL
110-
userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
111-
block(nil, error);
112-
} else {
113-
block(data, nil);
114-
}
115-
} else {
110+
// Fetch both images separately, call completion when they're both fetched.
111+
__block NSData *portrait = nil;
112+
__block NSData *landscape = nil;
113+
__block NSError *landscapeImageLoadError = nil;
114+
115+
[self fetchImageFromURL:_imageURL
116+
withBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) {
117+
__weak FIRIAMMessageContentDataWithImageURL *weakSelf = self;
118+
119+
// If the portrait image fails to load, we treat this as a failure.
120+
if (error) {
121+
// Cancel landscape image fetch.
122+
[weakSelf.URLSession invalidateAndCancel];
123+
124+
block(nil, nil, error);
125+
return;
126+
}
127+
128+
portrait = imageData;
129+
if (landscape || landscapeImageLoadError) {
130+
block(portrait, landscape, nil);
131+
}
132+
}];
133+
134+
[self fetchImageFromURL:_landscapeImageURL
135+
withBlock:^(NSData *_Nullable imageData, NSError *_Nullable error) {
136+
if (error) {
137+
landscapeImageLoadError = error;
138+
} else {
139+
landscape = imageData;
140+
}
141+
142+
if (portrait) {
143+
block(portrait, landscape, nil);
144+
}
145+
}];
146+
}
147+
}
148+
149+
- (void)fetchImageFromURL:(NSURL *)imageURL
150+
withBlock:(void (^)(NSData *_Nullable imageData, NSError *_Nullable error))block {
151+
NSURLRequest *imageDataRequest = [NSURLRequest requestWithURL:imageURL];
152+
NSURLSessionDataTask *task = [_URLSession
153+
dataTaskWithRequest:imageDataRequest
154+
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
155+
if (error) {
156+
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000003", @"Error in fetching image: %@",
157+
error);
158+
block(nil, error);
159+
} else {
160+
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
161+
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
162+
if (httpResponse.statusCode == SuccessHTTPStatusCode) {
163+
if (httpResponse.MIMEType == nil || ![httpResponse.MIMEType hasPrefix:@"image"]) {
116164
NSString *errorDesc =
117-
[NSString stringWithFormat:@"Failed HTTP request to crawl image %@: "
118-
"HTTP status code as %ld",
119-
self->_imageURL, (long)httpResponse.statusCode];
120-
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000001", @"%@", errorDesc);
165+
[NSString stringWithFormat:@"No image MIME type %@"
166+
" detected for URL %@",
167+
httpResponse.MIMEType, self.imageURL];
168+
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000004", @"%@", errorDesc);
169+
121170
NSError *error =
122-
[NSError errorWithDomain:NSURLErrorDomain
123-
code:httpResponse.statusCode
171+
[NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
172+
code:FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL
124173
userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
125174
block(nil, error);
175+
} else {
176+
block(data, nil);
126177
}
127178
} else {
128-
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000002",
129-
@"Internal error: got a non http response from fetching image for "
130-
@"image url as %@",
131-
self->_imageURL);
179+
NSString *errorDesc =
180+
[NSString stringWithFormat:@"Failed HTTP request to crawl image %@: "
181+
"HTTP status code as %ld",
182+
self->_imageURL, (long)httpResponse.statusCode];
183+
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000001", @"%@", errorDesc);
184+
NSError *error = [NSError errorWithDomain:NSURLErrorDomain
185+
code:httpResponse.statusCode
186+
userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
187+
block(nil, error);
132188
}
189+
} else {
190+
NSString *errorDesc =
191+
[NSString stringWithFormat:@"Internal error: got a non HTTP response from "
192+
@"fetching image for image URL as %@",
193+
imageURL];
194+
FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000002", @"%@", errorDesc);
195+
NSError *error = [NSError errorWithDomain:NSURLErrorDomain
196+
code:FIRIAMSDKRuntimeErrorNonHTTPResponseForImage
197+
userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
198+
block(nil, error);
133199
}
134-
}];
135-
[task resume];
136-
}
200+
}
201+
}];
202+
[task resume];
137203
}
204+
138205
@end

Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ NS_ASSUME_NONNULL_BEGIN
2222
typedef NS_ENUM(NSInteger, FIRIAMRenderingMode) {
2323
FIRIAMRenderAsBannerView,
2424
FIRIAMRenderAsModalView,
25-
FIRIAMRenderAsImageOnlyView
25+
FIRIAMRenderAsImageOnlyView,
26+
FIRIAMRenderAsCardView
2627
};
2728

2829
/**
@@ -42,6 +43,9 @@ typedef NS_ENUM(NSInteger, FIRIAMRenderingMode) {
4243
// text color for action button
4344
@property(nonatomic, copy) UIColor *btnTextColor;
4445

46+
// text color for secondary action button
47+
@property(nonatomic, copy) UIColor *secondaryActionBtnTextColor;
48+
4549
// background color for action button
4650
@property(nonatomic, copy) UIColor *btnBGColor;
4751

0 commit comments

Comments
 (0)