diff --git a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.h b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.h new file mode 100644 index 00000000000..57cbfb7ae81 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.h @@ -0,0 +1,35 @@ +/* + * Copyright 2018 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 + +#import "FIDBaseRenderingViewController.h" + +@class FIRInAppMessagingBannerDisplay; +@class FIDBaseRenderingViewController; +@protocol FIDTimeFetcher; +@protocol FIRInAppMessagingDisplayDelegate; + +NS_ASSUME_NONNULL_BEGIN +@interface FIDBannerViewController : FIDBaseRenderingViewController ++ (FIDBannerViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage:(FIRInAppMessagingBannerDisplay *)bannerMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m new file mode 100644 index 00000000000..6f242ffb3c9 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewController.m @@ -0,0 +1,293 @@ +/* + * Copyright 2018 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 + +#import "FIDBannerViewController.h" +#import "FIRCore+InAppMessagingDisplay.h" + +@interface FIDBannerViewController () + +@property(nonatomic, readwrite) FIRInAppMessagingBannerDisplay *bannerDisplayMessage; + +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageViewWidthConstraint; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageViewHeightConstraint; + +@property(weak, nonatomic) + IBOutlet NSLayoutConstraint *imageBottomAlignWithBodyLabelBottomConstraint; +@property(weak, nonatomic) IBOutlet UIImageView *imageView; +@property(weak, nonatomic) IBOutlet UILabel *titleLabel; +@property(weak, nonatomic) IBOutlet UILabel *bodyLabel; + +// Banner view will be rendered and dismissed with animation. Within viewDidLayoutSubviews function, +// we would position the view so that it's out of UIWindow range on the top so that later on it can +// slide in with animation. However, viewDidLayoutSubviews is also triggred in other scenarios +// like split view on iPad or device orientation changes where we don't want to hide the banner for +// animations. So to have different logic, we use this property to tell the two different +// cases apart and apply different positioning logic accordingly in viewDidLayoutSubviews. +@property(nonatomic) BOOL hidingForAnimation; + +@property(nonatomic, nullable) NSTimer *autoDismissTimer; +@end + +// The image display area dimension in points +static const CGFloat kBannerViewImageWidth = 60; +static const CGFloat kBannerViewImageHeight = 60; + +static const NSTimeInterval kBannerViewAnimationDuration = 0.3; // in seconds + +// Banner view will auto dismiss after this amount of time of showing if user does not take +// any other actions. It's in seconds. +static const NSTimeInterval kBannerAutoDimissTime = 12; + +// If the window width is larger than this threshold, we cap banner view width +// by it: showing a non full-width banner when it happens. +static const CGFloat kBannerViewMaxWidth = 736; + +static const CGFloat kSwipeUpThreshold = -10.0f; + +@implementation FIDBannerViewController + ++ (FIDBannerViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage:(FIRInAppMessagingBannerDisplay *)bannerMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher { + UIStoryboard *storyboard = + [UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard" bundle:resourceBundle]; + + if (storyboard == nil) { + FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300002", + @"Storyboard '" + "FIRInAppMessageDisplayStoryboard' not found in bundle %@", + resourceBundle); + return nil; + } + FIDBannerViewController *bannerVC = (FIDBannerViewController *)[storyboard + instantiateViewControllerWithIdentifier:@"banner-view-vc"]; + bannerVC.displayDelegate = displayDelegate; + bannerVC.bannerDisplayMessage = bannerMessage; + bannerVC.timeFetcher = timeFetcher; + + return bannerVC; +} + +- (void)setupRecognizers { + UIPanGestureRecognizer *panSwipeRecognizer = + [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanSwipe:)]; + [self.view addGestureRecognizer:panSwipeRecognizer]; + + UITapGestureRecognizer *tapGestureRecognizer = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(messageTapped:)]; + tapGestureRecognizer.delaysTouchesBegan = YES; + tapGestureRecognizer.numberOfTapsRequired = 1; + + [self.view addGestureRecognizer:tapGestureRecognizer]; +} + +- (void)handlePanSwipe:(UIPanGestureRecognizer *)recognizer { + // Detect the swipe gesture + if (recognizer.state == UIGestureRecognizerStateEnded) { + CGPoint vel = [recognizer velocityInView:recognizer.view]; + if (vel.y < kSwipeUpThreshold) { + [self closeViewFromManualDismiss]; + } + } +} + +- (void)viewDidLoad { + [super viewDidLoad]; + // Do any additional setup after loading the view from its nib. + + [self setupRecognizers]; + + self.titleLabel.text = self.bannerDisplayMessage.title; + self.bodyLabel.text = self.bannerDisplayMessage.bodyText; + + if (self.bannerDisplayMessage.imageData) { + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + + UIImage *image = [UIImage imageWithData:self.bannerDisplayMessage.imageData.imageRawData]; + + if (fabs(image.size.width / image.size.height - 1) > 0.02) { + // width and height differ by at least 2%, need to adjust image view + // size to respect the ratio + + // reduce height or width of the image view to retain the ratio of the image + if (image.size.width > image.size.height) { + CGFloat newImageHeight = kBannerViewImageWidth * image.size.height / image.size.width; + self.imageViewHeightConstraint.constant = newImageHeight; + } else { + CGFloat newImageWidth = kBannerViewImageHeight * image.size.width / image.size.height; + self.imageViewWidthConstraint.constant = newImageWidth; + } + } + self.imageView.image = image; + } else { + // Hide image and remove the bottom constraint between body label and image view. + self.imageViewWidthConstraint.constant = 0; + self.imageBottomAlignWithBodyLabelBottomConstraint.active = NO; + } + + // Set some rendering effects based on settings. + self.view.backgroundColor = self.bannerDisplayMessage.displayBackgroundColor; + self.titleLabel.textColor = self.bannerDisplayMessage.textColor; + self.bodyLabel.textColor = self.bannerDisplayMessage.textColor; + + self.view.layer.masksToBounds = NO; + self.view.layer.shadowOffset = CGSizeMake(2, 1); + self.view.layer.shadowRadius = 2; + self.view.layer.shadowOpacity = 0.4; + + // When created, we are hiding it for later animation + self.hidingForAnimation = YES; + [self setupAutoDismissTimer]; +} + +- (void)dismissViewWithAnimation:(void (^)(void))completion { + CGRect rectInNormalState = self.view.frame; + CGAffineTransform hidingTransform = + CGAffineTransformMakeTranslation(0, -rectInNormalState.size.height); + + [UIView animateWithDuration:kBannerViewAnimationDuration + delay:0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + self.view.transform = hidingTransform; + } + completion:^(BOOL finished) { + completion(); + }]; +} + +- (void)closeViewFromAutoDismiss { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300001", @"Auto dismiss the banner view"); + [self dismissViewWithAnimation:^(void) { + [self dismissView:FIRInAppMessagingDismissTypeAuto]; + }]; +} + +- (void)closeViewFromManualDismiss { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300003", @"Manually dismiss the banner view"); + [self.autoDismissTimer invalidate]; + [self dismissViewWithAnimation:^(void) { + [self dismissView:FIRInAppMessagingDismissTypeUserSwipe]; + }]; +} + +- (void)messageTapped:(UITapGestureRecognizer *)recognizer { + [self.autoDismissTimer invalidate]; + [self dismissViewWithAnimation:^(void) { + [self followActionURL]; + }]; +} + +- (void)adjustBodyLabelViewHeight { + // These lines make sure that we only change the height of the label view + // to fit the content. Doing [self.bodyLabel sizeToFit] only could potentially + // change the width as well. + CGRect theFrame = self.bodyLabel.frame; + [self.bodyLabel sizeToFit]; + theFrame.size.height = self.bodyLabel.frame.size.height; + self.bodyLabel.frame = theFrame; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + CGFloat bannerViewHeight = 0; + + [self adjustBodyLabelViewHeight]; + + if (self.bannerDisplayMessage.imageData) { + CGFloat imageBottom = CGRectGetMaxY(self.imageView.frame); + CGFloat bodyBottom = CGRectGetMaxY(self.bodyLabel.frame); + bannerViewHeight = MAX(imageBottom, bodyBottom); + } else { + bannerViewHeight = CGRectGetMaxY(self.bodyLabel.frame); + } + + bannerViewHeight += 5; // Add some padding margin on the bottom of the view + + CGFloat appWindowWidth = [self.view.window bounds].size.width; + CGFloat bannerViewWidth = appWindowWidth; + + if (bannerViewWidth > kBannerViewMaxWidth) { + bannerViewWidth = kBannerViewMaxWidth; + self.view.layer.cornerRadius = 4; + } + + CGRect viewRect = + CGRectMake((appWindowWidth - bannerViewWidth) / 2, 0, bannerViewWidth, bannerViewHeight); + self.view.frame = viewRect; + + if (self.hidingForAnimation) { + // Move the banner to be just above the top of the window to hide it. + self.view.center = CGPointMake(appWindowWidth / 2, -viewRect.size.height / 2); + } +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + CGRect rectInNormalState = self.view.frame; + CGPoint normalCenterPoint = + CGPointMake(rectInNormalState.origin.x + rectInNormalState.size.width / 2, + rectInNormalState.size.height / 2); + + self.hidingForAnimation = NO; + [UIView animateWithDuration:kBannerViewAnimationDuration + delay:0 + options:UIViewAnimationOptionCurveEaseInOut + animations:^{ + self.view.center = normalCenterPoint; + } + completion:nil]; +} + +- (void)setupAutoDismissTimer { + NSTimeInterval remaining = kBannerAutoDimissTime - super.aggregateImpressionTimeInSeconds; + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004", + @"Remaining banner auto dismiss time is %lf", remaining); + + // Set up the auto dismiss behavior. + __weak id weakSelf = self; + self.autoDismissTimer = + [NSTimer scheduledTimerWithTimeInterval:remaining + target:weakSelf + selector:@selector(closeViewFromAutoDismiss) + userInfo:nil + repeats:NO]; +} + +// Handlers for app become active inactive so that we can better adjust our auto dismiss feature +- (void)appDidBecomeInactive:(UIApplication *)application { + [super appDidBecomeInactive:application]; + [self.autoDismissTimer invalidate]; +} + +- (void)appDidBecomeActive:(UIApplication *)application { + [super appDidBecomeActive:application]; + [self setupAutoDismissTimer]; +} + +- (void)dealloc { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300005", + @"-[FIRIAMBannerViewController dealloc] triggered for %p", self); + [self.autoDismissTimer invalidate]; +} +@end diff --git a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.h b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.h new file mode 100644 index 00000000000..57aa05d4e1d --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.h @@ -0,0 +1,23 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN +@interface FIDBannerViewUIWindow : UIWindow + +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.m b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.m new file mode 100644 index 00000000000..05d41b2123c --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Banner/FIDBannerViewUIWindow.m @@ -0,0 +1,30 @@ +/* + * Copyright 2018 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 "FIDBannerViewUIWindow.h" + +@implementation FIDBannerViewUIWindow + +// For banner view message, we still allow the user to interact with the app's underlying view +// outside banner view's visible area. +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { + if (self.rootViewController && self.rootViewController.view) { + return CGRectContainsPoint(self.rootViewController.view.frame, point); + } else { + return NO; + } +} +@end diff --git a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h new file mode 100644 index 00000000000..c1bd7154118 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.h @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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 + +#import +#import "FIDTimeFetcher.h" + +@protocol FIRInAppMessagingDisplayDelegate; + +NS_ASSUME_NONNULL_BEGIN +@interface FIDBaseRenderingViewController : UIViewController +@property(nonatomic, readwrite) id timeFetcher; + +@property(nonatomic, readwrite) id displayDelegate; + +// These are the two methods we use to respond to app state change for the purpose of +// actual display time tracking. Subclass can override this one to have more logic for responding +// to the two events, but remember to trigger super's implementation. +- (void)appDidBecomeInactive:(UIApplication *)application; +- (void)appDidBecomeActive:(UIApplication *)application; + +// Tracking the aggregate impression time for the rendered message. Used to determine when +// we are eaching the minimal iimpression time requirements. Exposed so that sub banner vc +// class can use it for auto dismiss tracking +@property(nonatomic) double aggregateImpressionTimeInSeconds; + +// Call this when the user choose to dismiss the message +- (void)dismissView:(FIRInAppMessagingDismissType)dismissType; + +// Call this when end user wants to follow the action url +- (void)followActionURL; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m new file mode 100644 index 00000000000..8b416313a02 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIDBaseRenderingViewController.m @@ -0,0 +1,157 @@ +/* + * Copyright 2018 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 + +#import "FIDBaseRenderingViewController.h" +#import "FIDTimeFetcher.h" +#import "FIRCore+InAppMessagingDisplay.h" + +@interface FIDBaseRenderingViewController () +// For fiam messages, it's required to be kMinValidImpressionTime to +// be considered as a valid impression help. If the app is closed before that's reached, +// SDK may try to render the same message again in the future. +@property(nonatomic, nullable) NSTimer *minImpressionTimer; + +// Tracking the start time when the current impression session start. +@property(nonatomic) double currentImpressionStartTime; + +@end + +static const NSTimeInterval kMinValidImpressionTime = 3.0; + +@implementation FIDBaseRenderingViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + // In order to track display time for this message, we need to respond to + // app foreground/background events since viewDidAppear/viewDidDisappear are not + // triggered when app switches happen. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidBecomeInactive:) + name:UIApplicationWillResignActiveNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(appDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + self.aggregateImpressionTimeInSeconds = 0; +} + +- (void)viewDidAppear:(BOOL)animated { + [super viewDidAppear:animated]; + [self impressionStartCheckpoint]; +} + +- (void)viewDidDisappear:(BOOL)animated { + [super viewDidDisappear:animated]; + [self impressionStopCheckpoint]; +} + +// Call this when the view starts to be rendered so that we can track the aggregate impression +// time for the current message +- (void)impressionStartCheckpoint { + self.currentImpressionStartTime = [self.timeFetcher currentTimestampInSeconds]; + [self setupMinImpressionTimer]; +} + +// Trigger this when the view stops to be rendered so that we can track the aggregate impression +// time for the current message +- (void)impressionStopCheckpoint { + // Pause the impression timer. + [self.minImpressionTimer invalidate]; + + // Track the effective impression time for this impression session. + double effectiveImpressionTime = + [self.timeFetcher currentTimestampInSeconds] - self.currentImpressionStartTime; + self.aggregateImpressionTimeInSeconds += effectiveImpressionTime; +} + +- (void)dealloc { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID200001", + @"[FIDBaseRenderingViewController dealloc] triggered"); + [self.minImpressionTimer invalidate]; + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + +- (void)appDidBecomeInactive:(UIApplication *)application { + [self impressionStopCheckpoint]; +} + +- (void)appDidBecomeActive:(UIApplication *)application { + [self impressionStartCheckpoint]; +} + +- (void)minImpressionTimeReached { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID200004", + @"Min impression time has been reached."); + + if ([self.displayDelegate respondsToSelector:@selector(impressionDetected)]) { + [self.displayDelegate impressionDetected]; + } + + [NSNotificationCenter.defaultCenter removeObserver:self]; +} + +- (void)setupMinImpressionTimer { + NSTimeInterval remaining = kMinValidImpressionTime - self.aggregateImpressionTimeInSeconds; + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID200006", + @"Remaining minimal impression time is %lf", remaining); + + if (remaining < 0.00001) { + return; + } + + __weak id weakSelf = self; + self.minImpressionTimer = + [NSTimer scheduledTimerWithTimeInterval:remaining + target:weakSelf + selector:@selector(minImpressionTimeReached) + userInfo:nil + repeats:NO]; +} + +- (void)dismissView:(FIRInAppMessagingDismissType)dismissType { + [self.view.window setHidden:YES]; + // This is for the purpose of releasing the potential memory associated with the image view. + self.view.window.rootViewController = nil; + + if (self.displayDelegate) { + [self.displayDelegate messageDismissedWithType:dismissType]; + } else { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID200007", + @"Display delegate is nil while message is being dismissed."); + } + return; +} + +- (void)followActionURL { + [self.view.window setHidden:YES]; + // This is for the purpose of releasing the potential memory associated with the image view. + self.view.window.rootViewController = nil; + + if (self.displayDelegate) { + [self.displayDelegate messageClicked]; + } else { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID200008", + @"Display delegate is nil while trying to follow action URL."); + } + return; +} +@end diff --git a/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.h b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.h new file mode 100644 index 00000000000..602821d0caf --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.h @@ -0,0 +1,37 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN +/** + * To avoid the risk of hijacking the app's existing view transition flow, we render in-app message + * views in a top level UI Window instead of presenting from app's existing UIWindow. The caller is + * supposed to set the rootViewController to be the appropriate view controller for the in-app + * message and call setHidden:NO to make it really visible. + */ +@interface FIDRenderingWindowHelper : NSObject + +// Return the singleton UIWindow that can be used for rendering modal IAM views ++ (UIWindow *)UIWindowForModalView; + +// Return the singleton UIWindow that can be used for rendering banner IAM views ++ (UIWindow *)UIWindowForBannerView; + +// Return the singleton UIWindow that can be used for rendering banner IAM views ++ (UIWindow *)UIWindowForImageOnlyView; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m new file mode 100644 index 00000000000..6714dbd640e --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIDRenderingWindowHelper.m @@ -0,0 +1,59 @@ +/* + * Copyright 2018 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 "FIDRenderingWindowHelper.h" +#import "FIDBannerViewUIWindow.h" + +@implementation FIDRenderingWindowHelper + ++ (UIWindow *)UIWindowForModalView { + static UIWindow *UIWindowForModal; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + UIWindowForModal = [[UIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForModal.windowLevel = UIWindowLevelNormal; + }); + return UIWindowForModal; +} + ++ (UIWindow *)UIWindowForBannerView { + static UIWindow *UIWindowForBanner; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + UIWindowForBanner = [[FIDBannerViewUIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForBanner.windowLevel = UIWindowLevelNormal; + }); + + return UIWindowForBanner; +} + ++ (UIWindow *)UIWindowForImageOnlyView { + static UIWindow *UIWindowForImageOnly; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + UIWindow *appWindow = [[[UIApplication sharedApplication] delegate] window]; + UIWindowForImageOnly = [[UIWindow alloc] initWithFrame:[appWindow frame]]; + UIWindowForImageOnly.windowLevel = UIWindowLevelNormal; + }); + + return UIWindowForImageOnly; +} +@end diff --git a/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.h b/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.h new file mode 100644 index 00000000000..6e18a9a5037 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.h @@ -0,0 +1,29 @@ +/* + * Copyright 2018 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 + +// This file contains declarations that should go into FirebaseCore when +// Firebase InAppMessagingDisplay is merged into master. Keep them separate now to help +// with build from development folder and avoid merge conflicts. + +extern FIRLoggerService kFIRLoggerInAppMessagingDisplay; + +// this should eventually be in FIRError.h +extern NSString *const kFirebaseInAppMessagingDisplayErrorDomain; + +// this should eventually be in FIRError.h FIRAppInternal.h:46: +extern NSString *const kFIRServiceInAppMessagingDisplay; diff --git a/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.m b/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.m new file mode 100644 index 00000000000..6f6dcc3de88 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIRCore+InAppMessagingDisplay.m @@ -0,0 +1,22 @@ +/* + * Copyright 2018 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 +#import "FIRCore+InAppMessagingDisplay.h" + +NSString *const kFIRServiceInAppMessagingDisplay = @"InAppMessagingDisplay"; +NSString *const kFirebaseInAppMessagingDisplayErrorDomain = @"com.firebase.inappmessaging.display"; +FIRLoggerService kFIRLoggerInAppMessagingDisplay = @"[Firebase/InAppMessagingDisplay]"; diff --git a/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m b/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m new file mode 100644 index 00000000000..c8dbbae7a70 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/FIRIAMDefaultDisplayImpl.m @@ -0,0 +1,214 @@ +/* + * Copyright 2018 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 + +#import +#import +#import + +#import "FIDBannerViewController.h" +#import "FIDImageOnlyViewController.h" +#import "FIDModalViewController.h" +#import "FIDRenderingWindowHelper.h" +#import "FIDTimeFetcher.h" +#import "FIRCore+InAppMessagingDisplay.h" +#import "FIRIAMDefaultDisplayImpl.h" + +@implementation FIRIAMDefaultDisplayImpl + ++ (void)load { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(didReceiveConfigureSDKNotification:) + name:kFIRAppReadyToConfigureSDKNotification + object:nil]; +} + ++ (void)didReceiveConfigureSDKNotification:(NSNotification *)notification { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID100010", + @"Got notification for kFIRAppReadyToConfigureSDKNotification. Setting display " + "component on headless SDK."); + + FIRIAMDefaultDisplayImpl *display = [[FIRIAMDefaultDisplayImpl alloc] init]; + [FIRInAppMessaging inAppMessaging].messageDisplayComponent = display; +} + ++ (NSBundle *)getViewResourceBundle { + static NSBundle *resourceBundle; + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + // TODO. This logic of finding the resource bundle may need to change once it's open + // sourced + NSBundle *containingBundle = [NSBundle mainBundle]; + // This is assuming the display resource bundle is contained in the main bundle + NSURL *bundleURL = + [containingBundle URLForResource:@"InAppMessagingDisplayResources" withExtension:@"bundle"]; + resourceBundle = [NSBundle bundleWithURL:bundleURL]; + + if (resourceBundle == nil) { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID100007", + @"FIAM Display Resource bundle " + "is missing: not contained within bundle %@", + containingBundle); + } + }); + return resourceBundle; +} + ++ (void)displayModalViewWithMessageDefinition:(FIRInAppMessagingModalDisplay *)modalMessage + displayDelegate: + (id)displayDelegate { + NSBundle *resourceBundle = [self getViewResourceBundle]; + + if (resourceBundle == nil) { + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{@"message" : @"resource bundle is missing"}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + FIDTimerWithNSDate *timeFetcher = [[FIDTimerWithNSDate alloc] init]; + FIDModalViewController *modalVC = + [FIDModalViewController instantiateViewControllerWithResourceBundle:resourceBundle + displayMessage:modalMessage + displayDelegate:displayDelegate + timeFetcher:timeFetcher]; + + if (modalVC == nil) { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID100004", + @"View controller can not be created."); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + UIWindow *displayUIWindow = [FIDRenderingWindowHelper UIWindowForModalView]; + displayUIWindow.rootViewController = modalVC; + [displayUIWindow setHidden:NO]; + }); +} + ++ (void)displayBannerViewWithMessageDefinition:(FIRInAppMessagingBannerDisplay *)bannerMessage + displayDelegate: + (id)displayDelegate { + NSBundle *resourceBundle = [self getViewResourceBundle]; + + if (resourceBundle == nil) { + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + FIDTimerWithNSDate *timeFetcher = [[FIDTimerWithNSDate alloc] init]; + FIDBannerViewController *bannerVC = + [FIDBannerViewController instantiateViewControllerWithResourceBundle:resourceBundle + displayMessage:bannerMessage + displayDelegate:displayDelegate + timeFetcher:timeFetcher]; + + if (bannerVC == nil) { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID100008", + @"Banner view controller can not be created."); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + UIWindow *displayUIWindow = [FIDRenderingWindowHelper UIWindowForBannerView]; + displayUIWindow.rootViewController = bannerVC; + [displayUIWindow setHidden:NO]; + }); +} + ++ (void)displayImageOnlyViewWithMessageDefinition: + (FIRInAppMessagingImageOnlyDisplay *)imageOnlyMessage + displayDelegate: + (id)displayDelegate { + NSBundle *resourceBundle = [self getViewResourceBundle]; + + if (resourceBundle == nil) { + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + FIDTimerWithNSDate *timeFetcher = [[FIDTimerWithNSDate alloc] init]; + FIDImageOnlyViewController *imageOnlyVC = + [FIDImageOnlyViewController instantiateViewControllerWithResourceBundle:resourceBundle + displayMessage:imageOnlyMessage + displayDelegate:displayDelegate + timeFetcher:timeFetcher]; + + if (imageOnlyVC == nil) { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID100006", + @"Image only view controller can not be created."); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + return; + } + + UIWindow *displayUIWindow = [FIDRenderingWindowHelper UIWindowForImageOnlyView]; + displayUIWindow.rootViewController = imageOnlyVC; + [displayUIWindow setHidden:NO]; + }); +} + +#pragma mark - protocol FIRInAppMessagingDisplay +- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay + displayDelegate:(id)displayDelegate { + if ([messageForDisplay isKindOfClass:[FIRInAppMessagingModalDisplay class]]) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID100000", @"Display a modal message"); + [self.class displayModalViewWithMessageDefinition:(FIRInAppMessagingModalDisplay *) + messageForDisplay + displayDelegate:displayDelegate]; + + } else if ([messageForDisplay isKindOfClass:[FIRInAppMessagingBannerDisplay class]]) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID100001", @"Display a banner message"); + [self.class displayBannerViewWithMessageDefinition:(FIRInAppMessagingBannerDisplay *) + messageForDisplay + displayDelegate:displayDelegate]; + } else if ([messageForDisplay isKindOfClass:[FIRInAppMessagingImageOnlyDisplay class]]) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID100002", @"Display an image only message"); + [self.class displayImageOnlyViewWithMessageDefinition:(FIRInAppMessagingImageOnlyDisplay *) + messageForDisplay + displayDelegate:displayDelegate]; + } else { + FIRLogWarning(kFIRLoggerInAppMessagingDisplay, @"I-FID100003", + @"Unknown message type %@ " + "Don't know how to handle it.", + messageForDisplay.class); + NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingDisplayErrorDomain + code:FIAMDisplayRenderErrorTypeUnspecifiedError + userInfo:@{}]; + [displayDelegate displayErrorEncountered:error]; + } +} +@end diff --git a/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.h b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.h new file mode 100644 index 00000000000..ac3895ff4c2 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.h @@ -0,0 +1,35 @@ +/* + * Copyright 2018 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 + +#import "FIDBaseRenderingViewController.h" + +@class FIRInAppMessagingImageOnlyDisplay; +@protocol FIDTimeFetcher; +@protocol FIRInAppMessagingDisplayDelegate; + +NS_ASSUME_NONNULL_BEGIN +@interface FIDImageOnlyViewController : FIDBaseRenderingViewController ++ (FIDImageOnlyViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage: + (FIRInAppMessagingImageOnlyDisplay *)imageOnlyMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m new file mode 100644 index 00000000000..be6ec28444f --- /dev/null +++ b/Firebase/InAppMessagingDisplay/ImageOnly/FIDImageOnlyViewController.m @@ -0,0 +1,175 @@ +/* + * Copyright 2018 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 + +#import "FIDImageOnlyViewController.h" +#import "FIRCore+InAppMessagingDisplay.h" + +@interface FIDImageOnlyViewController () + +@property(nonatomic, readwrite) FIRInAppMessagingImageOnlyDisplay *imageOnlyMessage; + +@property(weak, nonatomic) IBOutlet UIImageView *imageView; +@property(weak, nonatomic) IBOutlet UIButton *closeButton; +@property(nonatomic, assign) CGSize imageOriginalSize; +@end + +@implementation FIDImageOnlyViewController + ++ (FIDImageOnlyViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage: + (FIRInAppMessagingImageOnlyDisplay *)imageOnlyMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher { + UIStoryboard *storyboard = + [UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard" bundle:resourceBundle]; + + if (storyboard == nil) { + FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300002", + @"Storyboard '" + "FIRInAppMessageDisplayStoryboard' not found in bundle %@", + resourceBundle); + return nil; + } + FIDImageOnlyViewController *imageOnlyVC = (FIDImageOnlyViewController *)[storyboard + instantiateViewControllerWithIdentifier:@"image-only-vc"]; + imageOnlyVC.displayDelegate = displayDelegate; + imageOnlyVC.imageOnlyMessage = imageOnlyMessage; + imageOnlyVC.timeFetcher = timeFetcher; + + return imageOnlyVC; +} + +- (IBAction)closeButtonClicked:(id)sender { + [self dismissView:FIRInAppMessagingDismissTypeUserTapClose]; +} + +- (void)setupRecognizers { + UITapGestureRecognizer *tapGestureRecognizer = + [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(messageTapped:)]; + tapGestureRecognizer.delaysTouchesBegan = YES; + tapGestureRecognizer.numberOfTapsRequired = 1; + + self.imageView.userInteractionEnabled = YES; + [self.imageView addGestureRecognizer:tapGestureRecognizer]; +} + +- (void)messageTapped:(UITapGestureRecognizer *)recognizer { + [self followActionURL]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + [self.view setBackgroundColor:[UIColor.grayColor colorWithAlphaComponent:0.5]]; + + if (self.imageOnlyMessage.imageData) { + UIImage *image = [UIImage imageWithData:self.imageOnlyMessage.imageData.imageRawData]; + self.imageOriginalSize = image.size; + [self.imageView setImage:image]; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + } + + [self setupRecognizers]; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + if (!self.imageOnlyMessage.imageData) { + return; + } + + // do the calculation in viewDidLayoutSubViews since self.view.window.frame is only + // reliable at this time + + // Calculate the size of the image view under the constraints: + // 1 Retain the image ratio + // 2 Have at least 30 point of margines around four sides of the image view + + CGFloat minimalMargine = 30; // 30 points + CGFloat maxImageViewWidth = self.view.window.frame.size.width - minimalMargine * 2; + CGFloat maxImageViewHeight = self.view.window.frame.size.height - minimalMargine * 2; + + CGFloat adjustedImageViewHeight = self.imageOriginalSize.height; + CGFloat adjustedImageViewWidth = self.imageOriginalSize.width; + + if (adjustedImageViewWidth > maxImageViewWidth || adjustedImageViewHeight > maxImageViewHeight) { + if (maxImageViewHeight / maxImageViewWidth > + self.imageOriginalSize.height / self.imageOriginalSize.width) { + // the image is relatively too wide compared against displayable area + adjustedImageViewWidth = maxImageViewWidth; + adjustedImageViewHeight = + adjustedImageViewWidth * self.imageOriginalSize.height / self.imageOriginalSize.width; + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID110002", + @"Use max available image display width as %lf", adjustedImageViewWidth); + } else { + // the image is relatively too narrow compared against displayable area + adjustedImageViewHeight = maxImageViewHeight; + adjustedImageViewWidth = + adjustedImageViewHeight * self.imageOriginalSize.width / self.imageOriginalSize.height; + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID110003", + @"Use max avilable image display height as %lf", adjustedImageViewHeight); + } + } else { + // image can be rendered fully at its original size + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID110001", + @"Image can be fully displayed in image only mode"); + } + + CGRect rect = CGRectMake(0, 0, adjustedImageViewWidth, adjustedImageViewHeight); + self.imageView.frame = rect; + self.imageView.center = self.view.center; + + CGFloat closeButtonCenterX = CGRectGetMaxX(self.imageView.frame); + CGFloat closeButtonCenterY = CGRectGetMinY(self.imageView.frame); + self.closeButton.center = CGPointMake(closeButtonCenterX, closeButtonCenterY); + + [self.view bringSubviewToFront:self.closeButton]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // close any potential keyboard, which would conflict with the modal in-app messagine view + [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) + to:nil + from:nil + forEvent:nil]; + if (self.imageOnlyMessage.renderAsTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID110004", + @"Flashing the close button since this is a test message."); + [self flashCloseButton:self.closeButton]; + } +} + +- (void)flashCloseButton:(UIButton *)closeButton { + closeButton.alpha = 1.0f; + [UIView animateWithDuration:2.0 + delay:0.0 + options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionRepeat | + UIViewAnimationOptionAutoreverse | + UIViewAnimationOptionAllowUserInteraction + animations:^{ + closeButton.alpha = 0.1f; + } + completion:^(BOOL finished){ + // Do nothing + }]; +} +@end diff --git a/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.h b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.h new file mode 100644 index 00000000000..db30f974f1b --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.h @@ -0,0 +1,33 @@ +/* + * Copyright 2018 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 + +#import "FIDBaseRenderingViewController.h" + +@class FIRInAppMessagingModalDisplay; +@protocol FIRInAppMessagingDisplayDelegate; + +NS_ASSUME_NONNULL_BEGIN +@interface FIDModalViewController : FIDBaseRenderingViewController ++ (FIDModalViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage:(FIRInAppMessagingModalDisplay *)modalMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m new file mode 100644 index 00000000000..30427f1a7a8 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Modal/FIDModalViewController.m @@ -0,0 +1,451 @@ +/* + * Copyright 2018 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 + +#import + +#import "FIDModalViewController.h" +#import "FIRCore+InAppMessagingDisplay.h" + +@interface FIDModalViewController () + +@property(nonatomic, readwrite) FIRInAppMessagingModalDisplay *modalDisplayMessage; + +@property(weak, nonatomic) IBOutlet UIImageView *imageView; +@property(weak, nonatomic) IBOutlet UILabel *titleLabel; +@property(weak, nonatomic) IBOutlet UIButton *actionButton; + +@property(weak, nonatomic) IBOutlet UIView *messageCardView; +@property(weak, nonatomic) IBOutlet UITextView *bodyTextView; +@property(weak, nonatomic) IBOutlet UIButton *closeButton; + +// this is only needed for removing the layout errors in interface builder. At runtime +// we determine the height via its content size. So disable this at runtime. +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *fixedMessageCardHeightConstraint; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *messageCardHeightMaxInTabletCase; + +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxActionButtonHeight; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTextViewHeightConstraint; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonTopToBodyBottomConstraint; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageActualHeightConstraint; + +// constraints manipulated further in portrait mode +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleLabelHeightConstraint; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *buttonBottomToContainerBottomInPortraitMode; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToTitleBottomInPortraitMode; + +// constraints manipulated further in landscape mode +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageWidthInLandscapeMode; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *titleTopToCardViewTop; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *cardLeadingMarginInLandscapeMode; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *maxCardHeightInLandscapeMode; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *imageTopToCardTopInLandscapeMode; +@property(weak, nonatomic) IBOutlet NSLayoutConstraint *bodyTopToTitleBottomInLandScapeMode; +@end + +static CGFloat VerticalSpacingBetweenTitleAndBody = 24; +static CGFloat VerticalSpacingBetweenBodyAndActionButton = 24; + +// the padding between the content and view card's top and bottom edges +static CGFloat TopBottomPaddingAroundContent = 24; +// the minimal padding size between msg card and app window's top and bottom +static CGFloat TopBottomPaddingAroundMsgCard = 30; + +// the horizontal spacing between image column and text/button column in landscape mode +static CGFloat LandScapePaddingBetweenImageAndTextColumn = 24; + +@implementation FIDModalViewController + ++ (FIDModalViewController *) + instantiateViewControllerWithResourceBundle:(NSBundle *)resourceBundle + displayMessage:(FIRInAppMessagingModalDisplay *)modalMessage + displayDelegate: + (id)displayDelegate + timeFetcher:(id)timeFetcher { + UIStoryboard *storyboard = + [UIStoryboard storyboardWithName:@"FIRInAppMessageDisplayStoryboard" bundle:resourceBundle]; + + if (storyboard == nil) { + FIRLogError(kFIRLoggerInAppMessagingDisplay, @"I-FID300001", + @"Storyboard '" + "FIRInAppMessageDisplayStoryboard' not found in bundle %@", + resourceBundle); + return nil; + } + FIDModalViewController *modalVC = (FIDModalViewController *)[storyboard + instantiateViewControllerWithIdentifier:@"modal-view-vc"]; + modalVC.displayDelegate = displayDelegate; + modalVC.modalDisplayMessage = modalMessage; + modalVC.timeFetcher = timeFetcher; + + return modalVC; +} + +- (IBAction)closeButtonClicked:(id)sender { + [self dismissView:FIRInAppMessagingDismissTypeUserTapClose]; +} + +- (IBAction)actionButtonTapped:(id)sender { + [self followActionURL]; +} + +- (void)viewDidLoad { + [super viewDidLoad]; + + // make the background half transparent + [self.view setBackgroundColor:[UIColor.grayColor colorWithAlphaComponent:0.5]]; + + // populating values for display elements + + self.titleLabel.text = self.modalDisplayMessage.title; + self.bodyTextView.text = self.modalDisplayMessage.bodyText; + + if (self.modalDisplayMessage.imageData) { + [self.imageView + setImage:[UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData]]; + self.imageView.contentMode = UIViewContentModeScaleAspectFit; + } + + self.messageCardView.backgroundColor = self.modalDisplayMessage.displayBackgroundColor; + self.messageCardView.layer.cornerRadius = 4; + + self.titleLabel.textColor = self.modalDisplayMessage.textColor; + self.bodyTextView.textColor = self.modalDisplayMessage.textColor; + self.bodyTextView.selectable = NO; + + if (self.modalDisplayMessage.actionButton.buttonText.length != 0) { + [self.actionButton setTitle:self.modalDisplayMessage.actionButton.buttonText + forState:UIControlStateNormal]; + self.actionButton.backgroundColor = self.modalDisplayMessage.actionButton.buttonBackgroundColor; + [self.actionButton setTitleColor:self.modalDisplayMessage.actionButton.buttonTextColor + forState:UIControlStateNormal]; + self.actionButton.layer.cornerRadius = 4; + + if (self.modalDisplayMessage.bodyText.length == 0) { + self.buttonTopToBodyBottomConstraint.constant = 0; + } + } else { + // either action button text is empty or nil + + // hide the action button and reclaim the space below the buttom + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300002", + @"Modal view to be rendered without action button"); + self.maxActionButtonHeight.constant = 0; + self.actionButton.clipsToBounds = YES; + self.buttonTopToBodyBottomConstraint.constant = 0; + } + + [self.view addConstraint:self.imageActualHeightConstraint]; + self.imageActualHeightConstraint.active = YES; + self.fixedMessageCardHeightConstraint.active = NO; +} + +// for text display UIview, which could be a UILabel or UITextView, decide the fit height under a +// given display width +- (CGFloat)determineTextAreaViewFitHeightForView:(UIView *)textView + withWidth:(CGFloat)displayWidth { + CGSize displaySize = CGSizeMake(displayWidth, FLT_MAX); + return [textView sizeThatFits:displaySize].height; +} + +// In both landscape or portrait mode, the title, body & button are aligned vertically and they form +// together have an impact on the height for that column. Many times, we need to calculate a +// suitable heights for them to help decide the layout. The height calculation is influced by quite +// a few factors: the text lenght of title and body, the presence/absense of body & button and +// available card/window sizes. So these are wrapped within +// estimateTextButtomColumnHeightWithDisplayWidth which produce a TitleBodyButtonHeightInfo struct +// to give the estimates of the heights of different elements. +struct TitleBodyButtonHeightInfo { + CGFloat titleHeight; + CGFloat bodyHeight; + + // this is the total height of title plus body plus the button. Notice that button or body are + // optional and the result totaColumnlHeight factor in these cases correctly + CGFloat totaColumnlHeight; +}; + +- (struct TitleBodyButtonHeightInfo)estimateTextBtnColumnHeightWithDisplayWidth: + (CGFloat)displayWidth + withMaxColumnHeight:(CGFloat)maxHeight { + struct TitleBodyButtonHeightInfo resultHeightInfo; + + CGFloat titleFitHeight = + [self determineTextAreaViewFitHeightForView:self.titleLabel withWidth:displayWidth]; + CGFloat bodyFitHeight = + self.modalDisplayMessage.bodyText.length == 0 + ? 0 + : [self determineTextAreaViewFitHeightForView:self.bodyTextView withWidth:displayWidth]; + + CGFloat bodyFitHeightWithPadding = self.modalDisplayMessage.bodyText.length == 0 + ? 0 + : bodyFitHeight + VerticalSpacingBetweenTitleAndBody; + + CGFloat buttonHeight = + self.modalDisplayMessage.actionButton == nil + ? 0 + : self.actionButton.frame.size.height + VerticalSpacingBetweenBodyAndActionButton; + + // we keep the spacing even if body or button is absent. + CGFloat fitColumnHeight = titleFitHeight + bodyFitHeightWithPadding + buttonHeight; + + if (fitColumnHeight < maxHeight) { + // every element get space that can fit the content + resultHeightInfo.bodyHeight = bodyFitHeight; + resultHeightInfo.titleHeight = titleFitHeight; + resultHeightInfo.totaColumnlHeight = fitColumnHeight; + } else { + // need to restrict heights of certain elements + resultHeightInfo.totaColumnlHeight = maxHeight; + if (self.modalDisplayMessage.bodyText.length == 0) { + // no message body, title will try to expand to take all the available height + resultHeightInfo.bodyHeight = 0; + if (self.modalDisplayMessage.actionButton == nil) { + resultHeightInfo.titleHeight = maxHeight; + } else { + // button height, if not 0, already accommodates the space above it + resultHeightInfo.titleHeight = maxHeight - buttonHeight; + } + } else { + // first give title up to 40% of available height + resultHeightInfo.titleHeight = fmin(titleFitHeight, maxHeight * 2 / 5); + + CGFloat availableBodyHeight = 0; + if (self.modalDisplayMessage.actionButton == nil) { + availableBodyHeight = + maxHeight - resultHeightInfo.titleHeight - VerticalSpacingBetweenTitleAndBody; + } else { + // body takes the rest minus button space + availableBodyHeight = maxHeight - resultHeightInfo.titleHeight - buttonHeight - + VerticalSpacingBetweenTitleAndBody; + } + + if (availableBodyHeight > bodyFitHeight) { + resultHeightInfo.bodyHeight = bodyFitHeight; + // give some back to title height since body does not use up all the allocation + resultHeightInfo.titleHeight += (availableBodyHeight - bodyFitHeight); + } else { + resultHeightInfo.bodyHeight = availableBodyHeight; + } + } + } + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300003", + @"In heights calculation (max-height = %lf, width = %lf), title heights is %lf, " + "body height is %lf, button height is %lf, total column heights are %lf", + maxHeight, displayWidth, resultHeightInfo.titleHeight, resultHeightInfo.bodyHeight, + buttonHeight, resultHeightInfo.totaColumnlHeight); + + return resultHeightInfo; +} + +// the following two layoutFineTunexx methods make additional adjustments for the view layout +// in portrait and landscape mode respectively. They are supposed to be triggered from +// viewDidLayoutSubviews since certain dimension sizes are only available there +- (void)layoutFineTuneInPortraitMode { + // for tablet case, since we use a fixed card height, the reference would be just the card height + // for non-tablet case, we want to use a dynamic height , so the reference would be the window + // height + CGFloat heightCalcReference = + self.messageCardHeightMaxInTabletCase.active + ? self.messageCardView.frame.size.height - TopBottomPaddingAroundContent * 2 + : self.view.window.frame.size.height - TopBottomPaddingAroundContent * 2 - + TopBottomPaddingAroundMsgCard * 2; + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300004", + @"The height calc reference is %lf " + "with frame height as %lf", + heightCalcReference, self.view.window.frame.size.height); + + // this makes sure titleLable gets correct width to be ready for later's height estimate for the + // text & button column + [self.messageCardView layoutIfNeeded]; + + // we reserve approximately 1/3 vertical space for image + CGFloat textBtnTotalAvailableHeight = + self.modalDisplayMessage.imageData ? heightCalcReference * 2 / 3 : heightCalcReference; + + struct TitleBodyButtonHeightInfo heights = + [self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width + withMaxColumnHeight:textBtnTotalAvailableHeight]; + + self.titleLabelHeightConstraint.constant = heights.titleHeight; + self.bodyTextViewHeightConstraint.constant = heights.bodyHeight; + + if (self.modalDisplayMessage.imageData) { + UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData]; + CGSize imageAvailableSpace = CGSizeMake(self.titleLabel.frame.size.width, + heightCalcReference - heights.totaColumnlHeight - + self.imageTopToTitleBottomInPortraitMode.constant); + + CGSize imageDisplaySize = + [self fitImageInRegionSize:imageAvailableSpace withImageSize:image.size]; + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300005", + @"Given actual image size %@ and available image display size %@, the actual" + "image display size is %@", + NSStringFromCGSize(image.size), NSStringFromCGSize(imageAvailableSpace), + NSStringFromCGSize(imageDisplaySize)); + + // for portrait mode, no need to change image width since no content is shown side to + // the image + self.imageActualHeightConstraint.constant = imageDisplaySize.height; + } else { + // no image case + self.imageActualHeightConstraint.constant = 0; + self.imageTopToTitleBottomInPortraitMode.constant = 0; + } +} + +- (CGSize)fitImageInRegionSize:(CGSize)regionSize withImageSize:(CGSize)imageSize { + if (imageSize.height <= regionSize.height && imageSize.width <= regionSize.width) { + return imageSize; // image can be fully rendered at its original dimension + } else { + CGFloat regionRatio = regionSize.width / regionSize.height; + CGFloat imageRaio = imageSize.width / imageSize.height; + + if (regionRatio < imageRaio) { + // bound on the width dimension + return CGSizeMake(regionSize.width, regionSize.width / imageRaio); + } else { + return CGSizeMake(regionSize.height * imageRaio, regionSize.height); + } + } +} + +// for devices of 4 inches or below (iphone se, iphone 5/5s and iphone 4s), reduce +// the padding sizes between elements in the text/button column for landscape mode +- (void)applySmallerSpacingForInLandscapeMode { + if (self.modalDisplayMessage.bodyText.length != 0) { + VerticalSpacingBetweenTitleAndBody = self.bodyTopToTitleBottomInLandScapeMode.constant = 12; + } + + if (self.modalDisplayMessage.actionButton != nil && + self.modalDisplayMessage.bodyText.length != 0) { + VerticalSpacingBetweenBodyAndActionButton = self.buttonTopToBodyBottomConstraint.constant = 12; + } +} + +- (void)layoutFineTuneInLandscapeMode { + // smaller spacing threshold is applied for screens equal or larger than 4.7 inches + if (self.view.window.frame.size.height <= 321) { + [self applySmallerSpacingForInLandscapeMode]; + } + + if (self.modalDisplayMessage.imageData) { + UIImage *image = [UIImage imageWithData:self.modalDisplayMessage.imageData.imageRawData]; + + CGFloat maxImageHeight = self.view.window.frame.size.height - + TopBottomPaddingAroundContent * 2 - TopBottomPaddingAroundMsgCard * 2; + CGFloat maxImageWidth = self.messageCardView.frame.size.width * 2 / 5; + CGSize imageDisplaySize = [self fitImageInRegionSize:CGSizeMake(maxImageWidth, maxImageHeight) + withImageSize:image.size]; + + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300008", + @"In landscape mode, image fit size is %@", NSStringFromCGSize(imageDisplaySize)); + + // resize image per imageSize + self.imageWidthInLandscapeMode.constant = imageDisplaySize.width; + self.imageActualHeightConstraint.constant = imageDisplaySize.height; + + // now we can estimate the new card width given the desired image size + + // this assumes we use half of the window width for diplaying the text/button column + CGFloat cardFitWidth = imageDisplaySize.width + self.view.window.frame.size.width / 2 + + LandScapePaddingBetweenImageAndTextColumn; + + self.cardLeadingMarginInLandscapeMode.constant = + fmax(15, (self.view.window.frame.size.width - cardFitWidth) / 2); + } else { + self.imageWidthInLandscapeMode.constant = 0; + self.imageActualHeightConstraint.constant = 0; + + // card would be of 3/5 width of the screen in landscape + self.cardLeadingMarginInLandscapeMode.constant = self.view.window.frame.size.width / 5; + } + + // this makes sure titleLable gets correct width to be ready for later's height estimate for the + // text & button column + [self.messageCardView layoutIfNeeded]; + + struct TitleBodyButtonHeightInfo heights = + [self estimateTextBtnColumnHeightWithDisplayWidth:self.titleLabel.frame.size.width + withMaxColumnHeight:self.view.frame.size.height - + TopBottomPaddingAroundContent * 2 - + TopBottomPaddingAroundMsgCard * 2]; + + self.titleLabelHeightConstraint.constant = heights.titleHeight; + self.bodyTextViewHeightConstraint.constant = heights.bodyHeight; + + // Adjust the height of the card + // are we bound by the text/button column height or image height ? + CGFloat cardHeight = fmax(self.imageActualHeightConstraint.constant, heights.totaColumnlHeight) + + TopBottomPaddingAroundContent * 2; + self.maxCardHeightInLandscapeMode.constant = cardHeight; + + // with the new card height, align the image and the text/btn column to center vertically + self.imageTopToCardTopInLandscapeMode.constant = + (cardHeight - self.imageActualHeightConstraint.constant) / 2; + self.titleTopToCardViewTop.constant = (cardHeight - heights.totaColumnlHeight) / 2; +} + +- (void)viewDidLayoutSubviews { + [super viewDidLayoutSubviews]; + + if (self.buttonBottomToContainerBottomInPortraitMode.active) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300009", + @"Modal view rendered in portrait mode"); + [self layoutFineTuneInPortraitMode]; + } else { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300010", + @"Modal view rendered in landscape mode"); + [self layoutFineTuneInLandscapeMode]; + } + + // always scroll to the top in case the body area is scrollable + [self.bodyTextView setContentOffset:CGPointZero]; +} + +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + // close any potential keyboard, which would conflict with the modal in-app messagine view + [[UIApplication sharedApplication] sendAction:@selector(resignFirstResponder) + to:nil + from:nil + forEvent:nil]; + + if (self.modalDisplayMessage.renderAsTestMessage) { + FIRLogDebug(kFIRLoggerInAppMessagingDisplay, @"I-FID300011", + @"Flushing the close button since this is a test message."); + [self flashCloseButton:self.closeButton]; + } +} + +- (void)flashCloseButton:(UIButton *)closeButton { + closeButton.alpha = 1.0f; + [UIView animateWithDuration:2.0 + delay:0.0 + options:UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionRepeat | + UIViewAnimationOptionAutoreverse | + UIViewAnimationOptionAllowUserInteraction + animations:^{ + closeButton.alpha = 0.1f; + } + completion:^(BOOL finished){ + // Do nothing + }]; +} +@end diff --git a/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h b/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h new file mode 100644 index 00000000000..41f6ba39d5a --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Public/FIRIAMDefaultDisplayImpl.h @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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 +#import + +NS_ASSUME_NONNULL_BEGIN +NS_SWIFT_NAME(InAppMessagingDefaultDisplayImpl) +/** + * Public class for displaying fiam messages. Most apps should not use it since its instance + * would be instantiated upon SDK start-up automatically. It's exposed in public interface + * to help UI Testing app access the UI layer directly. + */ +@interface FIRIAMDefaultDisplayImpl : NSObject +- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay + displayDelegate:(id)displayDelegate; +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/README.md b/Firebase/InAppMessagingDisplay/README.md new file mode 100644 index 00000000000..872eff63623 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/README.md @@ -0,0 +1,5 @@ +FirebaseInAppMessagingDisplay is the default UI implementation from Firebase for +rendering In-App Messaging messges to end users. + +Apps can also provide custom UI implementation to replace it. Check out our guides +for details. diff --git a/Firebase/InAppMessagingDisplay/Resources/FIRInAppMessageDisplayStoryboard.storyboard b/Firebase/InAppMessagingDisplay/Resources/FIRInAppMessageDisplayStoryboard.storyboard new file mode 100644 index 00000000000..dfe971e6881 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Resources/FIRInAppMessageDisplayStoryboard.storyboard @@ -0,0 +1,393 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Firebase/InAppMessagingDisplay/Resources/close-with-transparency.png b/Firebase/InAppMessagingDisplay/Resources/close-with-transparency.png new file mode 100644 index 00000000000..6ddd93c2629 Binary files /dev/null and b/Firebase/InAppMessagingDisplay/Resources/close-with-transparency.png differ diff --git a/Firebase/InAppMessagingDisplay/Resources/close-with-transparency@2x.png b/Firebase/InAppMessagingDisplay/Resources/close-with-transparency@2x.png new file mode 100644 index 00000000000..2a81a678081 Binary files /dev/null and b/Firebase/InAppMessagingDisplay/Resources/close-with-transparency@2x.png differ diff --git a/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.h b/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.h new file mode 100644 index 00000000000..9e25085bf3e --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.h @@ -0,0 +1,28 @@ +/* + * Copyright 2018 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 + +NS_ASSUME_NONNULL_BEGIN +// A protocol wrapping around function of getting timestamp. Created to help +// unit testing in which we need to control the elapsed time. +@protocol FIDTimeFetcher +- (NSTimeInterval)currentTimestampInSeconds; +@end + +@interface FIDTimerWithNSDate : NSObject +@end +NS_ASSUME_NONNULL_END diff --git a/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.m b/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.m new file mode 100644 index 00000000000..967c385bd91 --- /dev/null +++ b/Firebase/InAppMessagingDisplay/Util/FIDTimeFetcher.m @@ -0,0 +1,23 @@ +/* + * Copyright 2018 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 "FIDTimeFetcher.h" + +@implementation FIDTimerWithNSDate +- (NSTimeInterval)currentTimestampInSeconds { + return [[NSDate date] timeIntervalSince1970]; +} +@end diff --git a/FirebaseInAppMessagingDisplay.podspec b/FirebaseInAppMessagingDisplay.podspec new file mode 100644 index 00000000000..822c725d761 --- /dev/null +++ b/FirebaseInAppMessagingDisplay.podspec @@ -0,0 +1,43 @@ +Pod::Spec.new do |s| + s.name = 'FirebaseInAppMessagingDisplay' + s.version = '0.1.0' + s.summary = 'Firebase In-App Messaging UI for iOS' + + s.description = <<-DESC +FirebaseInAppMessagingDisplay is the default client UI implementation for +Firebase In-App Messaging SDK. + DESC + + s.homepage = 'https://firebase.google.com' + s.license = { :type => 'Apache', :file => 'LICENSE' } + s.authors = 'Google, Inc.' + + s.source = { + :git => 'https://github.com/firebase/firebase-ios-sdk.git', + :tag => s.version.to_s + } + s.social_media_url = 'https://twitter.com/Firebase' + s.ios.deployment_target = '8.0' + + s.cocoapods_version = '>= 1.4.0' + s.static_framework = true + s.prefix_header_file = false + + base_dir = "Firebase/InAppMessagingDisplay/" + s.source_files = base_dir + '**/*.[mh]' + s.public_header_files = base_dir + 'Public/*.h' + + s.resource_bundles = { + 'InAppMessagingDisplayResources' => [ base_dir + 'Resources/*.xib', + base_dir + 'Resources/*.storyboard', + base_dir + 'Resources/*.png'] + } + + s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' => + '$(inherited) ' + + 'FIRInAppMessagingDisplay_LIB_VERSION=' + String(s.version) + } + + s.dependency 'FirebaseCore' + s.dependency 'FirebaseInAppMessaging', '>=0.12.0' +end diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/AppDelegate.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/AppDelegate.swift new file mode 100644 index 00000000000..71a41a392fb --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/AppDelegate.swift @@ -0,0 +1,48 @@ +/* + * Copyright 2018 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 UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } +} diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000000..d8db8d65fd7 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "20x20", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "20x20", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "1x" + }, + { + "idiom" : "ipad", + "size" : "76x76", + "scale" : "2x" + }, + { + "idiom" : "ipad", + "size" : "83.5x83.5", + "scale" : "2x" + }, + { + "idiom" : "ios-marketing", + "size" : "1024x1024", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/Contents.json b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/Contents.json new file mode 100644 index 00000000000..da4a164c918 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift new file mode 100644 index 00000000000..91351b1c648 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/BannerMessageViewController.swift @@ -0,0 +1,130 @@ +/* + * Copyright 2018 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 UIKit + +import FirebaseInAppMessagingDisplay + +import FirebaseInAppMessaging + +class BannerMessageViewController: CommonMessageTestVC { + let displayImpl = InAppMessagingDefaultDisplayImpl() + + @IBOutlet var verifyLabel: UILabel! + + override func messageClicked() { + super.messageClicked() + verifyLabel.text = "message clicked!" + } + + override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { + super.messageClicked() + verifyLabel.text = "message dismissed!" + } + + @IBAction func showRegularBannerTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showBannerViewWithoutImageTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showBannerViewWithWideImageTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 800, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showBannerViewWithNarrowImageTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 800)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showBannerViewWithLargeBodyTextTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: longBodyText, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showBannerViewWithLongTitleTextTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingBannerDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: longTitleText, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } +} diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/LaunchScreen.storyboard b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 00000000000..f83f6fd5810 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/Main.storyboard b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/Main.storyboard new file mode 100644 index 00000000000..96f8aebeed7 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Base.lproj/Main.storyboard @@ -0,0 +1,335 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift new file mode 100644 index 00000000000..81ca8567622 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/CommonMessageTestVC.swift @@ -0,0 +1,76 @@ +/* + * Copyright 2018 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 Foundation + +import FirebaseInAppMessaging + +class CommonMessageTestVC: UIViewController, InAppMessagingDisplayDelegate { + var messageClosedWithClick = false + + var messageClosedDismiss = false + + // start of InAppMessagingDisplayDelegate functions + func messageClicked() { + print("message clicked to follow action url") + messageClosedWithClick = true + } + + func impressionDetected() { print("valid impression detected") } + func displayErrorEncountered(_ error: Error) { print("error encountered \(error)") } + func messageDismissed(dismissType: FIRInAppMessagingDismissType) { + print("message dimissed with type \(dismissType)") + messageClosedDismiss = true + } + + // end of InAppMessagingDisplayDelegate functions + + let normalMessageTitle = "Firebase In-App Message title" + let normalMessageBody = "Firebase In-App Message body" + let longBodyText = String(repeating: "This is long message body.", count: 40) + "End of body text." + let longTitleText = String(repeating: "This is long message title.", count: 10) + "End of title text." + + let startTime = Date().timeIntervalSince1970 + let endTime = Date().timeIntervalSince1970 + 1000 + + let defaultActionButton = InAppMessagingActionButton(buttonText: "Take action", + buttonTextColor: UIColor.black, + backgroundColor: UIColor.yellow) + + func produceImageOfSize(size: CGSize) -> Data? { + let color = UIColor.cyan + + let rect = CGRect(origin: .zero, size: size) + UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0) + color.setFill() + UIRectFill(rect) + + if let context = UIGraphicsGetCurrentContext() { + context.setStrokeColor(UIColor.red.cgColor) + context.strokeEllipse(in: rect) + } + + let imageFromGraphics = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + + if let image = imageFromGraphics { + return UIImagePNGRepresentation(image) + } else { + return nil + } + } +} diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift new file mode 100644 index 00000000000..181eff4cbee --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ImageOnlyMessageViewController.swift @@ -0,0 +1,93 @@ +/* + * Copyright 2018 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 UIKit +import FirebaseInAppMessaging +import FirebaseInAppMessagingDisplay + +class ImageOnlyMessageViewController: CommonMessageTestVC { + let displayImpl = InAppMessagingDefaultDisplayImpl() + + @IBOutlet var verifyLabel: UILabel! + + override func messageClicked() { + super.messageClicked() + verifyLabel.text = "message clicked!" + } + + override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { + super.messageClicked() + verifyLabel.text = "message dismissed!" + } + + @IBAction func showRegularImageOnlyTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + renderAsTestMessage: false, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showImageViewWithLargeImageDimensionTapped(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 1000, height: 1000)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + renderAsTestMessage: false, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showImageViewWithWideImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 1000, height: 100)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + renderAsTestMessage: false, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showImageViewWithNarrowImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 100, height: 1000)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + renderAsTestMessage: false, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showImageViewWithSmallImageDimensionTapped(_ sender: Any) { + let imageRawData = produceImageOfSize(size: CGSize(width: 50, height: 50)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingImageOnlyDisplay(messageID: "messageId", + renderAsTestMessage: false, + imageData: fiamImageData) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } +} diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Info.plist b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Info.plist new file mode 100644 index 00000000000..16be3b68112 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift new file mode 100644 index 00000000000..c60919e9df8 --- /dev/null +++ b/InAppMessagingDisplay/Example/FiamDisplaySwiftExample/ModalMessageViewController.swift @@ -0,0 +1,215 @@ +/* + * Copyright 2018 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 UIKit +import FirebaseInAppMessagingDisplay +import FirebaseInAppMessaging + +class ModalMessageViewController: CommonMessageTestVC { + let displayImpl = InAppMessagingDefaultDisplayImpl() + + @IBOutlet var verifyLabel: UILabel! + + override func messageClicked() { + super.messageClicked() + verifyLabel.text = "message clicked!" + } + + override func messageDismissed(dismissType dimissType: FIRInAppMessagingDismissType) { + super.messageClicked() + verifyLabel.text = "message dismissed!" + } + + @IBAction func showRegular(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithoutImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithoutButton(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionButton: nil) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithoutImageAndButton(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: nil) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithLargeBody(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: longBodyText, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithLargeTitleAndBody(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: longBodyText, + bodyText: longBodyText, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithLargeTitle(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: longBodyText, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithLargeTitleAndBodyWithoutImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: longBodyText, + bodyText: longBodyText, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithLargeTitleWithoutBodyWithoutImageWithoutButton(_ sender: Any) { + verifyLabel.text = "Verification Label" + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: longBodyText, + bodyText: "", + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: nil, + actionButton: nil) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithWideImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 600, height: 200)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + @IBAction func showWithThinImage(_ sender: Any) { + verifyLabel.text = "Verification Label" + let imageRawData = produceImageOfSize(size: CGSize(width: 200, height: 600)) + let fiamImageData = InAppMessagingImageData(imageURL: "url not important", imageData: imageRawData!) + + let modalMessage = InAppMessagingModalDisplay(messageID: "messageId", + renderAsTestMessage: false, + titleText: normalMessageTitle, + bodyText: normalMessageBody, + textColor: UIColor.black, + backgroundColor: UIColor.blue, + imageData: fiamImageData, + actionButton: defaultActionButton) + + displayImpl.displayMessage(modalMessage, displayDelegate: self) + } + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override func didReceiveMemoryWarning() { + super.didReceiveMemoryWarning() + // Dispose of any resources that can be recreated. + } +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj b/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..ffc9a6bff22 --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-Sample.xcodeproj/project.pbxproj @@ -0,0 +1,583 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5EACA9F21E16E885EEEA6AC5 /* Pods_FiamDisplaySwiftExample.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E5AD224A78D3137046BB5AB /* Pods_FiamDisplaySwiftExample.framework */; }; + AD7200B52124D19200AFD5F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200B42124D19200AFD5F3 /* AppDelegate.swift */; }; + AD7200BA2124D19200AFD5F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7200B82124D19200AFD5F3 /* Main.storyboard */; }; + AD7200BC2124D19400AFD5F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD7200BB2124D19400AFD5F3 /* Assets.xcassets */; }; + AD7200BF2124D19400AFD5F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7200BD2124D19400AFD5F3 /* LaunchScreen.storyboard */; }; + AD7200C82124E24300AFD5F3 /* ImageOnlyMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200C52124E24200AFD5F3 /* ImageOnlyMessageViewController.swift */; }; + AD7200C92124E24300AFD5F3 /* BannerMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200C62124E24300AFD5F3 /* BannerMessageViewController.swift */; }; + AD7200CA2124E24300AFD5F3 /* ModalMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200C72124E24300AFD5F3 /* ModalMessageViewController.swift */; }; + AD7200CC2124E2A800AFD5F3 /* CommonMessageTestVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200CB2124E2A800AFD5F3 /* CommonMessageTestVC.swift */; }; + AD7200D42125F92100AFD5F3 /* InAppMessagingDisplayModalViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200D32125F92100AFD5F3 /* InAppMessagingDisplayModalViewUITests.swift */; }; + AD7200DC2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200DB2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift */; }; + AD7200DE2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200DD2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift */; }; + AD7200E02126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD7200DF2126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + AD7200D62125F92100AFD5F3 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = ADA7B5B021223CED00B1C614 /* Project object */; + proxyType = 1; + remoteGlobalIDString = AD7200B12124D19200AFD5F3; + remoteInfo = FiamDisplaySwiftExample; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0F66149C0079E8409F390CBE /* Pods-InAppMessagingDisplay-Sample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessagingDisplay-Sample.release.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessagingDisplay-Sample/Pods-InAppMessagingDisplay-Sample.release.xcconfig"; sourceTree = ""; }; + 1E5AD224A78D3137046BB5AB /* Pods_FiamDisplaySwiftExample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_FiamDisplaySwiftExample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FiamDisplaySwiftExample.release.xcconfig"; path = "Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample.release.xcconfig"; sourceTree = ""; }; + 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-FiamDisplaySwiftExample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample.debug.xcconfig"; sourceTree = ""; }; + AD7200B22124D19200AFD5F3 /* FiamDisplaySwiftExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FiamDisplaySwiftExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AD7200B42124D19200AFD5F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AD7200B92124D19200AFD5F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + AD7200BB2124D19400AFD5F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AD7200BE2124D19400AFD5F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + AD7200C02124D19400AFD5F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD7200C52124E24200AFD5F3 /* ImageOnlyMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageOnlyMessageViewController.swift; sourceTree = ""; }; + AD7200C62124E24300AFD5F3 /* BannerMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BannerMessageViewController.swift; sourceTree = ""; }; + AD7200C72124E24300AFD5F3 /* ModalMessageViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ModalMessageViewController.swift; sourceTree = ""; }; + AD7200CB2124E2A800AFD5F3 /* CommonMessageTestVC.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CommonMessageTestVC.swift; sourceTree = ""; }; + AD7200D12125F92100AFD5F3 /* InAppMessagingDisplay-UITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "InAppMessagingDisplay-UITests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; + AD7200D32125F92100AFD5F3 /* InAppMessagingDisplayModalViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayModalViewUITests.swift; sourceTree = ""; }; + AD7200D52125F92100AFD5F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AD7200DB2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayUITestsBase.swift; sourceTree = ""; }; + AD7200DD2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayBannerViewUITests.swift; sourceTree = ""; }; + AD7200DF2126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppMessagingDisplayImageOnlyViewUITests.swift; sourceTree = ""; }; + C265D6A970BC7B345291115C /* Pods_InAppMessagingDisplay_Sample.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_InAppMessagingDisplay_Sample.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F9F84709E9638EACD6BB35C4 /* Pods-InAppMessagingDisplay-Sample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-InAppMessagingDisplay-Sample.debug.xcconfig"; path = "Pods/Target Support Files/Pods-InAppMessagingDisplay-Sample/Pods-InAppMessagingDisplay-Sample.debug.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AD7200AF2124D19200AFD5F3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 5EACA9F21E16E885EEEA6AC5 /* Pods_FiamDisplaySwiftExample.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD7200CE2125F92100AFD5F3 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 9355239275ABBF1A3B074E54 /* Pods */ = { + isa = PBXGroup; + children = ( + F9F84709E9638EACD6BB35C4 /* Pods-InAppMessagingDisplay-Sample.debug.xcconfig */, + 0F66149C0079E8409F390CBE /* Pods-InAppMessagingDisplay-Sample.release.xcconfig */, + 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */, + 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9619953F9C0FE918881BCDED /* Frameworks */ = { + isa = PBXGroup; + children = ( + C265D6A970BC7B345291115C /* Pods_InAppMessagingDisplay_Sample.framework */, + 1E5AD224A78D3137046BB5AB /* Pods_FiamDisplaySwiftExample.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + AD7200B32124D19200AFD5F3 /* FiamDisplaySwiftExample */ = { + isa = PBXGroup; + children = ( + AD7200CB2124E2A800AFD5F3 /* CommonMessageTestVC.swift */, + AD7200C62124E24300AFD5F3 /* BannerMessageViewController.swift */, + AD7200C52124E24200AFD5F3 /* ImageOnlyMessageViewController.swift */, + AD7200C72124E24300AFD5F3 /* ModalMessageViewController.swift */, + AD7200B42124D19200AFD5F3 /* AppDelegate.swift */, + AD7200B82124D19200AFD5F3 /* Main.storyboard */, + AD7200BB2124D19400AFD5F3 /* Assets.xcassets */, + AD7200BD2124D19400AFD5F3 /* LaunchScreen.storyboard */, + AD7200C02124D19400AFD5F3 /* Info.plist */, + ); + path = FiamDisplaySwiftExample; + sourceTree = ""; + }; + AD7200D22125F92100AFD5F3 /* InAppMessagingDisplay-UITests */ = { + isa = PBXGroup; + children = ( + AD7200D32125F92100AFD5F3 /* InAppMessagingDisplayModalViewUITests.swift */, + AD7200DB2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift */, + AD7200DD2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift */, + AD7200DF2126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift */, + AD7200D52125F92100AFD5F3 /* Info.plist */, + ); + path = "InAppMessagingDisplay-UITests"; + sourceTree = ""; + }; + ADA7B5AF21223CED00B1C614 = { + isa = PBXGroup; + children = ( + AD7200B32124D19200AFD5F3 /* FiamDisplaySwiftExample */, + AD7200D22125F92100AFD5F3 /* InAppMessagingDisplay-UITests */, + ADA7B5B921223CED00B1C614 /* Products */, + 9355239275ABBF1A3B074E54 /* Pods */, + 9619953F9C0FE918881BCDED /* Frameworks */, + ); + sourceTree = ""; + }; + ADA7B5B921223CED00B1C614 /* Products */ = { + isa = PBXGroup; + children = ( + AD7200B22124D19200AFD5F3 /* FiamDisplaySwiftExample.app */, + AD7200D12125F92100AFD5F3 /* InAppMessagingDisplay-UITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AD7200B12124D19200AFD5F3 /* FiamDisplaySwiftExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD7200C32124D19400AFD5F3 /* Build configuration list for PBXNativeTarget "FiamDisplaySwiftExample" */; + buildPhases = ( + 08D42C2738A5B40BC9075B65 /* [CP] Check Pods Manifest.lock */, + AD7200AE2124D19200AFD5F3 /* Sources */, + AD7200AF2124D19200AFD5F3 /* Frameworks */, + AD7200B02124D19200AFD5F3 /* Resources */, + 13AA512D7003940DF3593687 /* [CP] Embed Pods Frameworks */, + 0B0EF24B15B4BCBCC06B5C35 /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FiamDisplaySwiftExample; + productName = FiamDisplaySwiftExample; + productReference = AD7200B22124D19200AFD5F3 /* FiamDisplaySwiftExample.app */; + productType = "com.apple.product-type.application"; + }; + AD7200D02125F92100AFD5F3 /* InAppMessagingDisplay-UITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = AD7200DA2125F92100AFD5F3 /* Build configuration list for PBXNativeTarget "InAppMessagingDisplay-UITests" */; + buildPhases = ( + AD7200CD2125F92100AFD5F3 /* Sources */, + AD7200CE2125F92100AFD5F3 /* Frameworks */, + AD7200CF2125F92100AFD5F3 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + AD7200D72125F92100AFD5F3 /* PBXTargetDependency */, + ); + name = "InAppMessagingDisplay-UITests"; + productName = "InAppMessagingDisplay-UITests"; + productReference = AD7200D12125F92100AFD5F3 /* InAppMessagingDisplay-UITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + ADA7B5B021223CED00B1C614 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0940; + LastUpgradeCheck = 0940; + ORGANIZATIONNAME = Google; + TargetAttributes = { + AD7200B12124D19200AFD5F3 = { + CreatedOnToolsVersion = 9.4.1; + }; + AD7200D02125F92100AFD5F3 = { + CreatedOnToolsVersion = 9.4.1; + TestTargetID = AD7200B12124D19200AFD5F3; + }; + }; + }; + buildConfigurationList = ADA7B5B321223CED00B1C614 /* Build configuration list for PBXProject "InAppMessagingDisplay-Sample" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = ADA7B5AF21223CED00B1C614; + productRefGroup = ADA7B5B921223CED00B1C614 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AD7200B12124D19200AFD5F3 /* FiamDisplaySwiftExample */, + AD7200D02125F92100AFD5F3 /* InAppMessagingDisplay-UITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AD7200B02124D19200AFD5F3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7200BF2124D19400AFD5F3 /* LaunchScreen.storyboard in Resources */, + AD7200BC2124D19400AFD5F3 /* Assets.xcassets in Resources */, + AD7200BA2124D19200AFD5F3 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD7200CF2125F92100AFD5F3 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 08D42C2738A5B40BC9075B65 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-FiamDisplaySwiftExample-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 0B0EF24B15B4BCBCC06B5C35 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/FirebaseInAppMessagingDisplay/InAppMessagingDisplayResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/InAppMessagingDisplayResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 13AA512D7003940DF3593687 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/GoogleUtilities/GoogleUtilities.framework", + "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/GoogleUtilities.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FiamDisplaySwiftExample/Pods-FiamDisplaySwiftExample-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AD7200AE2124D19200AFD5F3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7200C82124E24300AFD5F3 /* ImageOnlyMessageViewController.swift in Sources */, + AD7200CA2124E24300AFD5F3 /* ModalMessageViewController.swift in Sources */, + AD7200CC2124E2A800AFD5F3 /* CommonMessageTestVC.swift in Sources */, + AD7200C92124E24300AFD5F3 /* BannerMessageViewController.swift in Sources */, + AD7200B52124D19200AFD5F3 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AD7200CD2125F92100AFD5F3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AD7200D42125F92100AFD5F3 /* InAppMessagingDisplayModalViewUITests.swift in Sources */, + AD7200DE2126147100AFD5F3 /* InAppMessagingDisplayBannerViewUITests.swift in Sources */, + AD7200DC2126136D00AFD5F3 /* InAppMessagingDisplayUITestsBase.swift in Sources */, + AD7200E02126150600AFD5F3 /* InAppMessagingDisplayImageOnlyViewUITests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AD7200D72125F92100AFD5F3 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = AD7200B12124D19200AFD5F3 /* FiamDisplaySwiftExample */; + targetProxy = AD7200D62125F92100AFD5F3 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + AD7200B82124D19200AFD5F3 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7200B92124D19200AFD5F3 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + AD7200BD2124D19400AFD5F3 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + AD7200BE2124D19400AFD5F3 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + AD7200C12124D19400AFD5F3 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8FF302EB136148B923534F2E /* Pods-FiamDisplaySwiftExample.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + "HEADER_SEARCH_PATHS[arch=*]" = "\"${PODS_ROOT}/../../../Firebase/InAppMessagingDisplay/\"/** \"${PODS_ROOT}/../../../Firebase/InAppMessaging/\"/**"; + INFOPLIST_FILE = FiamDisplaySwiftExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev.FiamDisplaySwiftExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AD7200C22124D19400AFD5F3 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 46449F37C45EA76C97C32634 /* Pods-FiamDisplaySwiftExample.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = FiamDisplaySwiftExample/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental1.dev.FiamDisplaySwiftExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + AD7200D82125F92100AFD5F3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "InAppMessagingDisplay-UITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.google.experimental1.dev.InAppMessagingDisplay-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = FiamDisplaySwiftExample; + }; + name = Debug; + }; + AD7200D92125F92100AFD5F3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "InAppMessagingDisplay-UITests/Info.plist"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.google.experimental1.dev.InAppMessagingDisplay-UITests"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 4.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = FiamDisplaySwiftExample; + }; + name = Release; + }; + ADA7B5CC21223CEE00B1C614 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + ADA7B5CD21223CEE00B1C614 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 11.4; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AD7200C32124D19400AFD5F3 /* Build configuration list for PBXNativeTarget "FiamDisplaySwiftExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7200C12124D19400AFD5F3 /* Debug */, + AD7200C22124D19400AFD5F3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AD7200DA2125F92100AFD5F3 /* Build configuration list for PBXNativeTarget "InAppMessagingDisplay-UITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AD7200D82125F92100AFD5F3 /* Debug */, + AD7200D92125F92100AFD5F3 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + ADA7B5B321223CED00B1C614 /* Build configuration list for PBXProject "InAppMessagingDisplay-Sample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + ADA7B5CC21223CEE00B1C614 /* Debug */, + ADA7B5CD21223CEE00B1C614 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = ADA7B5B021223CED00B1C614 /* Project object */; +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayBannerViewUITests.swift b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayBannerViewUITests.swift new file mode 100644 index 00000000000..d21d5505456 --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayBannerViewUITests.swift @@ -0,0 +1,194 @@ +/* + * Copyright 2018 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 Foundation + +import XCTest + +class InAppMessagingDisplayBannerViewUITests: InAppMessagingDisplayUITestsBase { + var app: XCUIApplication! + var verificationLabel: XCUIElement! + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + + app = XCUIApplication() + verificationLabel = app.staticTexts["verification-label-banner"] + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testNormalBannerView() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Show Regular Banner View"].tap() + + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + // This also verifies that up-swiping gesture would dismiss + // the banner view + bannerUIView.swipeUp() + + waitForElementToDisappear(bannerUIView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("dismissed")) + } + } + + func testBannerViewWithoutImage() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageViewElement = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + XCTAssert(!isElementExistentAndHavingSize(imageViewElement)) + + bannerUIView.tap() + waitForElementToDisappear(bannerUIView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("clicked")) + } + } + + func testBannerViewWithLongTitle() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Long Title"].tap() + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithWideImage() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Wide Image"].tap() + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithThinImage() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Thin Image"].tap() + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } + + func testBannerViewWithLargeBody() { + app.tabBars.buttons["Banner Messages"].tap() + + let titleElement = app.staticTexts["banner-message-title-view"] + let imageView = app.images["banner-image-view"] + let bodyElement = app.staticTexts["banner-body-label"] + let bannerUIView = app.otherElements["banner-mode-uiview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["With Large Body Text"].tap() + waitForElementToAppear(bannerUIView) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(titleElement)) + XCTAssert(isElementExistentAndHavingSize(bodyElement)) + + bannerUIView.swipeUp() + waitForElementToDisappear(bannerUIView) + } + } +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayImageOnlyViewUITests.swift b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayImageOnlyViewUITests.swift new file mode 100644 index 00000000000..ca3caf02547 --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayImageOnlyViewUITests.swift @@ -0,0 +1,157 @@ +/* + * Copyright 2018 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 Foundation +import XCTest + +class InAppMessagingImageOnlyViewUITests: InAppMessagingDisplayUITestsBase { + var app: XCUIApplication! + var verificationLabel: XCUIElement! + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + + app = XCUIApplication() + verificationLabel = app.staticTexts["verification-label-image-only"] + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testImageOnlyView() { + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Show Regular Image Only View"].tap() + + waitForElementToAppear(closeButton) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + imageView.tap() + waitForElementToDisappear(imageView) + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("clicked")) + } + } + + func testImageOnlyViewWithLargeImageDimension() { + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["High Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("dismissed")) + } + } + + func testImageOnlyViewWithLowImageDimension() { + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Low Dimension Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithWideImage() { + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Wide Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } + + func testImageOnlyViewWithNarrowImage() { + app.tabBars.buttons["Image Only Messages"].tap() + + let imageView = app.images["image-view-in-image-only-view"] + let closeButton = app.buttons["close-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Narrow Image"].tap() + + // wait time longer due to large image + waitForElementToAppear(closeButton, 10) + + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isUIElementWithinUIWindow(imageView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(imageView) + } + } +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayModalViewUITests.swift b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayModalViewUITests.swift new file mode 100644 index 00000000000..ed53f7aabaa --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayModalViewUITests.swift @@ -0,0 +1,399 @@ +/* + * Copyright 2018 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 XCTest + +class InAppMessagingDisplayModalViewUITests: InAppMessagingDisplayUITestsBase { + var app: XCUIApplication! + var verificationLabel: XCUIElement! + + override func setUp() { + super.setUp() + + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + // UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method. + XCUIApplication().launch() + + app = XCUIApplication() + verificationLabel = app.staticTexts["verification-label-modal"] + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDown() { + // Put teardown code here. This method is called after the invocation of each test method in the class. + super.tearDown() + } + + func testNormalModalView() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Regular"].tap() + waitForElementToAppear(closeButton) + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + if orientation == UIDeviceOrientation.portrait { + actionButton.tap() + } else { + closeButton.tap() + } + waitForElementToDisappear(messageCardView) + + let labelValue = verificationLabel.label + + if orientation == UIDeviceOrientation.portrait { + XCTAssertTrue(labelValue.contains("clicked")) + } else { + XCTAssertTrue(labelValue.contains("dismissed")) + } + } + } + + func testModalViewWithWideImage() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Wide Image"].tap() + + waitForElementToAppear(closeButton) + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("dismissed")) + } + } + + func testModalViewWithNarrowImage() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Thin Image"].tap() + + waitForElementToAppear(closeButton) + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + actionButton.tap() + waitForElementToDisappear(messageCardView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("clicked")) + } + } + + func testModalViewWithoutImage() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + let imageView = app.images["modal-image-view"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Image"].tap() + waitForElementToAppear(closeButton) + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssertFalse(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("dismissed")) + } + } + + func testModalViewWithoutImageOrActionButton() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Wthout Image or Action Button"].tap() + + waitForElementToAppear(closeButton) + + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + XCUIDevice.shared.orientation = .portrait + } + } + + func testModalViewWithoutActionButton() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + app.buttons["Without Action Button"].tap() + waitForElementToAppear(closeButton) + + XCTAssertFalse(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitle() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageBody() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + let actionButton = app.buttons["message-action-button"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["Large Title Text"].tap() + waitForElementToAppear(closeButton) + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + + actionButton.tap() + + waitForElementToDisappear(messageCardView) + let labelValue = verificationLabel.label + XCTAssertTrue(labelValue.contains("clicked")) + } + } + + func testModalViewWithLongMessageTitleAndMessageBody() { + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body"].tap() + waitForElementToAppear(closeButton) + + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: imageView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleAndMessageBodyWithoutImage() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title and Body Without Image"].tap() + waitForElementToAppear(closeButton) + + let actionButton = app.buttons["message-action-button"] + + XCTAssert(isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: actionButton)) + XCTAssert(childFrameWithinParentBound(parent: messageCardView, child: bodyTextview)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } + + func testModalViewWithLongMessageTitleWithoutBodyWithoutImageWithoutButton() { + let app = XCUIApplication() + app.tabBars.buttons["Modal Messages"].tap() + + let messageCardView = app.otherElements["message-card-view"] + let closeButton = app.buttons["close-button"] + let imageView = app.images["modal-image-view"] + let bodyTextview = app.textViews["message-body-textview"] + + let orientantions = [UIDeviceOrientation.portrait, UIDeviceOrientation.landscapeLeft] + + for orientation in orientantions { + XCUIDevice.shared.orientation = orientation + + app.buttons["With Large Title, No Image, No Body and No Button"].tap() + waitForElementToAppear(closeButton) + + let actionButton = app.buttons["message-action-button"] + + XCTAssert(!isElementExistentAndHavingSize(actionButton)) + XCTAssert(isElementExistentAndHavingSize(closeButton)) + XCTAssert(!isElementExistentAndHavingSize(bodyTextview)) + XCTAssert(isElementExistentAndHavingSize(messageCardView)) + XCTAssert(!isElementExistentAndHavingSize(imageView)) + + XCTAssert(isUIElementWithinUIWindow(messageCardView)) + + app.buttons["close-button"].tap() + + waitForElementToDisappear(messageCardView) + } + } +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayUITestsBase.swift b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayUITestsBase.swift new file mode 100644 index 00000000000..5d5715e0f44 --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/InAppMessagingDisplayUITestsBase.swift @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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 Foundation + +import XCTest + +class InAppMessagingDisplayUITestsBase: XCTestCase { + func waitForElementToAppear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == true") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func waitForElementToDisappear(_ element: XCUIElement, _ timeoutInSeconds: TimeInterval = 5) { + let existsPredicate = NSPredicate(format: "exists == false") + expectation(for: existsPredicate, evaluatedWith: element, handler: nil) + waitForExpectations(timeout: timeoutInSeconds, handler: nil) + } + + func childFrameWithinParentBound(parent: XCUIElement, child: XCUIElement) -> Bool { + return parent.frame.contains(child.frame) + } + + func isUIElementWithinUIWindow(_ uiElement: XCUIElement) -> Bool { + let app = XCUIApplication() + let window = app.windows.element(boundBy: 0) + return window.frame.contains(uiElement.frame) + } + + func isElementExistentAndHavingSize(_ uiElement: XCUIElement) -> Bool { + // on iOS 9.3 for a XCUIElement whose height or width <=0, uiElement.exists still returns true + // on iOS 10.3, for such an element uiElement.exists returns false + // this function is to handle the existence (in our semanatic visible) testing for both cases + return uiElement.exists && uiElement.frame.size.height > 0.1 && uiElement.frame.size.width > 0.1 + } +} diff --git a/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/Info.plist b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/Info.plist new file mode 100644 index 00000000000..6c40a6cd0c4 --- /dev/null +++ b/InAppMessagingDisplay/Example/InAppMessagingDisplay-UITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/InAppMessagingDisplay/Example/Podfile b/InAppMessagingDisplay/Example/Podfile new file mode 100644 index 00000000000..d1b566b6a77 --- /dev/null +++ b/InAppMessagingDisplay/Example/Podfile @@ -0,0 +1,8 @@ + +use_frameworks! + +target 'FiamDisplaySwiftExample' do + platform :ios, '8.0' + pod 'FirebaseInAppMessagingDisplay', :path => '../..' +end + diff --git a/InAppMessagingDisplay/README.md b/InAppMessagingDisplay/README.md new file mode 100644 index 00000000000..166f87cfad7 --- /dev/null +++ b/InAppMessagingDisplay/README.md @@ -0,0 +1,13 @@ +This is a sample project for testing Firebase InAppMessaging Display SDK. + +## Usage + +``` +$ cd InAppMessaging/Example +$ pod update +$ open InAppMessagingDisplay-Sample.xcworkspace +Select the FiamDisplaySwiftExample scheme +⌘-u to build and run the UI tests or ⌘-R to run the sample app. +``` + + diff --git a/README.md b/README.md index 4414b3e3ee9..51a0e0acd42 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ This repository contains a subset of the Firebase iOS SDK source. It currently includes FirebaseCore, FirebaseAuth, FirebaseDatabase, FirebaseFirestore, -FirebaseFunctions, FirebaseMessaging and FirebaseStorage. +FirebaseFunctions, FirebaseInAppMessagingDisplay, FirebaseMessaging and +FirebaseStorage. The repository also includes GoogleUtilities source. The [GoogleUtilities](GoogleUtilities/README.md) pod is