Переглянути джерело

Open source FIAM headless SDK (#2312)

christibbs 7 роки тому
батько
коміт
3905bd250a
100 змінених файлів з 9201 додано та 0 видалено
  1. 61 0
      Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h
  2. 45 0
      Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h
  3. 170 0
      Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m
  4. 51 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h
  5. 202 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m
  6. 48 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h
  7. 171 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m
  8. 46 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h
  9. 203 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m
  10. 75 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h
  11. 233 0
      Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m
  12. 6 0
      Firebase/InAppMessaging/CHANGELOG.md
  13. 40 0
      Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h
  14. 313 0
      Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m
  15. 42 0
      Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h
  16. 47 0
      Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h
  17. 138 0
      Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m
  18. 60 0
      Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h
  19. 85 0
      Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m
  20. 36 0
      Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h
  21. 56 0
      Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h
  22. 31 0
      Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m
  23. 34 0
      Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h
  24. 33 0
      Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m
  25. 36 0
      Firebase/InAppMessaging/FIRCore+InAppMessaging.h
  26. 22 0
      Firebase/InAppMessaging/FIRCore+InAppMessaging.m
  27. 144 0
      Firebase/InAppMessaging/FIRInAppMessaging.m
  28. 26 0
      Firebase/InAppMessaging/FIRInAppMessagingPrivate.h
  29. 89 0
      Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h
  30. 215 0
      Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m
  31. 75 0
      Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h
  32. 260 0
      Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m
  33. 39 0
      Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h
  34. 120 0
      Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m
  35. 22 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h
  36. 66 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m
  37. 23 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h
  38. 55 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m
  39. 22 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h
  40. 62 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m
  41. 37 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h
  42. 32 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m
  43. 61 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h
  44. 497 0
      Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m
  45. 59 0
      Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h
  46. 253 0
      Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m
  47. 22 0
      Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h
  48. 51 0
      Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m
  49. 91 0
      Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h
  50. 223 0
      Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m
  51. 55 0
      Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h
  52. 272 0
      Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m
  53. 30 0
      Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h
  54. 64 0
      Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m
  55. 80 0
      Firebase/InAppMessaging/Public/FIRInAppMessaging.h
  56. 251 0
      Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h
  57. 108 0
      Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m
  58. 46 0
      Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h
  59. 244 0
      Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m
  60. 56 0
      Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h
  61. 431 0
      Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m
  62. 73 0
      Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h
  63. 113 0
      Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m
  64. 25 0
      Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h
  65. 53 0
      Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h
  66. 35 0
      Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m
  67. 33 0
      Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h
  68. 137 0
      Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m
  69. 29 0
      Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h
  70. 56 0
      Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m
  71. 28 0
      Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h
  72. 23 0
      Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m
  73. 30 0
      Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h
  74. 42 0
      Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m
  75. 31 0
      Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h
  76. 39 0
      Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m
  77. BIN
      Firebase/InAppMessaging/firebase_28dp.png
  78. 39 0
      FirebaseInAppMessaging.podspec
  79. 36 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h
  80. 118 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m
  81. 93 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json
  82. 20 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h
  83. 170 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m
  84. 23 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h
  85. 137 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m
  86. 27 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard
  87. 395 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard
  88. 28 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist
  89. 67 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist
  90. 21 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h
  91. 73 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m
  92. 34 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile
  93. 24 0
      InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m
  94. 28 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist
  95. 18 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile
  96. 423 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj
  97. 21 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h
  98. 70 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m
  99. 93 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json
  100. 31 0
      InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard

+ 61 - 0
Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLogger.h

@@ -0,0 +1,61 @@
+/*
+ * 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/Foundation.h>
+#import "FIRIAMClientInfoFetcher.h"
+#import "FIRIAMTimeFetcher.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/// Values for different fiam activity types.
+typedef NS_ENUM(NSInteger, FIRIAMAnalyticsLogEventType) {
+
+  FIRIAMAnalyticsLogEventUnknown = -1,
+
+  FIRIAMAnalyticsEventMessageImpression = 0,
+  FIRIAMAnalyticsEventActionURLFollow = 1,
+  FIRIAMAnalyticsEventMessageDismissAuto = 2,
+  FIRIAMAnalyticsEventMessageDismissClick = 3,
+  FIRIAMAnalyticsEventMessageDismissSwipe = 4,
+
+  // category: errors happened
+  FIRIAMAnalyticsEventImageFetchError = 11,
+  FIRIAMAnalyticsEventImageFormatUnsupported = 12,
+
+  FIRIAMAnalyticsEventFetchAPINetworkError = 13,
+  FIRIAMAnalyticsEventFetchAPIClientError = 14,  // server returns 4xx status code
+  FIRIAMAnalyticsEventFetchAPIServerError = 15,  // server returns 5xx status code
+
+  // Events for test messages
+  FIRIAMAnalyticsEventTestMessageImpression = 16,
+  FIRIAMAnalyticsEventTestMessageClick = 17,
+};
+
+// a protocol for collecting Analytics log records. It's implementation will decide
+// what to do with that analytics log record
+@protocol FIRIAMAnalyticsEventLogger
+/**
+ * Adds an analytics log record.
+ * @param eventTimeInMs the timestamp in ms for when the event happened.
+ *      if it's nil, the implementation will use the current system for this info.
+ */
+- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType
+                   forCampaignID:(NSString *)campaignID
+                withCampaignName:(NSString *)campaignName
+                   eventTimeInMs:(nullable NSNumber *)eventTimeInMs
+                      completion:(void (^)(BOOL success))completion;
+@end
+NS_ASSUME_NONNULL_END

+ 45 - 0
Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.h

@@ -0,0 +1,45 @@
+/*
+ * 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/Foundation.h>
+
+#import "FIRIAMAnalyticsEventLogger.h"
+
+@class FIRIAMClearcutLogger;
+@protocol FIRIAMTimeFetcher;
+@protocol FIRAnalyticsInterop;
+
+NS_ASSUME_NONNULL_BEGIN
+/**
+ * Implementation of protocol FIRIAMAnalyticsEventLogger by doing two things
+ *  1 Firing Firebase Analytics Events for impressions and clicks and dismisses
+ *  2 Making clearcut logging for all other types of analytics events
+ */
+@interface FIRIAMAnalyticsEventLoggerImpl : NSObject <FIRIAMAnalyticsEventLogger>
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ *
+ *  @param userDefaults needed for tracking upload timing info persistently.If nil, using
+ *    NSUserDefaults standardUserDefaults. It's defined as a parameter to help with
+ *    unit testing mocking
+ */
+- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger
+                      usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     usingUserDefaults:(nullable NSUserDefaults *)userDefaults
+                             analytics:(nullable id<FIRAnalyticsInterop>)analytics;
+@end
+NS_ASSUME_NONNULL_END

+ 170 - 0
Firebase/InAppMessaging/Analytics/FIRIAMAnalyticsEventLoggerImpl.m

@@ -0,0 +1,170 @@
+/*
+ * 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 "FIRIAMAnalyticsEventLoggerImpl.h"
+
+#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
+#import <FirebaseCore/FIRLogger.h>
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutLogger.h"
+
+typedef void (^FIRAUserPropertiesCallback)(NSDictionary *userProperties);
+
+@interface FIRIAMAnalyticsEventLoggerImpl ()
+@property(readonly, nonatomic) FIRIAMClearcutLogger *clearCutLogger;
+@property(readonly, nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+@property(nonatomic, readonly) NSUserDefaults *userDefaults;
+@end
+
+// in these kFAXX constants, FA represents FirebaseAnalytics
+static NSString *const kFIREventOriginFIAM = @"fiam";
+;
+static NSString *const kFAEventNameForImpression = @"firebase_in_app_message_impression";
+static NSString *const kFAEventNameForAction = @"firebase_in_app_message_action";
+static NSString *const kFAEventNameForDismiss = @"firebase_in_app_message_dismiss";
+
+// In order to support tracking conversions from clicking a fiam event, we need to set
+// an analytics user property with the fiam message's campaign id.
+// This is the user property as kFIRUserPropertyLastNotification defined for FCM.
+// Unlike FCM, FIAM would only allow the user property to exist up to certain expiration time
+// after which, we stop attributing any further conversions to that fiam message click.
+// So we include kFAUserPropertyPrefixForFIAM as the prefix for the entry written by fiam SDK
+// to avoid removing entries written by FCM SDK
+static NSString *const kFAUserPropertyForLastNotification = @"_ln";
+static NSString *const kFAUserPropertyPrefixForFIAM = @"fiam:";
+
+// This user defaults key is for the entry to tell when we should remove the private user
+// property from a prior action url click to stop conversion attribution for a campaign
+static NSString *const kFIAMUserDefaualtsKeyForRemoveUserPropertyTimeInSeconds =
+    @"firebase-iam-conversion-tracking-expires-in-seconds";
+
+@implementation FIRIAMAnalyticsEventLoggerImpl {
+  id<FIRAnalyticsInterop> _analytics;
+}
+
+- (instancetype)initWithClearcutLogger:(FIRIAMClearcutLogger *)ctLogger
+                      usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     usingUserDefaults:(nullable NSUserDefaults *)userDefaults
+                             analytics:(nullable id<FIRAnalyticsInterop>)analytics {
+  if (self = [super init]) {
+    _clearCutLogger = ctLogger;
+    _timeFetcher = timeFetcher;
+    _analytics = analytics;
+    _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults];
+
+    if (!_analytics) {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM280002",
+                    @"Firebase In App Messaging was not configured with FirebaseAnalytics.");
+    }
+  }
+  return self;
+}
+
+- (NSDictionary *)constructFAEventParamsWithCampaignID:(NSString *)campaignID
+                                          campaignName:(NSString *)campaignName {
+  // event parameter names are aligned with definitions in event_names_util.cc
+  return @{
+    @"_nmn" : campaignName ?: @"unknown",
+    @"_nmid" : campaignID ?: @"unknown",
+    @"_ndt" : @([self.timeFetcher currentTimestampInSeconds])
+  };
+}
+
+- (void)logFAEventsForMessageImpressionWithcampaignID:(NSString *)campaignID
+                                         campaignName:(NSString *)campaignName {
+  if (_analytics) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280001",
+                @"Log campaign impression Firebase Analytics event for campaign ID %@", campaignID);
+
+    NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID
+                                                         campaignName:campaignName];
+    [_analytics logEventWithOrigin:kFIREventOriginFIAM
+                              name:kFAEventNameForImpression
+                        parameters:params];
+  }
+}
+
+- (BOOL)setAnalyticsUserPropertyForKey:(NSString *)key withValue:(NSString *)value {
+  if (!_analytics || !key || !value) {
+    return NO;
+  }
+  [_analytics setUserPropertyWithOrigin:kFIREventOriginFIAM name:key value:value];
+  return YES;
+}
+
+- (void)logFAEventsForMessageActionWithCampaignID:(NSString *)campaignID
+                                     campaignName:(NSString *)campaignName {
+  if (_analytics) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280004",
+                @"Log action click Firebase Analytics event for campaign ID %@", campaignID);
+
+    NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID
+                                                         campaignName:campaignName];
+
+    [_analytics logEventWithOrigin:kFIREventOriginFIAM
+                              name:kFAEventNameForAction
+                        parameters:params];
+  }
+
+  // set a special user property so that conversion events can be queried based on that
+  // for reporting purpose
+  NSString *conversionTrackingUserPropertyValue =
+      [NSString stringWithFormat:@"%@%@", kFAUserPropertyPrefixForFIAM, campaignID];
+
+  if ([self setAnalyticsUserPropertyForKey:kFAUserPropertyForLastNotification
+                                 withValue:conversionTrackingUserPropertyValue]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280009",
+                @"User property for conversion tracking was set for campaign %@", campaignID);
+  }
+}
+
+- (void)logFAEventsForMessageDismissWithcampaignID:(NSString *)campaignID
+                                      campaignName:(NSString *)campaignName {
+  if (_analytics) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM280007",
+                @"Log message dismiss Firebase Analytics event for campaign ID %@", campaignID);
+
+    NSDictionary *params = [self constructFAEventParamsWithCampaignID:campaignID
+                                                         campaignName:campaignName];
+    [_analytics logEventWithOrigin:kFIREventOriginFIAM
+                              name:kFAEventNameForDismiss
+                        parameters:params];
+  }
+}
+
+- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType
+                   forCampaignID:(NSString *)campaignID
+                withCampaignName:(NSString *)campaignName
+                   eventTimeInMs:(nullable NSNumber *)eventTimeInMs
+                      completion:(void (^)(BOOL success))completion {
+  // log Firebase Analytics event first
+  if (eventType == FIRIAMAnalyticsEventMessageImpression) {
+    [self logFAEventsForMessageImpressionWithcampaignID:campaignID campaignName:campaignName];
+  } else if (eventType == FIRIAMAnalyticsEventActionURLFollow) {
+    [self logFAEventsForMessageActionWithCampaignID:campaignID campaignName:campaignName];
+  } else if (eventType == FIRIAMAnalyticsEventMessageDismissAuto ||
+             eventType == FIRIAMAnalyticsEventMessageDismissClick) {
+    [self logFAEventsForMessageDismissWithcampaignID:campaignID campaignName:campaignName];
+  }
+
+  // and do clearcut logging as well
+  [self.clearCutLogger logAnalyticsEventForType:eventType
+                                  forCampaignID:campaignID
+                               withCampaignName:campaignName
+                                  eventTimeInMs:eventTimeInMs
+                                     completion:completion];
+}
+@end

+ 51 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.h

@@ -0,0 +1,51 @@
+/*
+ * 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/Foundation.h>
+
+@class FIRIAMClearcutLogRecord;
+@protocol FIRIAMTimeFetcher;
+
+NS_ASSUME_NONNULL_BEGIN
+// class for sending requests to clearcut over its http API
+@interface FIRIAMClearcutHttpRequestSender : NSObject
+
+/**
+ * Create an FIRIAMClearcutHttpRequestSender instance with specified clearcut server.
+ *
+ * @param serverHost API server host.
+ * @param osMajorVersion detected iOS major version of the current device
+ */
+- (instancetype)initWithClearcutHost:(NSString *)serverHost
+                    usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                  withOSMajorVersion:(NSString *)osMajorVersion;
+
+/**
+ * Sends a batch of FIRIAMClearcutLogRecord records to clearcut server.
+ * @param logs an array of log records to be sent.
+ * @param completion is the handler to triggered upon completion. 'success' is a bool
+ *       to indicate if the sending is successful. 'shouldRetryLogs' indicates if these
+ *       logs need to be retried later on. On success case, waitTimeInMills is the value
+ *       returned from clearcut server to indicate the minimal wait time before another
+ *       send request can be attempted.
+ */
+
+- (void)sendClearcutHttpRequestForLogs:(NSArray<FIRIAMClearcutLogRecord *> *)logs
+                        withCompletion:(void (^)(BOOL success,
+                                                 BOOL shouldRetryLogs,
+                                                 int64_t waitTimeInMills))completion;
+@end
+NS_ASSUME_NONNULL_END

+ 202 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutHttpRequestSender.m

@@ -0,0 +1,202 @@
+/*
+ * 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 <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutHttpRequestSender.h"
+#import "FIRIAMClearcutLogStorage.h"
+#import "FIRIAMClientInfoFetcher.h"
+#import "FIRIAMTimeFetcher.h"
+
+@interface FIRIAMClearcutHttpRequestSender ()
+@property(readonly, copy, nonatomic) NSString *serverHostName;
+
+@property(readwrite, nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+@property(readonly, copy, nonatomic) NSString *osMajorVersion;
+@end
+
+@implementation FIRIAMClearcutHttpRequestSender
+
+- (instancetype)initWithClearcutHost:(NSString *)serverHost
+                    usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                  withOSMajorVersion:(NSString *)osMajorVersion {
+  if (self = [super init]) {
+    _serverHostName = [serverHost copy];
+    _timeFetcher = timeFetcher;
+    _osMajorVersion = [osMajorVersion copy];
+  }
+  return self;
+}
+
+- (void)updateRequestBodyWithClearcutEnvelopeFields:(NSMutableDictionary *)bodyDict {
+  bodyDict[@"client_info"] = @{
+    @"client_type" : @15,  // 15 is the enum value for IOS_FIREBASE client
+    @"ios_client_info" : @{@"os_major_version" : self.osMajorVersion ?: @""}
+  };
+  bodyDict[@"log_source"] = @"FIREBASE_INAPPMESSAGING";
+
+  NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000;
+  bodyDict[@"request_time_ms"] = @((long)nowInMs);
+}
+
+- (NSArray<NSDictionary *> *)constructLogEventsArrayLogRecords:
+    (NSArray<FIRIAMClearcutLogRecord *> *)logRecords {
+  NSMutableArray<NSDictionary *> *logEvents = [[NSMutableArray alloc] init];
+  for (id next in logRecords) {
+    FIRIAMClearcutLogRecord *logRecord = (FIRIAMClearcutLogRecord *)next;
+    [logEvents addObject:@{
+      @"event_time_ms" : @((long)logRecord.eventTimestampInSeconds * 1000),
+      @"source_extension_json" : logRecord.eventExtensionJsonString ?: @""
+    }];
+  }
+
+  return [logEvents copy];
+}
+
+// @return nil if error happened in constructing the body
+- (NSDictionary *)constructRequestBodyWithRetryRecords:
+    (NSArray<FIRIAMClearcutLogRecord *> *)logRecords {
+  NSMutableDictionary *body = [[NSMutableDictionary alloc] init];
+  [self updateRequestBodyWithClearcutEnvelopeFields:body];
+  body[@"log_event"] = [self constructLogEventsArrayLogRecords:logRecords];
+  return [body copy];
+}
+
+// a helper method for dealing with the response received from
+// executing NSURLSessionDataTask. Triggers the completion callback accordingly
+- (void)handleClearcutAPICallResponseWithData:(NSData *)data
+                                     response:(NSURLResponse *)response
+                                        error:(NSError *)error
+                                   completion:
+                                       (nonnull void (^)(BOOL success,
+                                                         BOOL shouldRetryLogs,
+                                                         int64_t waitTimeInMills))completion {
+  if (error) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250003",
+                  @"Internal error: encountered error in uploading clearcut message"
+                   ":%@",
+                  error);
+    completion(NO, YES, 0);
+    return;
+  }
+
+  if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250008",
+                  @"Received non http response from sending "
+                   "clearcut requests %@",
+                  response);
+    completion(NO, YES, 0);
+    return;
+  }
+
+  NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+  if (httpResponse.statusCode == 200) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250004",
+                @"Sending clearcut logging request was successful");
+
+    NSError *errorJson = nil;
+    NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data
+                                                                 options:kNilOptions
+                                                                   error:&errorJson];
+
+    int64_t waitTimeFromClearcutServer = 0;
+    if (!errorJson && responseDict[@"next_request_wait_millis"]) {
+      waitTimeFromClearcutServer = [responseDict[@"next_request_wait_millis"] longLongValue];
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250007",
+                  @"Wait time from clearcut server response is %d seconds",
+                  (int)waitTimeFromClearcutServer / 1000);
+    }
+    completion(YES, NO, waitTimeFromClearcutServer);
+  } else if (httpResponse.statusCode == 400) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250012",
+                  @"Seeing 400 status code in response and we are discarding this log"
+                  @"record");
+    // 400 means bad request data and it won't be successful with retries. So
+    // we give up on these log records
+    completion(NO, NO, 0);
+  } else {
+    // May need to handle 401 errors if we do authentication in the future
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005",
+                  @"Other http status code seen in clearcut request response %d",
+                  (int)httpResponse.statusCode);
+    // can be retried
+    completion(NO, YES, 0);
+  }
+}
+
+- (void)sendClearcutHttpRequestForLogs:(NSArray<FIRIAMClearcutLogRecord *> *)logs
+                        withCompletion:(nonnull void (^)(BOOL success,
+                                                         BOOL shouldRetryLogs,
+                                                         int64_t waitTimeInMills))completion {
+  NSDictionary *requestBody = [self constructRequestBodyWithRetryRecords:logs];
+
+  if (!requestBody) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250014",
+                  @"Not able to construct request body for clearcut request, giving up");
+    completion(NO, NO, 0);
+  } else {
+    // sending the log via a http request
+    NSURLSession *URLSession = [NSURLSession sharedSession];
+    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
+    [request setHTTPMethod:@"POST"];
+    [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
+    [request addValue:@"application/json" forHTTPHeaderField:@"Accept"];
+
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250001",
+                @"Request body dictionary is %@ for clearcut logging request", requestBody);
+
+    NSError *error;
+    NSData *requestBodyData = [NSJSONSerialization dataWithJSONObject:requestBody
+                                                              options:0
+                                                                error:&error];
+
+    if (error) {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250011",
+                    @"Error in creating request body json for clearcut requests:%@", error);
+      completion(NO, NO, 0);
+      return;
+    }
+
+    NSString *requestURLString =
+        [NSString stringWithFormat:@"https://%@/log?format=json_proto", self.serverHostName];
+    [request setURL:[NSURL URLWithString:requestURLString]];
+    [request setHTTPBody:requestBodyData];
+
+    NSURLSessionDataTask *clearCutLogDataTask =
+        [URLSession dataTaskWithRequest:request
+                      completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+                        [self handleClearcutAPICallResponseWithData:data
+                                                           response:response
+                                                              error:error
+                                                         completion:completion];
+                      }];
+
+    if (clearCutLogDataTask == nil) {
+      NSString *errorDesc = @"Internal error: NSURLSessionDataTask failed to be created due to "
+                             "possibly incorrect parameters";
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM250005", @"%@", errorDesc);
+      completion(NO, NO, 0);
+    } else {
+      [clearCutLogDataTask resume];
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM250002",
+                  @"Making a restful api for sending clearcut logging data with "
+                   "a NSURLSessionDataTask request as %@",
+                  clearCutLogDataTask.currentRequest);
+    }
+  }
+}
+@end

+ 48 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.h

@@ -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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMClearcutLogRecord : NSObject <NSSecureCoding>
+@property(nonatomic, copy, readonly) NSString *eventExtensionJsonString;
+@property(nonatomic, readonly) NSInteger eventTimestampInSeconds;
+- (instancetype)initWithExtensionJsonString:(NSString *)jsonString
+                    eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds;
+@end
+
+@protocol FIRIAMTimeFetcher;
+
+// A local persistent storage for saving FIRIAMClearcutLogRecord objects
+// so that they can be delivered to clearcut server.
+// Based on the clearcut log structure, our strategy is to store the json string
+// for the source extension since it does not need to be modified upon delivery retries.
+// The envelope of the clearcut log will be reconstructed when delivery is
+// attempted.
+
+@interface FIRIAMClearcutLogStorage : NSObject
+- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds
+                             withTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher;
+
+// add new records into the storage
+- (void)pushRecords:(NSArray<FIRIAMClearcutLogRecord *> *)newRecords;
+
+// pop all the records that have not expired yet. With this call, these
+// records are removed from the book of this local storage object.
+// @param upTo the cap on how many records to be popped.
+- (NSArray<FIRIAMClearcutLogRecord *> *)popStillValidRecordsForUpTo:(NSInteger)upTo;
+@end
+NS_ASSUME_NONNULL_END

+ 171 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogStorage.m

@@ -0,0 +1,171 @@
+/*
+ * 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/UIKit.h>
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutLogStorage.h"
+#import "FIRIAMTimeFetcher.h"
+
+@implementation FIRIAMClearcutLogRecord
+static NSString *const kEventTimestampKey = @"event_ts_seconds";
+static NSString *const kEventExtensionJson = @"extension_js";
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (instancetype)initWithExtensionJsonString:(NSString *)jsonString
+                    eventTimestampInSeconds:(NSInteger)eventTimestampInSeconds {
+  self = [super init];
+  if (self != nil) {
+    _eventTimestampInSeconds = eventTimestampInSeconds;
+    _eventExtensionJsonString = jsonString;
+  }
+  return self;
+}
+
+- (id)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self != nil) {
+    _eventTimestampInSeconds = [decoder decodeIntegerForKey:kEventTimestampKey];
+    _eventExtensionJsonString = [decoder decodeObjectOfClass:[NSString class]
+                                                      forKey:kEventExtensionJson];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeInteger:self.eventTimestampInSeconds forKey:kEventTimestampKey];
+  [encoder encodeObject:self.eventExtensionJsonString forKey:kEventExtensionJson];
+}
+@end
+
+@interface FIRIAMClearcutLogStorage ()
+@property(nonatomic) NSInteger recordExpiresInSeconds;
+@property(nonatomic) NSMutableArray<FIRIAMClearcutLogRecord *> *records;
+@property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+@end
+
+// We keep all the records in memory and flush them into files upon receiving
+// applicationDidEnterBackground notifications.
+@implementation FIRIAMClearcutLogStorage
+
++ (NSString *)determineCacheFilePath {
+  static NSString *logCachePath;
+  static dispatch_once_t onceToken;
+
+  dispatch_once(&onceToken, ^{
+    NSString *libraryDirPath =
+        NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
+    logCachePath =
+        [NSString stringWithFormat:@"%@/firebase-iam-clearcut-retry-records", libraryDirPath];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230001",
+                @"Persistent file path for clearcut log records is %@", logCachePath);
+  });
+  return logCachePath;
+}
+
+- (instancetype)initWithExpireAfterInSeconds:(NSInteger)expireInSeconds
+                             withTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
+  if (self = [super init]) {
+    _records = [[NSMutableArray alloc] init];
+    _timeFetcher = timeFetcher;
+    _recordExpiresInSeconds = expireInSeconds;
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(appWillBecomeInactive)
+                                                 name:UIApplicationWillResignActiveNotification
+                                               object:nil];
+    @try {
+      [self loadFromCachePath:nil];
+    } @catch (NSException *exception) {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM230004",
+                    @"Non-fatal exception in loading persisted clearcut log records: %@.",
+                    exception);
+    }
+  }
+  return self;
+}
+
+- (void)appWillBecomeInactive {
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    [self saveIntoCacheWithPath:nil];
+  });
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)pushRecords:(NSArray<FIRIAMClearcutLogRecord *> *)newRecords {
+  @synchronized(self) {
+    [self.records addObjectsFromArray:newRecords];
+  }
+}
+
+- (NSArray<FIRIAMClearcutLogRecord *> *)popStillValidRecordsForUpTo:(NSInteger)upTo {
+  NSMutableArray<FIRIAMClearcutLogRecord *> *resultArray = [[NSMutableArray alloc] init];
+  NSInteger nowInSeconds = (NSInteger)[self.timeFetcher currentTimestampInSeconds];
+
+  NSInteger next = 0;
+
+  @synchronized(self) {
+    while (resultArray.count < upTo && next < self.records.count) {
+      FIRIAMClearcutLogRecord *nextRecord = self.records[next++];
+      if (nextRecord.eventTimestampInSeconds > nowInSeconds - self.recordExpiresInSeconds) {
+        // record not expired yet
+        [resultArray addObject:nextRecord];
+      }
+    }
+
+    [self.records removeObjectsInRange:NSMakeRange(0, next)];
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230005",
+              @"Returning %d clearcut retry records from popStillValidRecords",
+              (int)resultArray.count);
+  return resultArray;
+}
+
+- (void)loadFromCachePath:(NSString *)cacheFilePath {
+  NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath;
+
+  NSTimeInterval start = [self.timeFetcher currentTimestampInSeconds];
+  id fetchedClearcutRetryRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
+  if (fetchedClearcutRetryRecords) {
+    @synchronized(self) {
+      self.records = (NSMutableArray<FIRIAMClearcutLogRecord *> *)fetchedClearcutRetryRecords;
+    }
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230002",
+                @"Loaded %d clearcut log records from file in %lf seconds", (int)self.records.count,
+                (double)[self.timeFetcher currentTimestampInSeconds] - start);
+  }
+}
+
+- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath {
+  NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath;
+  @synchronized(self) {
+    BOOL saveResult = [NSKeyedArchiver archiveRootObject:self.records toFile:filePath];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM230003",
+                @"Saving %d clearcut log records into file is %@", (int)self.records.count,
+                saveResult ? @"successful" : @"failure");
+
+    return saveResult;
+  }
+}
+@end

+ 46 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.h

@@ -0,0 +1,46 @@
+/*
+ * 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/Foundation.h>
+#import "FIRIAMAnalyticsEventLogger.h"
+#import "FIRIAMClientInfoFetcher.h"
+#import "FIRIAMTimeFetcher.h"
+
+@class FIRIAMClearcutUploader;
+
+NS_ASSUME_NONNULL_BEGIN
+// FIRIAMAnalyticsEventLogger implementation using Clearcut. It turns a IAM analytics event
+// into the corresponding FIRIAMClearcutLogRecord and then hand it over to
+// a FIRIAMClearcutUploader instance for the actual sending and potential failure and retry
+// logic
+@interface FIRIAMClearcutLogger : NSObject <FIRIAMAnalyticsEventLogger>
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Create an instance which uses NSURLSession to make clearcut api calls.
+ *
+ * @param clientInfoFetcher used to fetch iid info for the current app.
+ * @param timeFetcher time fetcher object
+ * @param uploader FIRIAMClearcutUploader object for receiving the log record
+ */
+- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber
+                                fbAppId:(NSString *)fbAppId
+                      clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher
+                       usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                          usingUploader:(FIRIAMClearcutUploader *)uploader;
+@end
+NS_ASSUME_NONNULL_END

+ 203 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutLogger.m

@@ -0,0 +1,203 @@
+/*
+ * 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 <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutLogStorage.h"
+#import "FIRIAMClearcutLogger.h"
+#import "FIRIAMClearcutUploader.h"
+
+@interface FIRIAMClearcutLogger ()
+
+// these two writable for assisting unit testing need
+@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender;
+@property(readwrite, nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+
+@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher;
+@property(readonly, nonatomic) FIRIAMClearcutUploader *ctUploader;
+
+@property(readonly, copy, nonatomic) NSString *fbProjectNumber;
+@property(readonly, copy, nonatomic) NSString *fbAppId;
+
+@end
+
+@implementation FIRIAMClearcutLogger {
+  NSString *_iid;
+}
+- (instancetype)initWithFBProjectNumber:(NSString *)fbProjectNumber
+                                fbAppId:(NSString *)fbAppId
+                      clientInfoFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher
+                       usingTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                          usingUploader:(FIRIAMClearcutUploader *)uploader {
+  if (self = [super init]) {
+    _fbProjectNumber = fbProjectNumber;
+    _fbAppId = fbAppId;
+    _clientInfoFetcher = clientInfoFetcher;
+    _timeFetcher = timeFetcher;
+    _ctUploader = uploader;
+  }
+  return self;
+}
+
++ (void)updateSourceExtensionDictWithAnalyticsEventEnumType:(FIRIAMAnalyticsLogEventType)eventType
+                                                    forDict:(NSMutableDictionary *)dict {
+  switch (eventType) {
+    case FIRIAMAnalyticsEventMessageImpression:
+      dict[@"event_type"] = @"IMPRESSION_EVENT_TYPE";
+      break;
+    case FIRIAMAnalyticsEventActionURLFollow:
+      dict[@"event_type"] = @"CLICK_EVENT_TYPE";
+      break;
+    case FIRIAMAnalyticsEventMessageDismissAuto:
+      dict[@"dismiss_type"] = @"AUTO";
+      break;
+    case FIRIAMAnalyticsEventMessageDismissClick:
+      dict[@"dismiss_type"] = @"CLICK";
+      break;
+    case FIRIAMAnalyticsEventMessageDismissSwipe:
+      dict[@"dismiss_type"] = @"SWIPE";
+      break;
+    case FIRIAMAnalyticsEventImageFetchError:
+      dict[@"render_error_reason"] = @"IMAGE_FETCH_ERROR";
+      break;
+    case FIRIAMAnalyticsEventImageFormatUnsupported:
+      dict[@"render_error_reason"] = @"IMAGE_UNSUPPORTED_FORMAT";
+      break;
+    case FIRIAMAnalyticsEventFetchAPIClientError:
+      dict[@"fetch_error_reason"] = @"CLIENT_ERROR";
+      break;
+    case FIRIAMAnalyticsEventFetchAPIServerError:
+      dict[@"fetch_error_reason"] = @"SERVER_ERROR";
+      break;
+    case FIRIAMAnalyticsEventFetchAPINetworkError:
+      dict[@"fetch_error_reason"] = @"NETWORK_ERROR";
+      break;
+    case FIRIAMAnalyticsEventTestMessageImpression:
+      dict[@"event_type"] = @"TEST_MESSAGE_IMPRESSION_EVENT_TYPE";
+      break;
+    case FIRIAMAnalyticsEventTestMessageClick:
+      dict[@"event_type"] = @"TEST_MESSAGE_CLICK_EVENT_TYPE";
+      break;
+    case FIRIAMAnalyticsLogEventUnknown:
+      break;
+  }
+}
+
+// constructing CampaignAnalytics proto defined in campaign_analytics.proto and serialize it into
+// a string.
+// @return nil if error happened
+- (NSString *)constructSourceExtensionJsonForClearcutWithEventType:
+                  (FIRIAMAnalyticsLogEventType)eventType
+                                                     forCampaignID:(NSString *)campaignID
+                                                     eventTimeInMs:(NSNumber *)eventTimeInMs
+                                                        instanceID:(NSString *)instanceID {
+  NSMutableDictionary *campaignAnalyticsDict = [[NSMutableDictionary alloc] init];
+
+  campaignAnalyticsDict[@"project_number"] = self.fbProjectNumber;
+  campaignAnalyticsDict[@"campaign_id"] = campaignID;
+  campaignAnalyticsDict[@"client_app"] =
+      @{@"google_app_id" : self.fbAppId, @"firebase_instance_id" : instanceID};
+  campaignAnalyticsDict[@"client_timestamp_millis"] = eventTimeInMs;
+  [self.class updateSourceExtensionDictWithAnalyticsEventEnumType:eventType
+                                                          forDict:campaignAnalyticsDict];
+
+  campaignAnalyticsDict[@"fiam_sdk_version"] = [self.clientInfoFetcher getIAMSDKVersion];
+
+  // turn campaignAnalyticsDict into a json string
+  NSError *error;
+  NSData *jsonData = [NSJSONSerialization
+      dataWithJSONObject:campaignAnalyticsDict  // Here you can pass array or dictionary
+                 options:0  // Pass 0 if you don't care about the readability of the generated
+                            // string
+                   error:&error];
+
+  if (jsonData) {
+    NSString *jsonString;
+    jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210006",
+                @"Source extension json string produced as %@", jsonString);
+    return jsonString;
+  } else {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210007",
+                  @"Error in generating source extension json string: %@", error);
+    return nil;
+  }
+}
+
+- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType
+                   forCampaignID:(NSString *)campaignID
+               withEventTimeInMs:(nullable NSNumber *)eventTimeInMs
+                             IID:(NSString *)iid
+                      completion:(void (^)(BOOL success))completion {
+  NSTimeInterval nowInMs = [self.timeFetcher currentTimestampInSeconds] * 1000;
+  if (!eventTimeInMs) {
+    eventTimeInMs = @((long)nowInMs);
+  }
+
+  NSString *sourceExtensionJsonString =
+      [self constructSourceExtensionJsonForClearcutWithEventType:eventType
+                                                   forCampaignID:campaignID
+                                                   eventTimeInMs:eventTimeInMs
+                                                      instanceID:iid];
+
+  FIRIAMClearcutLogRecord *newRecord = [[FIRIAMClearcutLogRecord alloc]
+      initWithExtensionJsonString:sourceExtensionJsonString
+          eventTimestampInSeconds:eventTimeInMs.integerValue / 1000];
+  [self.ctUploader addNewLogRecord:newRecord];
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210003",
+              @"One more clearcut log record created and sent to uploader with source extension %@",
+              sourceExtensionJsonString);
+  completion(YES);
+}
+
+- (void)logAnalyticsEventForType:(FIRIAMAnalyticsLogEventType)eventType
+                   forCampaignID:(NSString *)campaignID
+                withCampaignName:(NSString *)campaignName
+                   eventTimeInMs:(nullable NSNumber *)eventTimeInMs
+                      completion:(void (^)(BOOL success))completion {
+  if (!_iid) {
+    [self.clientInfoFetcher
+        fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber
+                               withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token,
+                                                NSError *_Nullable error) {
+                                 if (error) {
+                                   FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM210001",
+                                                 @"Failed to get iid value for clearcut logging %@",
+                                                 error);
+                                   completion(NO);
+                                 } else {
+                                   // persist iid through the whole life-cycle
+                                   self->_iid = iid;
+                                   [self logAnalyticsEventForType:eventType
+                                                    forCampaignID:campaignID
+                                                withEventTimeInMs:eventTimeInMs
+                                                              IID:iid
+                                                       completion:completion];
+                                 }
+                               }];
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210004",
+                @"Using remembered iid for event logging");
+    [self logAnalyticsEventForType:eventType
+                     forCampaignID:campaignID
+                 withEventTimeInMs:eventTimeInMs
+                               IID:_iid
+                        completion:completion];
+  }
+}
+@end

+ 75 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.h

@@ -0,0 +1,75 @@
+/*
+ * 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/Foundation.h>
+
+@class FIRIAMClearcutLogRecord;
+@class FIRIAMClearcutHttpRequestSender;
+@class FIRIAMClearcutLogStorage;
+
+@protocol FIRIAMTimeFetcher;
+
+NS_ASSUME_NONNULL_BEGIN
+
+// class for defining a number of configs to control clearcut upload behavior
+@interface FIRIAMClearcutStrategy : NSObject
+
+// minimalWaitTimeInMills and maximumWaitTimeInMills defines the bottom and
+// upper bound of the wait time before next upload if prior upload attempt was
+// successful. Clearcut may return a value to give the wait time guidance in
+// the upload response, but we also use these two values for sanity check to avoid
+// too crazy behavior if the guidance value from server does not make sense
+@property(nonatomic, readonly) NSInteger minimalWaitTimeInMills;
+@property(nonatomic, readonly) NSInteger maximumWaitTimeInMills;
+
+// back off wait time in mills if a prior upload attempt fails
+@property(nonatomic, readonly) NSInteger failureBackoffTimeInMills;
+
+// the maximum number of log records to be sent in one upload attempt
+@property(nonatomic, readonly) NSInteger batchSendSize;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills
+                        maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills
+                 failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills
+                             batchSendSize:(NSInteger)batchSendSize;
+
+- (NSString *)description;
+@end
+
+// A class for accepting new clearcut logs and scheduling the uploading of the logs in batches
+// based on defined strategies.
+@interface FIRIAMClearcutUploader : NSObject
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ *
+ * @param userDefaults needed for tracking upload timing info persistently.If nil, using
+ * NSUserDefaults standardUserDefaults. It's defined as a parameter to help with
+ * unit testing mocking
+ */
+- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender
+                          timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                           logStorage:(FIRIAMClearcutLogStorage *)retryStorage
+                        usingStrategy:(FIRIAMClearcutStrategy *)strategy
+                    usingUserDefaults:(nullable NSUserDefaults *)userDefaults;
+/**
+ * This should return very quickly without blocking on and actual log uploading to
+ * clearcut server, which is done asynchronously
+ */
+- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record;
+@end
+NS_ASSUME_NONNULL_END

+ 233 - 0
Firebase/InAppMessaging/Analytics/FIRIAMClearcutUploader.m

@@ -0,0 +1,233 @@
+/*
+ * 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 <FirebaseCore/FIRLogger.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutUploader.h"
+#import "FIRIAMTimeFetcher.h"
+
+#import "FIRIAMClearcutHttpRequestSender.h"
+#import "FIRIAMClearcutLogStorage.h"
+
+// a macro for turning a millisecond value into seconds
+#define MILLS_TO_SECONDS(x) (((long)x) / 1000)
+
+@implementation FIRIAMClearcutStrategy
+- (instancetype)initWithMinWaitTimeInMills:(NSInteger)minWaitTimeInMills
+                        maxWaitTimeInMills:(NSInteger)maxWaitTimeInMills
+                 failureBackoffTimeInMills:(NSInteger)failureBackoffTimeInMills
+                             batchSendSize:(NSInteger)batchSendSize {
+  if (self = [super init]) {
+    _minimalWaitTimeInMills = minWaitTimeInMills;
+    _maximumWaitTimeInMills = maxWaitTimeInMills;
+    _failureBackoffTimeInMills = failureBackoffTimeInMills;
+    _batchSendSize = batchSendSize;
+  }
+  return self;
+}
+
+- (NSString *)description {
+  return [NSString stringWithFormat:@"min wait time in seconds:%ld;max wait time in seconds:%ld;"
+                                     "failure backoff time in seconds:%ld;batch send size:%d",
+                                    MILLS_TO_SECONDS(self.minimalWaitTimeInMills),
+                                    MILLS_TO_SECONDS(self.maximumWaitTimeInMills),
+                                    MILLS_TO_SECONDS(self.failureBackoffTimeInMills),
+                                    (int)self.batchSendSize];
+}
+@end
+
+@interface FIRIAMClearcutUploader () {
+  dispatch_queue_t _queue;
+  BOOL _nextSendScheduled;
+}
+
+@property(readwrite, nonatomic) FIRIAMClearcutHttpRequestSender *requestSender;
+@property(nonatomic, assign) int64_t nextValidSendTimeInMills;
+
+@property(nonatomic, readonly) id<FIRIAMTimeFetcher> timeFetcher;
+@property(nonatomic, readonly) FIRIAMClearcutLogStorage *logStorage;
+
+@property(nonatomic, readonly) FIRIAMClearcutStrategy *strategy;
+@property(nonatomic, readonly) NSUserDefaults *userDefaults;
+@end
+
+static NSString *FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills =
+    @"firebase-iam-next-clearcut-upload-timestamp-in-mills";
+
+/**
+ * The high level behavior in this implementation is like this
+ *  1 New records always pushed into FIRIAMClearcutLogStorage first.
+ *  2 Upload log records in batches.
+ *  3 If prior upload was successful, next upload would wait for the time parsed out of the
+ *      clearcut response body.
+ *  4 If prior upload failed, next upload attempt would wait for failureBackoffTimeInMills defined
+ *      in strategy
+ *  5 When app
+ */
+
+@implementation FIRIAMClearcutUploader
+
+- (instancetype)initWithRequestSender:(FIRIAMClearcutHttpRequestSender *)requestSender
+                          timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                           logStorage:(FIRIAMClearcutLogStorage *)logStorage
+                        usingStrategy:(FIRIAMClearcutStrategy *)strategy
+                    usingUserDefaults:(nullable NSUserDefaults *)userDefaults {
+  if (self = [super init]) {
+    _nextSendScheduled = NO;
+    _timeFetcher = timeFetcher;
+    _requestSender = requestSender;
+    _logStorage = logStorage;
+    _strategy = strategy;
+    _queue = dispatch_queue_create("com.google.firebase.inappmessaging.clearcut_upload", NULL);
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(appWillEnterForeground:)
+                                                 name:UIApplicationWillEnterForegroundNotification
+                                               object:nil];
+
+    _userDefaults = userDefaults ? userDefaults : [NSUserDefaults standardUserDefaults];
+    // it would be 0 if it does not exist, which is equvilent to saying that
+    // you can send now
+    _nextValidSendTimeInMills = (int64_t)
+        [_userDefaults doubleForKey:FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
+
+    // seed the first send upon SDK start-up
+    [self scheduleNextSend];
+
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260001",
+                @"FIRIAMClearcutUploader created with strategy as %@", self.strategy);
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)appWillEnterForeground:(UIApplication *)application {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260010",
+              @"App foregrounded, FIRIAMClearcutUploader will seed next send");
+  [self scheduleNextSend];
+}
+
+- (void)addNewLogRecord:(FIRIAMClearcutLogRecord *)record {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260002",
+              @"New log record sent to clearcut uploader");
+
+  [self.logStorage pushRecords:@[ record ]];
+  [self scheduleNextSend];
+}
+
+- (void)attemptUploading {
+  NSArray<FIRIAMClearcutLogRecord *> *availbleLogs =
+      [self.logStorage popStillValidRecordsForUpTo:self.strategy.batchSendSize];
+
+  if (availbleLogs.count > 0) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260011", @"Deliver %d clearcut records",
+                (int)availbleLogs.count);
+    [self.requestSender
+        sendClearcutHttpRequestForLogs:availbleLogs
+                        withCompletion:^(BOOL success, BOOL shouldRetryLogs,
+                                         int64_t waitTimeInMills) {
+                          if (success) {
+                            FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260003",
+                                        @"Delivering %d clearcut records was successful",
+                                        (int)availbleLogs.count);
+                            // make sure the effective wait time is between two bounds
+                            // defined in strategy
+                            waitTimeInMills =
+                                MAX(self.strategy.minimalWaitTimeInMills, waitTimeInMills);
+
+                            waitTimeInMills =
+                                MIN(waitTimeInMills, self.strategy.maximumWaitTimeInMills);
+                          } else {
+                            // failed to deliver
+                            FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260004",
+                                        @"Failed to attempt the delivery of %d clearcut "
+                                        @"records and should-retry for them is %@",
+                                        (int)availbleLogs.count, shouldRetryLogs ? @"YES" : @"NO");
+                            if (shouldRetryLogs) {
+                              /**
+                               * Note that there is a chance that the app crashes before we can
+                               * call pushRecords: on the logStorage below which means we lost
+                               * these log records permanently. This is a trade-off between handling
+                               * duplicate records on server side vs taking the risk of lossing
+                               * data. This implementation picks the latter.
+                               */
+                              FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007",
+                                          @"Push failed log records back to storage");
+                              [self.logStorage pushRecords:availbleLogs];
+                            }
+
+                            waitTimeInMills = (int64_t)self.strategy.failureBackoffTimeInMills;
+                          }
+
+                          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260005",
+                                      @"Wait for at least %ld seconds before next upload attempt",
+                                      MILLS_TO_SECONDS(waitTimeInMills));
+
+                          self.nextValidSendTimeInMills =
+                              (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000 +
+                              waitTimeInMills;
+
+                          // persisted so that it can be recovered next time the app runs
+                          [self.userDefaults
+                              setDouble:(double)self.nextValidSendTimeInMills
+                                 forKey:
+                                     FIRIAM_UserDefaultsKeyForNextValidClearcutUploadTimeInMills];
+
+                          @synchronized(self) {
+                            self->_nextSendScheduled = NO;
+                          }
+                          [self scheduleNextSend];
+                        }];
+
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260007", @"No clearcut records to be uploaded");
+    @synchronized(self) {
+      _nextSendScheduled = NO;
+    }
+  }
+}
+
+- (void)scheduleNextSend {
+  @synchronized(self) {
+    if (_nextSendScheduled) {
+      // already scheduled next send, don't do it again
+      return;
+    } else {
+      _nextSendScheduled = YES;
+    }
+  }
+
+  int64_t delayTimeInMills =
+      self.nextValidSendTimeInMills - (int64_t)[self.timeFetcher currentTimestampInSeconds] * 1000;
+
+  if (delayTimeInMills <= 0) {
+    delayTimeInMills = 0;  // no need to delay since we can send now
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM260006",
+              @"Next upload attempt scheduled in %d seconds", (int)delayTimeInMills / 1000);
+
+  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayTimeInMills * (int64_t)NSEC_PER_MSEC),
+                 _queue, ^{
+                   [self attemptUploading];
+                 });
+}
+
+@end

+ 6 - 0
Firebase/InAppMessaging/CHANGELOG.md

@@ -0,0 +1,6 @@
+# 2018-09-25 -- v0.12.0
+- Separated UI functionality into a new open source SDK called FirebaseInAppMessagingDisplay.
+- Respect fetch between wait time returned from API responses.
+
+# 2018-08-15 -- v0.11.0
+- First Beta Release.

+ 40 - 0
Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.h

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRIAMMessageDefinition;
+@protocol FIRIAMTimeFetcher;
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Class responsible for parsing the json response data from the restful API endpoint
+// for serving eligible messages for the current SDK clients.
+@interface FIRIAMFetchResponseParser : NSObject
+
+// Turn the API response into a number of FIRIAMMessageDefinition objects. If any of them is invalid
+// it would be ignored and not represented in the response array.
+// @param discardCount if not nil, it would contain, on return, the number of invalid messages
+// detected uring parsing.
+// @param fetchWaitTime would be non nil if fetch wait time data is found in the api response.
+- (NSArray<FIRIAMMessageDefinition *> *)parseAPIResponseDictionary:(NSDictionary *)responseDict
+                                                 discardedMsgCount:(NSInteger *)discardCount
+                                            fetchWaitTimeInSeconds:(NSNumber **)fetchWaitTime;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher;
+@end
+NS_ASSUME_NONNULL_END

+ 313 - 0
Firebase/InAppMessaging/Data/FIRIAMFetchResponseParser.m

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

+ 42 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageContentData.h

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+/**
+ * This protocol models the message content (non-ui related) data for an in-app message.
+ */
+@protocol FIRIAMMessageContentData
+@property(nonatomic, readonly, nonnull) NSString *titleText;
+@property(nonatomic, readonly, nonnull) NSString *bodyText;
+@property(nonatomic, readonly, nullable) NSString *actionButtonText;
+@property(nonatomic, readonly, nullable) NSURL *actionURL;
+@property(nonatomic, readonly, nullable) NSURL *imageURL;
+
+// Load image data and report the result in the callback block.
+// Expect these cases in the callback block
+// If error happens, error parameter will be non-nil.
+// If no error happens and imageData parameter is nil, it indicates the case that there
+// is no image assoicated with the message.
+// If error is nil and imageData is not nil, then the image data is loaded successfully
+- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
+                                         NSError *_Nullable error))block;
+
+// convert to a description string of the content
+- (NSString *)description;
+@end
+NS_ASSUME_NONNULL_END

+ 47 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.h

@@ -0,0 +1,47 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRIAMMessageContentData.h"
+
+NS_ASSUME_NONNULL_BEGIN
+/**
+ * An implementation for protocol FIRIAMMessageContentData. This class takes a image url
+ * and fetch it over the network to retrieve the image data.
+ */
+@interface FIRIAMMessageContentDataWithImageURL : NSObject <FIRIAMMessageContentData>
+/**
+ * Create an instance which uses NSURLSession to do the image data fetching.
+ *
+ * @param title Message title text.
+ * @param body Message body text.
+ * @param actionButtonText Text for action button.
+ * @param actionURL url string for action.
+ * @param imageURL  the url to the image. It can be nil to indicate the non-image in-app
+ *                  message case.
+ * @param URLSession can be nil in which case the class would create NSURLSession
+ *                   internally to perform the network request. Having it here so that
+ *                   it's easier for doing mocking with unit testing.
+ */
+- (instancetype)initWithMessageTitle:(NSString *)title
+                         messageBody:(NSString *)body
+                    actionButtonText:(nullable NSString *)actionButtonText
+                           actionURL:(nullable NSURL *)actionURL
+                            imageURL:(nullable NSURL *)imageURL
+                     usingURLSession:(nullable NSURLSession *)URLSession;
+@end
+NS_ASSUME_NONNULL_END

+ 138 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageContentDataWithImageURL.m

@@ -0,0 +1,138 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMMessageContentData.h"
+#import "FIRIAMMessageContentDataWithImageURL.h"
+#import "FIRIAMSDKRuntimeErrorCodes.h"
+
+static NSInteger const SuccessHTTPStatusCode = 200;
+
+@interface FIRIAMMessageContentDataWithImageURL ()
+@property(nonatomic, readwrite, nonnull, copy) NSString *titleText;
+@property(nonatomic, readwrite, nonnull, copy) NSString *bodyText;
+@property(nonatomic, copy, nullable) NSString *actionButtonText;
+@property(nonatomic, copy, nullable) NSURL *actionURL;
+@property(nonatomic, nullable, copy) NSURL *imageURL;
+@property(readonly) NSURLSession *URLSession;
+@end
+
+@implementation FIRIAMMessageContentDataWithImageURL
+- (instancetype)initWithMessageTitle:(NSString *)title
+                         messageBody:(NSString *)body
+                    actionButtonText:(nullable NSString *)actionButtonText
+                           actionURL:(nullable NSURL *)actionURL
+                            imageURL:(nullable NSURL *)imageURL
+                     usingURLSession:(nullable NSURLSession *)URLSession {
+  if (self = [super init]) {
+    _titleText = title;
+    _bodyText = body;
+    _imageURL = imageURL;
+    _actionButtonText = actionButtonText;
+    _actionURL = actionURL;
+
+    if (imageURL) {
+      _URLSession = URLSession ? URLSession : [NSURLSession sharedSession];
+    }
+  }
+  return self;
+}
+
+#pragma protocol FIRIAMMessageContentData
+
+- (NSString *)description {
+  return [NSString stringWithFormat:@"Message content: title '%@',"
+                                     "body '%@', imageURL '%@', action URL '%@'",
+                                    self.titleText, self.bodyText, self.imageURL, self.actionURL];
+}
+
+- (NSString *)getTitleText {
+  return _titleText;
+}
+
+- (NSString *)getBodyText {
+  return _bodyText;
+}
+
+- (nullable NSString *)getActionButtonText {
+  return _actionButtonText;
+}
+
+- (void)loadImageDataWithBlock:(void (^)(NSData *_Nullable imageData,
+                                         NSError *_Nullable error))block {
+  if (!block) {
+    // no need for any further action if block is nil
+    return;
+  }
+
+  if (!_imageURL) {
+    // no image data since image url is nil
+    block(nil, nil);
+  } else {
+    NSURLRequest *imageDataRequest = [NSURLRequest requestWithURL:_imageURL];
+    NSURLSessionDataTask *task = [_URLSession
+        dataTaskWithRequest:imageDataRequest
+          completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+            if (error) {
+              FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000003",
+                            @"Error in fetching image: %@", error);
+              block(nil, error);
+            } else {
+              if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+                NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+                if (httpResponse.statusCode == SuccessHTTPStatusCode) {
+                  if (httpResponse.MIMEType == nil || ![httpResponse.MIMEType hasPrefix:@"image"]) {
+                    NSString *errorDesc =
+                        [NSString stringWithFormat:@"None image MIME type %@"
+                                                    " detected for url %@",
+                                                   httpResponse.MIMEType, self.imageURL];
+                    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000004", @"%@", errorDesc);
+
+                    NSError *error =
+                        [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                                            code:FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL
+                                        userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
+                    block(nil, error);
+                  } else {
+                    block(data, nil);
+                  }
+                } else {
+                  NSString *errorDesc =
+                      [NSString stringWithFormat:@"Failed HTTP request to crawl image %@: "
+                                                  "HTTP status code as %ld",
+                                                 self->_imageURL, (long)httpResponse.statusCode];
+                  FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000001", @"%@", errorDesc);
+                  NSError *error =
+                      [NSError errorWithDomain:NSURLErrorDomain
+                                          code:httpResponse.statusCode
+                                      userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
+                  block(nil, error);
+                }
+              } else {
+                FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM000002",
+                              @"Internal error: got a non http response from fetching image for "
+                              @"image url as %@",
+                              self->_imageURL);
+              }
+            }
+          }];
+    [task resume];
+  }
+}
+@end

+ 60 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.h

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import <Foundation/Foundation.h>
+
+#import "FIRIAMMessageRenderData.h"
+
+@class FIRIAMDisplayTriggerDefinition;
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMMessageDefinition : NSObject
+@property(nonatomic, nonnull, readonly) FIRIAMMessageRenderData *renderData;
+
+// metadata data that does not affect the rendering content/effect directly
+@property(nonatomic, readonly) NSTimeInterval startTime;
+@property(nonatomic, readonly) NSTimeInterval endTime;
+
+// a fiam message can have multiple triggers and any of them on its own can cause
+// the message to be rendered
+@property(nonatomic, readonly) NSArray<FIRIAMDisplayTriggerDefinition *> *renderTriggers;
+
+/// A flag for client-side testing messages
+@property(nonatomic, readonly) BOOL isTestMessage;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Create a regular message definition.
+ */
+- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData
+                         startTime:(NSTimeInterval)startTime
+                           endTime:(NSTimeInterval)endTime
+                 triggerDefinition:(NSArray<FIRIAMDisplayTriggerDefinition *> *)renderTriggers;
+
+/**
+ * Create a test message definition.
+ */
+- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData;
+
+- (BOOL)messageHasExpired;
+- (BOOL)messageHasStarted;
+
+// should this message be rendered when the app gets foregrounded?
+- (BOOL)messageRenderedOnAppForegroundEvent;
+// should this message be rendered when a given analytics event is fired?
+- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName;
+@end
+NS_ASSUME_NONNULL_END

+ 85 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageDefinition.m

@@ -0,0 +1,85 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMMessageDefinition.h"
+#import "FIRIAMDisplayTriggerDefinition.h"
+
+@implementation FIRIAMMessageRenderData
+
+- (instancetype)initWithMessageID:(NSString *)messageID
+                      messageName:(NSString *)messageName
+                      contentData:(id<FIRIAMMessageContentData>)contentData
+                  renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect {
+  if (self = [super init]) {
+    _contentData = contentData;
+    _renderingEffectSettings = renderEffect;
+    _messageID = [messageID copy];
+    _name = [messageName copy];
+  }
+  return self;
+}
+@end
+
+@implementation FIRIAMMessageDefinition
+- (instancetype)initWithRenderData:(FIRIAMMessageRenderData *)renderData
+                         startTime:(NSTimeInterval)startTime
+                           endTime:(NSTimeInterval)endTime
+                 triggerDefinition:(NSArray<FIRIAMDisplayTriggerDefinition *> *)renderTriggers {
+  if (self = [super init]) {
+    _renderData = renderData;
+    _renderTriggers = renderTriggers;
+    _startTime = startTime;
+    _endTime = endTime;
+    _isTestMessage = NO;
+  }
+  return self;
+}
+
+- (instancetype)initTestMessageWithRenderData:(FIRIAMMessageRenderData *)renderData {
+  if (self = [super init]) {
+    _renderData = renderData;
+    _isTestMessage = YES;
+  }
+  return self;
+}
+
+- (BOOL)messageHasExpired {
+  return self.endTime < [[NSDate date] timeIntervalSince1970];
+}
+
+- (BOOL)messageRenderedOnAppForegroundEvent {
+  for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) {
+    if (nextTrigger.triggerType == FIRIAMRenderTriggerOnAppForeground) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
+- (BOOL)messageRenderedOnAnalyticsEvent:(NSString *)eventName {
+  for (FIRIAMDisplayTriggerDefinition *nextTrigger in self.renderTriggers) {
+    if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent &&
+        [nextTrigger.firebaseEventName isEqualToString:eventName]) {
+      return YES;
+    }
+  }
+  return NO;
+}
+
+- (BOOL)messageHasStarted {
+  return self.startTime < [[NSDate date] timeIntervalSince1970];
+}
+@end

+ 36 - 0
Firebase/InAppMessaging/Data/FIRIAMMessageRenderData.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRIAMRenderingEffectSetting.h"
+
+@protocol FIRIAMMessageContentData;
+NS_ASSUME_NONNULL_BEGIN
+// This wraps the data that's needed for render the message's content in UI. It also contains
+// certain meta data that's needed in responding to user's action
+@interface FIRIAMMessageRenderData : NSObject
+@property(nonatomic, nonnull, readonly) id<FIRIAMMessageContentData> contentData;
+@property(nonatomic, nonnull, readonly) FIRIAMRenderingEffectSetting *renderingEffectSettings;
+@property(nonatomic, nonnull, copy, readonly) NSString *messageID;
+@property(nonatomic, nonnull, copy, readonly) NSString *name;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+                      messageName:(NSString *)messageName
+                      contentData:(id<FIRIAMMessageContentData>)contentData
+                  renderingEffect:(FIRIAMRenderingEffectSetting *)renderEffect;
+@end
+NS_ASSUME_NONNULL_END

+ 56 - 0
Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.h

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+typedef NS_ENUM(NSInteger, FIRIAMRenderingMode) {
+  FIRIAMRenderAsBannerView,
+  FIRIAMRenderAsModalView,
+  FIRIAMRenderAsImageOnlyView
+};
+
+/**
+ * A class for modeling rendering effect settings for in-app messaging
+ */
+@interface FIRIAMRenderingEffectSetting : NSObject
+
+@property(nonatomic) FIRIAMRenderingMode viewMode;
+
+// background color for the display area, including both the text's background and
+// padding's background
+@property(nonatomic, copy) UIColor *displayBGColor;
+
+// text color, covering both the title and body texts
+@property(nonatomic, copy) UIColor *textColor;
+
+// text color for action button
+@property(nonatomic, copy) UIColor *btnTextColor;
+
+// background color for action button
+@property(nonatomic, copy) UIColor *btnBGColor;
+
+// duration of the banner view before triggering auto-dismiss
+@property(nonatomic) CGFloat autoDimissBannerAfterNSeconds;
+
+// A flag to control rendering the message as a client-side testing message
+@property(nonatomic) BOOL isTestMessage;
+
++ (instancetype)getDefaultRenderingEffectSetting;
+@end
+NS_ASSUME_NONNULL_END

+ 31 - 0
Firebase/InAppMessaging/Data/FIRIAMRenderingEffectSetting.m

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "FIRIAMRenderingEffectSetting.h"
+
+@implementation FIRIAMRenderingEffectSetting
+
++ (instancetype)getDefaultRenderingEffectSetting {
+  FIRIAMRenderingEffectSetting *setting = [[FIRIAMRenderingEffectSetting alloc] init];
+
+  setting.btnBGColor = [UIColor colorWithRed:0.3 green:0.55 blue:0.28 alpha:1.0];
+  setting.displayBGColor = [UIColor whiteColor];
+  setting.textColor = [UIColor blackColor];
+  setting.btnTextColor = [UIColor whiteColor];
+  setting.autoDimissBannerAfterNSeconds = 12;
+  setting.isTestMessage = NO;
+  return setting;
+}
+@end

+ 34 - 0
Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FIRIAMRenderTrigger) {
+  FIRIAMRenderTriggerOnAppForeground,
+  FIRIAMRenderTriggerOnFirebaseAnalyticsEvent
+};
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMDisplayTriggerDefinition : NSObject
+@property(nonatomic, readonly) FIRIAMRenderTrigger triggerType;
+
+// applicable only when triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent
+@property(nonatomic, copy, nullable, readonly) NSString *firebaseEventName;
+
+- (instancetype)initForAppForegroundTrigger;
+- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title;
+@end
+NS_ASSUME_NONNULL_END

+ 33 - 0
Firebase/InAppMessaging/DisplayTrigger/FIRIAMDisplayTriggerDefinition.m

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMDisplayTriggerDefinition.h"
+
+@implementation FIRIAMDisplayTriggerDefinition
+- (instancetype)initForAppForegroundTrigger {
+  if (self = [super init]) {
+    _triggerType = FIRIAMRenderTriggerOnAppForeground;
+  }
+  return self;
+}
+- (instancetype)initWithFirebaseAnalyticEvent:(NSString *)title {
+  if (self = [super init]) {
+    _triggerType = FIRIAMRenderTriggerOnFirebaseAnalyticsEvent;
+    _firebaseEventName = title;
+  }
+  return self;
+}
+@end

+ 36 - 0
Firebase/InAppMessaging/FIRCore+InAppMessaging.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+// This file contains declarations that should go into FirebaseCore when
+// Firebase InAppMessaging is merged into master. Keep them separate now to help
+// with build from development folder and avoid merge conflicts.
+
+// this should eventually be in FIRLogger.h
+extern FIRLoggerService kFIRLoggerInAppMessaging;
+
+// this should eventually be in FIRError.h
+extern NSString *const kFirebaseInAppMessagingErrorDomain;
+
+// this should eventually be in FIRError.h FIRAppInternal.h:46:
+extern NSString *const kFIRServiceInAppMessaging;
+
+// InAppMessaging doesn't provide any functionality to other components,
+// so it provides a private, empty protocol that it conforms to and use it for registration.
+
+@protocol FIRInAppMessagingInstanceProvider
+@end

+ 22 - 0
Firebase/InAppMessaging/FIRCore+InAppMessaging.m

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRCore+InAppMessaging.h"
+
+NSString *const kFIRServiceInAppMessaging = @"InAppMessaging";
+NSString *const kFirebaseInAppMessagingErrorDomain = @"com.firebase.inappmessaging";
+FIRLoggerService kFIRLoggerInAppMessaging = @"[Firebase/InAppMessaging]";

+ 144 - 0
Firebase/InAppMessaging/FIRInAppMessaging.m

@@ -0,0 +1,144 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRInAppMessaging.h"
+
+#import <Foundation/Foundation.h>
+
+#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
+#import <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRComponent.h>
+#import <FirebaseCore/FIRComponentContainer.h>
+#import <FirebaseCore/FIRDependency.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMDisplayExecutor.h"
+#import "FIRIAMRuntimeManager.h"
+#import "FIRInAppMessaging+Bootstrap.h"
+#import "FIRInAppMessagingPrivate.h"
+
+static BOOL _autoBootstrapOnFIRAppInit = YES;
+
+@implementation FIRInAppMessaging {
+  BOOL _messageDisplaySuppressed;
+}
+
+// Call this to present the SDK being auto bootstrapped with other Firebase SDKs. It needs
+// to be triggered before [FIRApp configure] is executed. This should only be needed for
+// testing app that wants to use custom fiam SDK settings.
++ (void)disableAutoBootstrapWithFIRApp {
+  _autoBootstrapOnFIRAppInit = NO;
+}
+
+// extract macro value into a C string
+#define STR_FROM_MACRO(x) #x
+#define STR(x) STR_FROM_MACRO(x)
+
++ (void)load {
+  [FIRApp
+      registerInternalLibrary:(Class<FIRLibrary>)self
+                     withName:@"fire-iam"
+                  withVersion:[NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)]];
+}
+
++ (nonnull NSArray<FIRComponent *> *)componentsToRegister {
+  FIRDependency *analyticsDep = [FIRDependency dependencyWithProtocol:@protocol(FIRAnalyticsInterop)
+                                                           isRequired:YES];
+  FIRComponentCreationBlock creationBlock =
+      ^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
+    // Ensure it's cached so it returns the same instance every time fiam is called.
+    *isCacheable = YES;
+    id<FIRAnalyticsInterop> analytics = FIR_COMPONENT(FIRAnalyticsInterop, container);
+    return [[FIRInAppMessaging alloc] initWithAnalytics:analytics];
+  };
+  FIRComponent *fiamProvider =
+      [FIRComponent componentWithProtocol:@protocol(FIRInAppMessagingInstanceProvider)
+                      instantiationTiming:FIRInstantiationTimingLazy
+                             dependencies:@[ analyticsDep ]
+                            creationBlock:creationBlock];
+
+  return @[ fiamProvider ];
+}
+
++ (void)configureWithApp:(FIRApp *)app {
+  if (!app.isDefaultApp) {
+    // Only configure for the default FIRApp.
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170000",
+                @"Firebase InAppMessaging only works with the default app.");
+    return;
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170001",
+              @"Got notification for kFIRAppReadyToConfigureSDKNotification");
+  if (_autoBootstrapOnFIRAppInit) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170002",
+                @"Auto bootstrap Firebase in-app messaging SDK");
+    [self bootstrapIAMFromFIRApp:app];
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170003",
+                @"No auto bootstrap Firebase in-app messaging SDK");
+  }
+}
+
+- (instancetype)initWithAnalytics:(id<FIRAnalyticsInterop>)analytics {
+  if (self = [super init]) {
+    _messageDisplaySuppressed = NO;
+    _analytics = analytics;
+  }
+  return self;
+}
+
++ (FIRInAppMessaging *)inAppMessaging {
+  FIRApp *defaultApp = [FIRApp defaultApp];  // Missing configure will be logged here.
+  id<FIRInAppMessagingInstanceProvider> inAppMessaging =
+      FIR_COMPONENT(FIRInAppMessagingInstanceProvider, defaultApp.container);
+  return (FIRInAppMessaging *)inAppMessaging;
+}
+
+- (BOOL)messageDisplaySuppressed {
+  return _messageDisplaySuppressed;
+}
+
+- (void)setMessageDisplaySuppressed:(BOOL)suppressed {
+  _messageDisplaySuppressed = suppressed;
+  [[FIRIAMRuntimeManager getSDKRuntimeInstance] setShouldSuppressMessageDisplay:suppressed];
+}
+
+- (BOOL)automaticDataCollectionEnabled {
+  return [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled;
+}
+
+- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled {
+  [FIRIAMRuntimeManager getSDKRuntimeInstance].automaticDataCollectionEnabled =
+      automaticDataCollectionEnabled;
+}
+
+- (void)setMessageDisplayComponent:(id<FIRInAppMessagingDisplay>)messageDisplayComponent {
+  _messageDisplayComponent = messageDisplayComponent;
+
+  if (messageDisplayComponent == nil) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002", @"messageDisplayComponent set to nil.");
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001",
+                @"Setting a non-nil message display component");
+  }
+
+  // Forward the setting to the display executor.
+  [FIRIAMRuntimeManager getSDKRuntimeInstance].displayExecutor.messageDisplayComponent =
+      messageDisplayComponent;
+}
+
+@end

+ 26 - 0
Firebase/InAppMessaging/FIRInAppMessagingPrivate.h

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019 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 <FirebaseCore/FIRLibrary.h>
+#import "FIRCore+InAppMessaging.h"
+#import "FIRInAppMessaging.h"
+
+@protocol FIRInAppMessagingInstanceProvider;
+@protocol FIRLibrary;
+
+@interface FIRInAppMessaging () <FIRInAppMessagingInstanceProvider, FIRLibrary>
+@property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
+@end

+ 89 - 0
Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.h

@@ -0,0 +1,89 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+/// Values for different fiam activity types.
+typedef NS_ENUM(NSInteger, FIRIAMActivityType) {
+  FIRIAMActivityTypeFetchMessage = 0,
+  FIRIAMActivityTypeRenderMessage = 1,
+  FIRIAMActivityTypeDismissMessage = 2,
+
+  // Triggered checks
+  FIRIAMActivityTypeCheckForOnOpenMessage = 3,
+  FIRIAMActivityTypeCheckForAnalyticsEventMessage = 4,
+  FIRIAMActivityTypeCheckForFetch = 5,
+};
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMActivityRecord : NSObject <NSCoding>
+@property(nonatomic, nonnull, readonly) NSDate *timestamp;
+@property(nonatomic, readonly) FIRIAMActivityType activityType;
+@property(nonatomic, readonly) BOOL success;
+@property(nonatomic, copy, nonnull, readonly) NSString *detail;
+
+- (instancetype)init NS_UNAVAILABLE;
+// Current timestamp would be fetched if parameter 'timestamp' is passed in as null
+- (instancetype)initWithActivityType:(FIRIAMActivityType)type
+                        isSuccessful:(BOOL)isSuccessful
+                          withDetail:(NSString *)detail
+                           timestamp:(nullable NSDate *)timestamp;
+
+- (NSString *)displayStringForActivityType;
+@end
+
+/**
+ * This is the class for tracking fiam flow related activity logs. Its content can later on be
+ * retrieved for debugging/reporting purpose.
+ */
+@interface FIRIAMActivityLogger : NSObject
+
+// If it's NO, activity logs of certain types won't get recorded by Logger. Consult
+// isMandatoryType implementation to tell what are the types belong to verbose mode
+// Turn it on for debugging cases
+@property(nonatomic, readonly) BOOL verboseMode;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+/**
+ * Parameter maxBeforeReduce and sizeAfterReduce defines the shrinking behavior when we reach
+ * the size cap of log storage: when we see the number of log records goes beyond
+ * maxBeforeReduce, we would trigger a reduction action which would bring the array length to be
+ * the size as defined by sizeAfterReduce
+ *
+ * @param verboseMode see the comments for the verboseMode property
+ * @param loadFromCache loads from cache to initialize the log list if it's true. Be aware that
+ *     in this case, you should not call this method in main thread since reading the cache file
+ *     can take time.
+ */
+- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce
+                         withSizeAfterReduce:(NSInteger)sizeAfterReduce
+                                 verboseMode:(BOOL)verboseMode
+                               loadFromCache:(BOOL)loadFromCache;
+
+/**
+ * Inserting a new record into activity log.
+ *
+ * @param newRecord new record to be inserted
+ */
+- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord;
+
+/**
+ * Get a immutable copy of the existing activity log records.
+ */
+- (NSArray<FIRIAMActivityRecord *> *)readRecords;
+@end
+NS_ASSUME_NONNULL_END

+ 215 - 0
Firebase/InAppMessaging/Flows/FIRIAMActivityLogger.m

@@ -0,0 +1,215 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <UIKit/UIKit.h>
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMActivityLogger.h"
+@implementation FIRIAMActivityRecord
+
+static NSString *const kActiveTypeArchiveKey = @"type";
+static NSString *const kIsSuccessArchiveKey = @"is_success";
+static NSString *const kTimeStampArchiveKey = @"timestamp";
+static NSString *const kDetailArchiveKey = @"detail";
+
+- (id)initWithCoder:(NSCoder *)decoder {
+  self = [super init];
+  if (self != nil) {
+    _activityType = [decoder decodeIntegerForKey:kActiveTypeArchiveKey];
+    _timestamp = [decoder decodeObjectForKey:kTimeStampArchiveKey];
+    _success = [decoder decodeBoolForKey:kIsSuccessArchiveKey];
+    _detail = [decoder decodeObjectForKey:kDetailArchiveKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)encoder {
+  [encoder encodeInteger:self.activityType forKey:kActiveTypeArchiveKey];
+  [encoder encodeObject:self.timestamp forKey:kTimeStampArchiveKey];
+  [encoder encodeBool:self.success forKey:kIsSuccessArchiveKey];
+  [encoder encodeObject:self.detail forKey:kDetailArchiveKey];
+}
+
+- (instancetype)initWithActivityType:(FIRIAMActivityType)type
+                        isSuccessful:(BOOL)isSuccessful
+                          withDetail:(NSString *)detail
+                           timestamp:(nullable NSDate *)timestamp {
+  if (self = [super init]) {
+    _activityType = type;
+    _success = isSuccessful;
+    _detail = detail;
+    _timestamp = timestamp ? timestamp : [[NSDate alloc] init];
+  }
+  return self;
+}
+
+- (NSString *)displayStringForActivityType {
+  switch (self.activityType) {
+    case FIRIAMActivityTypeFetchMessage:
+      return @"Message Fetching";
+    case FIRIAMActivityTypeRenderMessage:
+      return @"Message Rendering";
+    case FIRIAMActivityTypeDismissMessage:
+      return @"Message Dismiss";
+    case FIRIAMActivityTypeCheckForOnOpenMessage:
+      return @"OnOpen Msg Check";
+    case FIRIAMActivityTypeCheckForAnalyticsEventMessage:
+      return @"Analytic Msg Check";
+    case FIRIAMActivityTypeCheckForFetch:
+      return @"Fetch Check";
+  }
+}
+@end
+
+@interface FIRIAMActivityLogger ()
+@property(nonatomic) BOOL isDirty;
+
+// always insert at the head of this array so that they are always in anti-chronological order
+@property(nonatomic, nonnull) NSMutableArray<FIRIAMActivityRecord *> *activityRecords;
+
+// When we see the number of log records goes beyond maxRecordCountBeforeReduce, we would trigger
+// a reduction action which would bring the array length to be the size as defined by
+// newSizeAfterReduce
+@property(nonatomic, readonly) NSInteger maxRecordCountBeforeReduce;
+@property(nonatomic, readonly) NSInteger newSizeAfterReduce;
+
+@end
+
+@implementation FIRIAMActivityLogger
+- (instancetype)initWithMaxCountBeforeReduce:(NSInteger)maxBeforeReduce
+                         withSizeAfterReduce:(NSInteger)sizeAfterReduce
+                                 verboseMode:(BOOL)verboseMode
+                               loadFromCache:(BOOL)loadFromCache {
+  if (self = [super init]) {
+    _maxRecordCountBeforeReduce = maxBeforeReduce;
+    _newSizeAfterReduce = sizeAfterReduce;
+    _activityRecords = [[NSMutableArray alloc] init];
+    _verboseMode = verboseMode;
+    _isDirty = NO;
+
+    [[NSNotificationCenter defaultCenter] addObserver:self
+                                             selector:@selector(appWillBecomeInactive)
+                                                 name:UIApplicationWillResignActiveNotification
+                                               object:nil];
+
+    if (loadFromCache) {
+      @try {
+        [self loadFromCachePath:nil];
+      } @catch (NSException *exception) {
+        FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310003",
+                      @"Non-fatal exception in loading persisted activity log records: %@.",
+                      exception);
+      }
+    }
+  }
+  return self;
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
++ (NSString *)determineCacheFilePath {
+  static NSString *logCachePath;
+  static dispatch_once_t onceToken;
+
+  dispatch_once(&onceToken, ^{
+    NSString *cacheDirPath =
+        NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
+    logCachePath = [NSString stringWithFormat:@"%@/firebase-iam-activity-log-cache", cacheDirPath];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310001",
+                @"Persistent file path for activity log data is %@", logCachePath);
+  });
+  return logCachePath;
+}
+
+- (void)loadFromCachePath:(NSString *)cacheFilePath {
+  NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath;
+
+  id fetchedActivityRecords = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
+
+  if (fetchedActivityRecords) {
+    @synchronized(self) {
+      self.activityRecords = (NSMutableArray<FIRIAMActivityRecord *> *)fetchedActivityRecords;
+      self.isDirty = NO;
+    }
+  }
+}
+
+- (BOOL)saveIntoCacheWithPath:(NSString *)cacheFilePath {
+  NSString *filePath = cacheFilePath == nil ? [self.class determineCacheFilePath] : cacheFilePath;
+  @synchronized(self) {
+    BOOL result = [NSKeyedArchiver archiveRootObject:self.activityRecords toFile:filePath];
+    if (result) {
+      self.isDirty = NO;
+    }
+    return result;
+  }
+}
+
+- (void)appWillBecomeInactive {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310004",
+              @"App will become inactive, save"
+               " activity logs");
+
+  if (self.isDirty) {
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+      if ([self saveIntoCacheWithPath:nil]) {
+        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM310002",
+                    @"Persisting activity log data is was successful");
+      } else {
+        FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM310005",
+                      @"Persisting activity log data has failed");
+      }
+    });
+  }
+}
+
+// Helper function to determine if a given activity type should be recorded under
+// non verbose type.
++ (BOOL)isMandatoryType:(FIRIAMActivityType)type {
+  switch (type) {
+    case FIRIAMActivityTypeFetchMessage:
+    case FIRIAMActivityTypeRenderMessage:
+    case FIRIAMActivityTypeDismissMessage:
+      return YES;
+    default:
+      return NO;
+  }
+}
+
+- (void)addLogRecord:(FIRIAMActivityRecord *)newRecord {
+  if (self.verboseMode || [FIRIAMActivityLogger isMandatoryType:newRecord.activityType]) {
+    @synchronized(self) {
+      [self.activityRecords insertObject:newRecord atIndex:0];
+
+      if (self.activityRecords.count >= self.maxRecordCountBeforeReduce) {
+        NSRange removeRange;
+        removeRange.location = self.newSizeAfterReduce;
+        removeRange.length = self.maxRecordCountBeforeReduce - self.newSizeAfterReduce;
+        [self.activityRecords removeObjectsInRange:removeRange];
+      }
+      self.isDirty = YES;
+    }
+  }
+}
+
+- (NSArray<FIRIAMActivityRecord *> *)readRecords {
+  return [self.activityRecords copy];
+}
+@end

+ 75 - 0
Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.h

@@ -0,0 +1,75 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMImpressionRecord : NSObject
+@property(nonatomic, readonly, copy) NSString *messageID;
+@property(nonatomic, readonly) long impressionTimeInSeconds;
+
+- (NSString *)description;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+          impressionTimeInSeconds:(long)impressionTime NS_DESIGNATED_INITIALIZER;
+@end
+
+// this protocol defines the interface for classes that can be used to track info regarding
+// display & fetch of iam messages. The info tracked here can be used to decide if it's due for
+// next display and/or fetch of iam messages.
+@protocol FIRIAMBookKeeper
+@property(nonatomic, readonly) double lastDisplayTime;
+@property(nonatomic, readonly) double lastFetchTime;
+@property(nonatomic, readonly) NSTimeInterval nextFetchWaitTime;
+
+// only call this when it's considered to be a valid impression (for example, meeting the minimum
+// display time requirement).
+- (void)recordNewImpressionForMessage:(NSString *)messageID
+          withStartTimestampInSeconds:(double)timestamp;
+
+- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount
+              withTimestampInSeconds:(double)fetchTimestamp
+                   nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime;
+
+// When we fetch the eligible message list from the sdk server, it can contain messages that are
+// already impressed for those that are defined to be displayed repeatedly (messages with custom
+// display frequency). We need then clean up the impression records for these messages so that
+// they can be displayed again on client side.
+- (void)clearImpressionsWithMessageList:(NSArray<NSString *> *)messageList;
+// fetch the impression list
+- (NSArray<FIRIAMImpressionRecord *> *)getImpressions;
+
+// For certain clients, they only need to get the list of the message ids in existing impression
+// records. This is a helper method for that.
+- (NSArray<NSString *> *)getMessageIDsFromImpressions;
+@end
+
+// implementation of FIRIAMBookKeeper protocol by storing data within iOS UserDefaults.
+// TODO: switch to something else if there is risks for the data being unintentionally deleted by
+// the app
+@interface FIRIAMBookKeeperViaUserDefaults : NSObject <FIRIAMBookKeeper>
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults NS_DESIGNATED_INITIALIZER;
+
+// for testing, don't use them for production purpose
+- (void)cleanupImpressions;
+- (void)cleanupFetchRecords;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 260 - 0
Firebase/InAppMessaging/Flows/FIRIAMBookKeeper.m

@@ -0,0 +1,260 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMBookKeeper.h"
+
+NSString *const FIRIAM_UserDefaultsKeyForImpressions = @"firebase-iam-message-impressions";
+NSString *const FIRIAM_UserDefaultsKeyForLastImpressionTimestamp =
+    @"firebase-iam-last-impression-timestamp";
+NSString *FIRIAM_UserDefaultsKeyForLastFetchTimestamp = @"firebase-iam-last-fetch-timestamp";
+
+// The two keys used to map FIRIAMImpressionRecord object to a NSDictionary object for
+// persistence.
+NSString *const FIRIAM_ImpressionDictKeyForID = @"message_id";
+NSString *const FIRIAM_ImpressionDictKeyForTimestamp = @"impression_time";
+
+static NSString *const kUserDefaultsKeyForFetchWaitTime = @"firebase-iam-fetch-wait-time";
+
+// 24 hours
+static NSTimeInterval kDefaultFetchWaitTimeInSeconds = 24 * 60 * 60;
+
+// 3 days
+static NSTimeInterval kMaxFetchWaitTimeInSeconds = 3 * 24 * 60 * 60;
+
+@interface FIRIAMBookKeeperViaUserDefaults ()
+@property(nonatomic) double lastDisplayTime;
+@property(nonatomic) double lastFetchTime;
+@property(nonatomic) double nextFetchWaitTime;
+@property(nonatomic, nonnull) NSUserDefaults *defaults;
+@end
+
+@interface FIRIAMImpressionRecord ()
+- (instancetype)initWithStorageDictionary:(NSDictionary *)dict;
+@end
+
+@implementation FIRIAMImpressionRecord
+
+- (instancetype)initWithMessageID:(NSString *)messageID
+          impressionTimeInSeconds:(long)impressionTime {
+  if (self = [super init]) {
+    _messageID = messageID;
+    _impressionTimeInSeconds = impressionTime;
+  }
+  return self;
+}
+
+- (instancetype)initWithStorageDictionary:(NSDictionary *)dict {
+  id timestamp = dict[FIRIAM_ImpressionDictKeyForTimestamp];
+  id messageID = dict[FIRIAM_ImpressionDictKeyForID];
+
+  if (![timestamp isKindOfClass:[NSNumber class]] || ![messageID isKindOfClass:[NSString class]]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270003",
+                @"Incorrect data in the dictionary object for creating a FIRIAMImpressionRecord"
+                 " object");
+    return nil;
+  } else {
+    return [self initWithMessageID:messageID
+           impressionTimeInSeconds:((NSNumber *)timestamp).longValue];
+  }
+}
+
+- (NSString *)description {
+  return [NSString stringWithFormat:@"%@ impressed at %ld in seconds", self.messageID,
+                                    self.impressionTimeInSeconds];
+}
+@end
+
+@implementation FIRIAMBookKeeperViaUserDefaults
+
+- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults {
+  if (self = [super init]) {
+    _defaults = userDefaults;
+
+    // ok if it returns 0 due to the entry being absent
+    _lastDisplayTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp];
+    _lastFetchTime = [_defaults doubleForKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];
+
+    id fetchWaitTimeEntry = [_defaults objectForKey:kUserDefaultsKeyForFetchWaitTime];
+
+    if (![fetchWaitTimeEntry isKindOfClass:NSNumber.class]) {
+      // This corresponds to the case there is no wait time entry is set in user defaults yet
+      _nextFetchWaitTime = kDefaultFetchWaitTimeInSeconds;
+    } else {
+      _nextFetchWaitTime = ((NSNumber *)fetchWaitTimeEntry).doubleValue;
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270009",
+                  @"Next fetch wait time loaded from user defaults is %lf", _nextFetchWaitTime);
+    }
+  }
+  return self;
+}
+
+// A helper function for reading and verifying the stored array data for impressions
+// in UserDefaults. It returns nil if it does not exist or fail to pass the data type
+// checking.
+- (NSArray *)fetchImpressionArrayFromStorage {
+  id impressionsData = [self.defaults objectForKey:FIRIAM_UserDefaultsKeyForImpressions];
+
+  if (impressionsData && ![impressionsData isKindOfClass:[NSArray class]]) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM270007",
+                  @"Found non-array data from impression userdefaults storage with key %@",
+                  FIRIAM_UserDefaultsKeyForImpressions);
+    return nil;
+  }
+  return (NSArray *)impressionsData;
+}
+
+- (void)recordNewImpressionForMessage:(NSString *)messageID
+          withStartTimestampInSeconds:(double)timestamp {
+  @synchronized(self) {
+    NSArray *oldImpressions = [self fetchImpressionArrayFromStorage];
+    // oldImpressions could be nil at the first time
+    NSMutableArray *newImpressions =
+        oldImpressions ? [oldImpressions mutableCopy] : [[NSMutableArray alloc] init];
+
+    // Two cases
+    //    If a prior impression exists for that messageID, update its impression timestamp
+    //    If a prior impression for that messageID does not exist, add a new entry for the
+    //    messageID.
+
+    NSDictionary *newImpressionEntry = @{
+      FIRIAM_ImpressionDictKeyForID : messageID,
+      FIRIAM_ImpressionDictKeyForTimestamp : [NSNumber numberWithDouble:timestamp]
+    };
+
+    BOOL oldImpressionRecordFound = NO;
+
+    for (int i = 0; i < newImpressions.count; i++) {
+      if ([newImpressions[i] isKindOfClass:[NSDictionary class]]) {
+        NSDictionary *currentItem = (NSDictionary *)newImpressions[i];
+        if ([messageID isEqualToString:currentItem[FIRIAM_ImpressionDictKeyForID]]) {
+          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270001",
+                      @"Updating timestamp of existing impression record to be %f for "
+                       "message %@",
+                      timestamp, messageID);
+
+          [newImpressions replaceObjectAtIndex:i withObject:newImpressionEntry];
+          oldImpressionRecordFound = YES;
+          break;
+        }
+      }
+    }
+
+    if (!oldImpressionRecordFound) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270002",
+                  @"Insert the first impression record for message %@ with timestamp in seconds "
+                   "as %f",
+                  messageID, timestamp);
+      [newImpressions addObject:newImpressionEntry];
+    }
+
+    [self.defaults setObject:newImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions];
+    [self.defaults setDouble:timestamp forKey:FIRIAM_UserDefaultsKeyForLastImpressionTimestamp];
+    self.lastDisplayTime = timestamp;
+  }
+}
+
+- (void)clearImpressionsWithMessageList:(NSArray<NSString *> *)messageList {
+  @synchronized(self) {
+    NSArray *existingImpressions = [self fetchImpressionArrayFromStorage];
+
+    NSSet<NSString *> *messageIDSet = [NSSet setWithArray:messageList];
+    NSPredicate *notInMessageListPredicate =
+        [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+          if (![evaluatedObject isKindOfClass:[NSDictionary class]]) {
+            return NO;  // unexpected item. Throw it away
+          }
+          NSDictionary *impression = (NSDictionary *)evaluatedObject;
+          return impression[FIRIAM_ImpressionDictKeyForID] &&
+                 ![messageIDSet containsObject:impression[FIRIAM_ImpressionDictKeyForID]];
+        }];
+
+    NSArray<NSDictionary *> *updatedImpressions =
+        [existingImpressions filteredArrayUsingPredicate:notInMessageListPredicate];
+
+    if (existingImpressions.count != updatedImpressions.count) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270004",
+                  @"Updating the impression records after purging %d items based on the "
+                   "server fetch response",
+                  (int)(existingImpressions.count - updatedImpressions.count));
+      [self.defaults setObject:updatedImpressions forKey:FIRIAM_UserDefaultsKeyForImpressions];
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270005",
+                  @"No impression records update due to no change after applying the server "
+                   "message list");
+    }
+  }
+}
+
+- (NSArray<FIRIAMImpressionRecord *> *)getImpressions {
+  NSArray<NSDictionary *> *impressionsFromStorage = [self fetchImpressionArrayFromStorage];
+
+  NSMutableArray<FIRIAMImpressionRecord *> *resultArray = [[NSMutableArray alloc] init];
+
+  for (NSDictionary *next in impressionsFromStorage) {
+    FIRIAMImpressionRecord *nextImpression =
+        [[FIRIAMImpressionRecord alloc] initWithStorageDictionary:next];
+    [resultArray addObject:nextImpression];
+  }
+
+  return resultArray;
+}
+
+- (NSArray<NSString *> *)getMessageIDsFromImpressions {
+  NSArray<NSDictionary *> *impressionsFromStorage = [self fetchImpressionArrayFromStorage];
+
+  NSMutableArray<NSString *> *resultArray = [[NSMutableArray alloc] init];
+
+  for (NSDictionary *next in impressionsFromStorage) {
+    [resultArray addObject:next[FIRIAM_ImpressionDictKeyForID]];
+  }
+
+  return resultArray;
+}
+
+- (void)recordNewFetchWithFetchCount:(NSInteger)fetchedMsgCount
+              withTimestampInSeconds:(double)fetchTimestamp
+                   nextFetchWaitTime:(nullable NSNumber *)nextFetchWaitTime;
+{
+  [self.defaults setDouble:fetchTimestamp forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];
+  self.lastFetchTime = fetchTimestamp;
+
+  if (nextFetchWaitTime) {
+    if (nextFetchWaitTime.doubleValue > kMaxFetchWaitTimeInSeconds) {
+      FIRLogInfo(kFIRLoggerInAppMessaging, @"I-IAM270006",
+                 @"next fetch wait time %lf is too large. Ignore it.",
+                 nextFetchWaitTime.doubleValue);
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM270008",
+                  @"Setting next fetch wait time as %lf from fetch response.",
+                  nextFetchWaitTime.doubleValue);
+      self.nextFetchWaitTime = nextFetchWaitTime.doubleValue;
+      [self.defaults setObject:nextFetchWaitTime forKey:kUserDefaultsKeyForFetchWaitTime];
+    }
+  }
+}
+
+- (void)cleanupImpressions {
+  [self.defaults setObject:@[] forKey:FIRIAM_UserDefaultsKeyForImpressions];
+}
+
+- (void)cleanupFetchRecords {
+  [self.defaults setDouble:0 forKey:FIRIAM_UserDefaultsKeyForLastFetchTimestamp];
+  self.lastFetchTime = 0;
+}
+@end

+ 39 - 0
Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.h

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+// A class for wrapping the interactions for retrieving client side info to be used in request
+// parameter for interacting with Firebase iam servers.
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMClientInfoFetcher : NSObject
+// Fetch the up-to-date Firebase instance id and token data. Since it involves a server interaction,
+// completion callback is provided for receiving the result.
+- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber
+                               withCompletion:(void (^)(NSString *_Nullable iid,
+                                                        NSString *_Nullable token,
+                                                        NSError *_Nullable error))completion;
+
+// Following are synchronous methods for fetching data
+- (nullable NSString *)getDeviceLanguageCode;
+- (nullable NSString *)getAppVersion;
+- (nullable NSString *)getOSVersion;
+- (nullable NSString *)getOSMajorVersion;
+- (nullable NSString *)getTimezone;
+- (NSString *)getIAMSDKVersion;
+@end
+NS_ASSUME_NONNULL_END

+ 120 - 0
Firebase/InAppMessaging/Flows/FIRIAMClientInfoFetcher.m

@@ -0,0 +1,120 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRLogger.h>
+#import <FirebaseInstanceID/FirebaseInstanceID.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClientInfoFetcher.h"
+
+// declaratons for FIRInstanceID SDK
+@implementation FIRIAMClientInfoFetcher
+- (void)fetchFirebaseIIDDataWithProjectNumber:(NSString *)projectNumber
+                               withCompletion:(void (^)(NSString *_Nullable iid,
+                                                        NSString *_Nullable token,
+                                                        NSError *_Nullable error))completion {
+  FIRInstanceID *iid = [FIRInstanceID instanceID];
+
+  // tokenWithAuthorizedEntity would only communicate with server on periodical cycles.
+  // For other times, it's going to fetch from local cache, so it's not causing any performance
+  // concern in the fetch flow.
+  [iid tokenWithAuthorizedEntity:projectNumber
+                           scope:@"fiam"
+                         options:nil
+                         handler:^(NSString *_Nullable token, NSError *_Nullable error) {
+                           if (error) {
+                             FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190001",
+                                           @"Error in fetching iid token: %@",
+                                           error.localizedDescription);
+                             completion(nil, nil, error);
+                           } else {
+                             FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM190002",
+                                         @"Successfully generated iid token");
+                             // now we can go ahead to fetch the id
+                             [iid getIDWithHandler:^(NSString *_Nullable identity,
+                                                     NSError *_Nullable error) {
+                               if (error) {
+                                 FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM190004",
+                                               @"Error in fetching iid value: %@",
+                                               error.localizedDescription);
+                               } else {
+                                 FIRLogDebug(
+                                     kFIRLoggerInAppMessaging, @"I-IAM190005",
+                                     @"Successfully in fetching both iid value as %@ and iid token"
+                                      " as %@",
+                                     identity, token);
+                                 completion(identity, token, nil);
+                               }
+                             }];
+                           }
+                         }];
+}
+
+- (nullable NSString *)getDeviceLanguageCode {
+  // No caching since it's requested at pretty low frequency and we get the benefit of seeing
+  // updated info the setting has changed
+  NSArray<NSString *> *preferredLanguages = [NSLocale preferredLanguages];
+  return preferredLanguages.firstObject;
+}
+
+- (nullable NSString *)getAppVersion {
+  // Since this won't change, read it once in the whole life-cycle of the app and cache its value
+  static NSString *appVersion = nil;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    appVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+  });
+  return appVersion;
+}
+
+- (nullable NSString *)getOSVersion {
+  // Since this won't change, read it once in the whole life-cycle of the app and cache its value
+  static NSString *OSVersion = nil;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    NSOperatingSystemVersion systemVersion = [NSProcessInfo processInfo].operatingSystemVersion;
+    OSVersion = [NSString stringWithFormat:@"%ld.%ld.%ld", (long)systemVersion.majorVersion,
+                                           (long)systemVersion.minorVersion,
+                                           (long)systemVersion.patchVersion];
+  });
+  return OSVersion;
+}
+
+- (nullable NSString *)getOSMajorVersion {
+  NSArray *versionItems = [[self getOSVersion] componentsSeparatedByString:@"."];
+
+  if (versionItems.count > 0) {
+    return (NSString *)versionItems[0];
+  } else {
+    return nil;
+  }
+}
+
+- (nullable NSString *)getTimezone {
+  // No caching to deal with potential changes.
+  return [NSTimeZone localTimeZone].name;
+}
+
+// extract macro value into a C string
+#define STR_FROM_MACRO(x) #x
+#define STR(x) STR_FROM_MACRO(x)
+
+- (NSString *)getIAMSDKVersion {
+  // FIRInAppMessaging_LIB_VERSION macro comes from pod definition
+  return [NSString stringWithUTF8String:STR(FIRInAppMessaging_LIB_VERSION)];
+}
+@end

+ 22 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.h

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMDisplayCheckTriggerFlow.h"
+
+// An implementation of FIRIAMDisplayCheckTriggerFlow by triggering the display check when
+// a Firebase Analytics event is fired.
+@interface FIRIAMDisplayCheckOnAnalyticEventsFlow : FIRIAMDisplayCheckTriggerFlow
+@end

+ 66 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAnalyticEventsFlow.m

@@ -0,0 +1,66 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
+#import <FirebaseAnalyticsInterop/FIRAnalyticsInteropListener.h>
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
+#import "FIRIAMDisplayExecutor.h"
+#import "FIRInAppMessagingPrivate.h"
+
+@interface FIRIAMDisplayCheckOnAnalyticEventsFlow () <FIRAnalyticsInteropListener>
+@end
+
+@implementation FIRIAMDisplayCheckOnAnalyticEventsFlow {
+  dispatch_queue_t eventListenerQueue;
+}
+
+- (void)start {
+  @synchronized(self) {
+    if (eventListenerQueue == nil) {
+      eventListenerQueue =
+          dispatch_queue_create("com.google.firebase.inappmessage.firevent_listener", NULL);
+    }
+
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140002",
+                @"Start observing Firebase Analytics events for rendering messages.");
+
+    [[FIRInAppMessaging inAppMessaging].analytics registerAnalyticsListener:self
+                                                                 withOrigin:@"fiam"];
+  }
+}
+
+- (void)messageTriggered:(NSString *)name parameters:(NSDictionary *)parameters {
+  // Dispatch to a serial queue eventListenerQueue to avoid the complications that two
+  // concurrent Firebase Analytics events triggering the
+  // checkAndDisplayNextContextualMessageForAnalyticsEvent flow concurrently.
+  dispatch_async(self->eventListenerQueue, ^{
+    [self.displayExecutor checkAndDisplayNextContextualMessageForAnalyticsEvent:name];
+  });
+}
+
+- (void)stop {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM140003",
+              @"Stop observing Firebase Analytics events for display check.");
+
+  @synchronized(self) {
+    [[FIRInAppMessaging inAppMessaging].analytics unregisterAnalyticsListenerWithOrigin:@"fiam"];
+  }
+}
+
+@end

+ 23 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.h

@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMDisplayCheckTriggerFlow.h"
+
+// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded
+@interface FIRIAMDisplayCheckOnAppForegroundFlow : FIRIAMDisplayCheckTriggerFlow
+- (void)start;
+- (void)stop;
+@end

+ 55 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnAppForegroundFlow.m

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMDisplayCheckOnAppForegroundFlow.h"
+#import "FIRIAMDisplayExecutor.h"
+
+@implementation FIRIAMDisplayCheckOnAppForegroundFlow
+
+- (void)start {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500002",
+              @"Start observing app foreground notifications for rendering messages.");
+  [[NSNotificationCenter defaultCenter] addObserver:self
+                                           selector:@selector(appWillEnterForeground:)
+                                               name:UIApplicationWillEnterForegroundNotification
+                                             object:nil];
+}
+
+- (void)appWillEnterForeground:(UIApplication *)application {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500001",
+              @"App foregrounded, wake up to check in-app messaging.");
+
+  // Show the message with 0.5 second delay so that the app's UI is more stable.
+  // When messages are displayed, the UI operation will be dispatched back to main UI thread.
+  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC),
+                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+                   [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
+                 });
+}
+
+- (void)stop {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM500004",
+              @"Stop observing app foreground notifications.");
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+@end

+ 22 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h

@@ -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 "FIRIAMDisplayCheckTriggerFlow.h"
+
+@interface FIRIAMDisplayCheckOnFetchDoneNotificationFlow : FIRIAMDisplayCheckTriggerFlow
+- (void)start;
+- (void)stop;
+@end

+ 62 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckOnFetchDoneNotificationFlow.m

@@ -0,0 +1,62 @@
+/*
+ * 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 <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+
+#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h"
+#import "FIRIAMDisplayExecutor.h"
+
+extern NSString *const kFIRIAMFetchIsDoneNotification;
+
+@implementation FIRIAMDisplayCheckOnFetchDoneNotificationFlow
+
+- (void)start {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001",
+              @"Start observing fetch done notifications for rendering messages.");
+  [[NSNotificationCenter defaultCenter] addObserver:self
+                                           selector:@selector(fetchIsDone)
+                                               name:kFIRIAMFetchIsDoneNotification
+                                             object:nil];
+}
+
+- (void)checkAndRenderMessage {
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
+  });
+}
+
+- (void)fetchIsDone {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002",
+              @"Fetch is done. Start message rendering flow.");
+
+  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 500 * (int64_t)NSEC_PER_MSEC),
+                 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+                   [self checkAndRenderMessage];
+                 });
+}
+
+- (void)stop {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003",
+              @"Stop observing fetch is done notifications.");
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+@end

+ 37 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.h

@@ -0,0 +1,37 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRIAMDisplayExecutor;
+NS_ASSUME_NONNULL_BEGIN
+
+// Parent class for modeling different flows in which we would trigger the check to see if there
+// is appropriate in-app messaging to be rendered. Notice that the flow only triggers the check
+// and whether it turns out to have any eligible message to be displayed depending on if certain
+// conditions are met
+@interface FIRIAMDisplayCheckTriggerFlow : NSObject
+
+// Accessed by subclasses, not intended by other clients
+@property(nonatomic, nonnull, readonly) FIRIAMDisplayExecutor *displayExecutor;
+- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor;
+
+// subclasses should implement the follow two methods to start/stop their concrete
+// display check flow
+- (void)start;
+- (void)stop;
+@end
+NS_ASSUME_NONNULL_END

+ 32 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayCheckTriggerFlow.m

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMDisplayCheckTriggerFlow.h"
+
+@implementation FIRIAMDisplayCheckTriggerFlow
+- (instancetype)initWithDisplayFlow:(FIRIAMDisplayExecutor *)displayExecutor {
+  if (self = [super init]) {
+    _displayExecutor = displayExecutor;
+  }
+  return self;
+}
+
+// Providing fake implementations to avoid xcode complain about incomplete implementation.
+- (void)start {
+}
+- (void)stop {
+}
+@end

+ 61 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.h

@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRIAMActionURLFollower.h"
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMClearcutLogger.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMTimeFetcher.h"
+#import "FIRInAppMessagingRendering.h"
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMDisplaySetting : NSObject
+@property(nonatomic) NSTimeInterval displayMinIntervalInMinutes;
+@end
+
+// The class for checking if there are appropriate messages to be displayed and if so, render it.
+// There are other flows that would determine the timing for the checking and then use this class
+// instance for the actual check/display.
+//
+// In addition to fetch eligible message from message cache, this class also ensures certain
+// conditions are satisfied for the rendering
+//   1 No current in-app message is being displayed
+//   2 For non-contextual messages, the display interval in display setting is met.
+@interface FIRIAMDisplayExecutor : NSObject
+
+- (instancetype)initWithSetting:(FIRIAMDisplaySetting *)setting
+                   messageCache:(FIRIAMMessageClientCache *)cache
+                    timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     bookKeeper:(id<FIRIAMBookKeeper>)displayBookKeeper
+              actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower
+                 activityLogger:(FIRIAMActivityLogger *)activityLogger
+           analyticsEventLogger:(id<FIRIAMAnalyticsEventLogger>)analyticsEventLogger;
+
+// Check and display next in-app message eligible for app open trigger
+- (void)checkAndDisplayNextAppForegroundMessage;
+// Check and display next in-app message eligible for analytics event trigger with given event name.
+- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName;
+
+// a boolean flag that can be used to suppress/resume displaying messages.
+@property(nonatomic) BOOL suppressMessageDisplay;
+
+// This is the display component used by display executor for actual message rendering.
+@property(nonatomic) id<FIRInAppMessagingDisplay> messageDisplayComponent;
+@end
+NS_ASSUME_NONNULL_END

+ 497 - 0
Firebase/InAppMessaging/Flows/FIRIAMDisplayExecutor.m

@@ -0,0 +1,497 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMDisplayExecutor.h"
+#import "FIRIAMMessageContentData.h"
+#import "FIRIAMMessageDefinition.h"
+#import "FIRIAMSDKRuntimeErrorCodes.h"
+
+@implementation FIRIAMDisplaySetting
+@end
+
+@interface FIRIAMDisplayExecutor () <FIRInAppMessagingDisplayDelegate>
+@property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+
+// YES if a message is being rendered at this time
+@property(nonatomic) BOOL isMsgBeingDisplayed;
+@property(nonatomic) NSTimeInterval lastDisplayTime;
+@property(nonatomic, nonnull, readonly) FIRIAMDisplaySetting *setting;
+@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache;
+@property(nonatomic, nonnull, readonly) id<FIRIAMBookKeeper> displayBookKeeper;
+@property(nonatomic) BOOL impressionRecorded;
+@property(nonatomic, nonnull, readonly) id<FIRIAMAnalyticsEventLogger> analyticsEventLogger;
+@property(nonatomic, nonnull, readonly) FIRIAMActionURLFollower *actionURLFollower;
+@end
+
+@implementation FIRIAMDisplayExecutor {
+  FIRIAMMessageDefinition *_currentMsgBeingDisplayed;
+}
+
+#pragma mark - FIRInAppMessagingDisplayDelegate methods
+- (void)messageClicked {
+  self.isMsgBeingDisplayed = NO;
+  if (!_currentMsgBeingDisplayed.renderData.messageID) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400030",
+                  @"messageClicked called but "
+                   "there is no current message ID.");
+    return;
+  }
+
+  if (_currentMsgBeingDisplayed.isTestMessage) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400031",
+                @"A test message clicked. Do test event impression/click analytics logging");
+
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
+                   forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
+                withCampaignName:_currentMsgBeingDisplayed.renderData.name
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400036",
+                                    @"Logging analytics event for url following %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageClick
+                   forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
+                withCampaignName:_currentMsgBeingDisplayed.renderData.name
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400039",
+                                    @"Logging analytics event for url following %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+  } else {
+    // Logging the impression
+    [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
+                withMessageName:_currentMsgBeingDisplayed.renderData.name];
+
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventActionURLFollow
+                   forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
+                withCampaignName:_currentMsgBeingDisplayed.renderData.name
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400032",
+                                    @"Logging analytics event for url following %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+  }
+
+  NSURL *actionURL = _currentMsgBeingDisplayed.renderData.contentData.actionURL;
+
+  if (!actionURL) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400033",
+                @"messageClicked called but "
+                 "there is no action url specified in the message data.");
+    // it's equivalent to closing the message with no further action
+    return;
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400037", @"Following action url %@",
+                actionURL.absoluteString);
+    @try {
+      [self.actionURLFollower
+              followActionURL:actionURL
+          withCompletionBlock:^(BOOL success) {
+            FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400034",
+                        @"Seeing %@ from following action URL", success ? @"success" : @"error");
+          }];
+    } @catch (NSException *e) {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400035",
+                    @"Exception encountered in following "
+                     "action url (%@): %@ ",
+                    actionURL, e.description);
+      @throw;
+    }
+  }
+}
+
+- (void)messageDismissedWithType:(FIRInAppMessagingDismissType)dismissType {
+  self.isMsgBeingDisplayed = NO;
+  if (!_currentMsgBeingDisplayed.renderData.messageID) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400014",
+                  @"messageDismissedWithType called but "
+                   "there is no current message ID.");
+    return;
+  }
+
+  if (_currentMsgBeingDisplayed.isTestMessage) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400020",
+                @"A test message dismissed. Record the impression event.");
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventTestMessageImpression
+                   forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
+                withCampaignName:_currentMsgBeingDisplayed.renderData.name
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400038",
+                                    @"Logging analytics event for url following %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+
+    return;
+  }
+
+  // Logging the impression
+  [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
+              withMessageName:_currentMsgBeingDisplayed.renderData.name];
+
+  FIRIAMAnalyticsLogEventType logEventType = dismissType == FIRInAppMessagingDismissTypeAuto
+                                                 ? FIRIAMAnalyticsEventMessageDismissAuto
+                                                 : FIRIAMAnalyticsEventMessageDismissClick;
+
+  [self.analyticsEventLogger
+      logAnalyticsEventForType:logEventType
+                 forCampaignID:_currentMsgBeingDisplayed.renderData.messageID
+              withCampaignName:_currentMsgBeingDisplayed.renderData.name
+                 eventTimeInMs:nil
+                    completion:^(BOOL success) {
+                      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400004",
+                                  @"Logging analytics event for message dismiss %@",
+                                  success ? @"succeeded" : @"failed");
+                    }];
+}
+
+- (void)impressionDetected {
+  if (!_currentMsgBeingDisplayed.renderData.messageID) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400022",
+                  @"impressionDetected called but "
+                   "there is no current message ID.");
+    return;
+  }
+
+  if (!_currentMsgBeingDisplayed.isTestMessage) {
+    // Displayed long enough to be a valid impression.
+    [self recordValidImpression:_currentMsgBeingDisplayed.renderData.messageID
+                withMessageName:_currentMsgBeingDisplayed.renderData.name];
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400011",
+                @"A test message. Record the test message impression event.");
+    return;
+  }
+}
+
+- (void)displayErrorEncountered:(NSError *)error {
+  self.isMsgBeingDisplayed = NO;
+
+  if (!_currentMsgBeingDisplayed.renderData.messageID) {
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM400017",
+                  @"displayErrorEncountered called but "
+                   "there is no current message ID.");
+    return;
+  }
+
+  NSString *messageID = _currentMsgBeingDisplayed.renderData.messageID;
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400009",
+              @"Display ran into error for message %@: %@", messageID, error);
+
+  if (_currentMsgBeingDisplayed.isTestMessage) {
+    [self displayMessageLoadError:error];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400012",
+                @"A test message. No analytics tracking "
+                 "from image data loading failure");
+    return;
+  }
+
+  // we remove the message from the client side cache so that it won't be retried until next time
+  // it's fetched again from server.
+  [self.messageCache removeMessageWithId:messageID];
+  NSString *messageName = _currentMsgBeingDisplayed.renderData.name;
+
+  if ([error.domain isEqualToString:NSURLErrorDomain]) {
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventImageFetchError
+                   forCampaignID:messageID
+                withCampaignName:messageName
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400010",
+                                    @"Logging analytics event for image fetch error %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+  } else if (error.code == FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL) {
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventImageFormatUnsupported
+                   forCampaignID:messageID
+                withCampaignName:messageName
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400013",
+                                    @"Logging analytics event for image format error %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+  }
+}
+
+- (void)recordValidImpression:(NSString *)messageID withMessageName:(NSString *)messageName {
+  if (!self.impressionRecorded) {
+    [self.displayBookKeeper recordNewImpressionForMessage:messageID
+                              withStartTimestampInSeconds:self.lastDisplayTime];
+    self.impressionRecorded = YES;
+    [self.messageCache removeMessageWithId:messageID];
+    // Log an impression analytics event as well.
+    [self.analyticsEventLogger
+        logAnalyticsEventForType:FIRIAMAnalyticsEventMessageImpression
+                   forCampaignID:messageID
+                withCampaignName:messageName
+                   eventTimeInMs:nil
+                      completion:^(BOOL success) {
+                        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400007",
+                                    @"Logging analytics event for impression %@",
+                                    success ? @"succeeded" : @"failed");
+                      }];
+  }
+}
+
+- (void)displayMessageLoadError:(NSError *)error {
+  NSString *errorMsg = error.userInfo[NSLocalizedDescriptionKey]
+                           ? error.userInfo[NSLocalizedDescriptionKey]
+                           : @"Message loading failed";
+  UIAlertController *alert = [UIAlertController
+      alertControllerWithTitle:@"Firebase InAppMessaging fail to load a test message"
+                       message:errorMsg
+                preferredStyle:UIAlertControllerStyleAlert];
+
+  UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK"
+                                                          style:UIAlertActionStyleDefault
+                                                        handler:^(UIAlertAction *action){
+                                                        }];
+
+  [alert addAction:defaultAction];
+
+  [[UIApplication sharedApplication].keyWindow.rootViewController presentViewController:alert
+                                                                               animated:YES
+                                                                             completion:nil];
+}
+
+- (instancetype)initWithSetting:(FIRIAMDisplaySetting *)setting
+                   messageCache:(FIRIAMMessageClientCache *)cache
+                    timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     bookKeeper:(id<FIRIAMBookKeeper>)displayBookKeeper
+              actionURLFollower:(FIRIAMActionURLFollower *)actionURLFollower
+                 activityLogger:(FIRIAMActivityLogger *)activityLogger
+           analyticsEventLogger:(id<FIRIAMAnalyticsEventLogger>)analyticsEventLogger {
+  if (self = [super init]) {
+    _timeFetcher = timeFetcher;
+    _lastDisplayTime = displayBookKeeper.lastDisplayTime;
+    _setting = setting;
+    _messageCache = cache;
+    _displayBookKeeper = displayBookKeeper;
+    _isMsgBeingDisplayed = NO;
+    _analyticsEventLogger = analyticsEventLogger;
+    _actionURLFollower = actionURLFollower;
+    _suppressMessageDisplay = NO;  // always allow message display on startup
+  }
+  return self;
+}
+
+- (void)checkAndDisplayNextContextualMessageForAnalyticsEvent:(NSString *)eventName {
+  // synchronizing on self so that we won't potentially enter the render flow from two
+  // threads: example like showing analytics triggered message and a regular app open
+  // triggered message
+  @synchronized(self) {
+    if (self.suppressMessageDisplay) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400015",
+                  @"Message display is being suppressed. No contextual message rendering.");
+      return;
+    }
+
+    if (!self.messageDisplayComponent) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400026",
+                  @"Message display component is not present yet. No display should happen.");
+      return;
+    }
+
+    if (self.isMsgBeingDisplayed) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400008",
+                  @"An in-app message display is in progress, do not check analytics event "
+                   "based message for now.");
+
+      return;
+    }
+
+    // Pop up next analytics event based message to be displayed
+    FIRIAMMessageDefinition *nextAnalyticsBasedMessage =
+        [self.messageCache nextOnFirebaseAnalyticEventDisplayMsg:eventName];
+
+    if (nextAnalyticsBasedMessage) {
+      [self displayForMessage:nextAnalyticsBasedMessage];
+    }
+  }
+}
+
+- (FIRInAppMessagingBannerDisplay *)
+    bannerMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
+                             imageData:(FIRInAppMessagingImageData *)imageData {
+  NSString *title = definition.renderData.contentData.titleText;
+  NSString *body = definition.renderData.contentData.bodyText;
+
+  FIRInAppMessagingBannerDisplay *bannerMessage = [[FIRInAppMessagingBannerDisplay alloc]
+        initWithMessageID:definition.renderData.messageID
+      renderAsTestMessage:definition.isTestMessage
+                titleText:title
+                 bodyText:body
+                textColor:definition.renderData.renderingEffectSettings.textColor
+          backgroundColor:definition.renderData.renderingEffectSettings.displayBGColor
+                imageData:imageData];
+
+  return bannerMessage;
+}
+
+- (FIRInAppMessagingImageOnlyDisplay *)
+    imageOnlyMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
+                                imageData:(FIRInAppMessagingImageData *)imageData {
+  FIRInAppMessagingImageOnlyDisplay *imageOnlyMessage =
+      [[FIRInAppMessagingImageOnlyDisplay alloc] initWithMessageID:definition.renderData.messageID
+                                               renderAsTestMessage:definition.isTestMessage
+                                                         imageData:imageData];
+
+  return imageOnlyMessage;
+}
+
+- (FIRInAppMessagingModalDisplay *)
+    modalViewMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
+                                imageData:(FIRInAppMessagingImageData *)imageData {
+  // For easier reference in this method.
+  FIRIAMMessageRenderData *renderData = definition.renderData;
+
+  NSString *title = renderData.contentData.titleText;
+  NSString *body = renderData.contentData.bodyText;
+
+  FIRInAppMessagingActionButton *actionButton = nil;
+
+  if (definition.renderData.contentData.actionButtonText) {
+    actionButton = [[FIRInAppMessagingActionButton alloc]
+        initWithButtonText:renderData.contentData.actionButtonText
+           buttonTextColor:renderData.renderingEffectSettings.btnTextColor
+           backgroundColor:renderData.renderingEffectSettings.btnBGColor];
+  }
+
+  FIRInAppMessagingModalDisplay *modalViewMessage = [[FIRInAppMessagingModalDisplay alloc]
+        initWithMessageID:definition.renderData.messageID
+      renderAsTestMessage:definition.isTestMessage
+                titleText:title
+                 bodyText:body
+                textColor:renderData.renderingEffectSettings.textColor
+          backgroundColor:renderData.renderingEffectSettings.displayBGColor
+                imageData:imageData
+             actionButton:actionButton];
+
+  return modalViewMessage;
+}
+
+- (FIRInAppMessagingDisplayMessageBase *)
+    displayMessageWithMessageDefinition:(FIRIAMMessageDefinition *)definition
+                              imageData:(FIRInAppMessagingImageData *)imageData {
+  switch (definition.renderData.renderingEffectSettings.viewMode) {
+    case FIRIAMRenderAsBannerView:
+      return [self bannerMessageWithMessageDefinition:definition imageData:imageData];
+    case FIRIAMRenderAsModalView:
+      return [self modalViewMessageWithMessageDefinition:definition imageData:imageData];
+    case FIRIAMRenderAsImageOnlyView:
+      return [self imageOnlyMessageWithMessageDefinition:definition imageData:imageData];
+    default:
+      return nil;
+  }
+}
+
+- (void)displayForMessage:(FIRIAMMessageDefinition *)message {
+  _currentMsgBeingDisplayed = message;
+  [message.renderData.contentData
+      loadImageDataWithBlock:^(NSData *_Nullable imageNSData, NSError *error) {
+        FIRInAppMessagingImageData *imageData = nil;
+
+        if (error) {
+          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400019",
+                      @"Error in loading image data for the message.");
+
+          // short-circuit to display error handling
+          [self displayErrorEncountered:error];
+          return;
+        } else if (imageNSData != nil) {
+          imageData = [[FIRInAppMessagingImageData alloc]
+              initWithImageURL:message.renderData.contentData.imageURL.absoluteString
+                     imageData:imageNSData];
+        }
+
+        self.impressionRecorded = NO;
+        self.isMsgBeingDisplayed = YES;
+
+        FIRInAppMessagingDisplayMessageBase *displayMessage =
+            [self displayMessageWithMessageDefinition:message imageData:imageData];
+        [self.messageDisplayComponent displayMessage:displayMessage displayDelegate:self];
+      }];
+}
+
+- (BOOL)enoughIntervalFromLastDisplay {
+  NSTimeInterval intervalFromLastDisplayInSeconds =
+      [self.timeFetcher currentTimestampInSeconds] - self.lastDisplayTime;
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400005",
+              @"Interval time from last display is %lf seconds", intervalFromLastDisplayInSeconds);
+
+  return intervalFromLastDisplayInSeconds >= self.setting.displayMinIntervalInMinutes * 60.0;
+}
+
+- (void)checkAndDisplayNextAppForegroundMessage {
+  // synchronizing on self so that we won't potentially enter the render flow from two
+  // threads: example like showing analytics triggered message and a regular app open
+  // triggered message concurrently
+  @synchronized(self) {
+    if (!self.messageDisplayComponent) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400027",
+                  @"Message display component is not present yet. No display should happen.");
+      return;
+    }
+
+    if (self.suppressMessageDisplay) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400016",
+                  @"Message display is being suppressed. No regular message rendering.");
+      return;
+    }
+
+    if (self.isMsgBeingDisplayed) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400002",
+                  @"An in-app message display is in progress, do not over-display on top of it.");
+      return;
+    }
+
+    if ([self.messageCache hasTestMessage] || [self enoughIntervalFromLastDisplay]) {
+      // We can display test messages anytime or display regular messages when
+      // the display time interval has been reached
+      FIRIAMMessageDefinition *nextForegroundMessage = [self.messageCache nextOnAppOpenDisplayMsg];
+
+      if (nextForegroundMessage) {
+        [self displayForMessage:nextForegroundMessage];
+        self.lastDisplayTime = [self.timeFetcher currentTimestampInSeconds];
+      } else {
+        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400001",
+                    @"No appropriate in-app message detected for display.");
+      }
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM400003",
+                  @"Minimal display interval of %lf seconds has not been reached yet.",
+                  self.setting.displayMinIntervalInMinutes * 60.0);
+    }
+  }
+}
+@end

+ 59 - 0
Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.h

@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMSDKModeManager.h"
+#import "FIRIAMTimeFetcher.h"
+
+@protocol FIRIAMAnalyticsEventLogger;
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMFetchSetting : NSObject
+@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes;
+@end
+
+typedef void (^FIRIAMFetchMessageCompletionHandler)(
+    NSArray<FIRIAMMessageDefinition *> *_Nullable messages,
+    NSNumber *_Nullable nextFetchWaitTime,
+    NSInteger discardedMessageCount,
+    NSError *_Nullable error);
+
+@protocol FIRIAMMessageFetcher
+- (void)fetchMessagesWithImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressonList
+                         withCompletion:(FIRIAMFetchMessageCompletionHandler)completion;
+@end
+
+// Parent class for supporting different fetching flows. Subclass is supposed to trigger
+// checkAndFetch at appropriate moments based on its fetch strategy
+@interface FIRIAMFetchFlow : NSObject
+- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting
+                   messageCache:(FIRIAMMessageClientCache *)cache
+                 messageFetcher:(id<FIRIAMMessageFetcher>)messageFetcher
+                    timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     bookKeeper:(id<FIRIAMBookKeeper>)displayBookKeeper
+                 activityLogger:(FIRIAMActivityLogger *)activityLogger
+           analyticsEventLogger:(id<FIRIAMAnalyticsEventLogger>)analyticsEventLogger
+           FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager;
+
+// Triggers a potential fetch of in-app messaging from the source. It would check and respect the
+// the fetchMinIntervalInMinutes defined in setting
+- (void)checkAndFetch;
+@end
+NS_ASSUME_NONNULL_END

+ 253 - 0
Firebase/InAppMessaging/Flows/FIRIAMFetchFlow.m

@@ -0,0 +1,253 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutLogger.h"
+#import "FIRIAMFetchFlow.h"
+
+@implementation FIRIAMFetchSetting
+@end
+
+// the notification message to say that the fetch flow is done
+NSString *const kFIRIAMFetchIsDoneNotification = @"FIRIAMFetchIsDoneNotification";
+
+@interface FIRIAMFetchFlow ()
+@property(nonatomic) id<FIRIAMTimeFetcher> timeFetcher;
+@property(nonatomic) NSTimeInterval lastFetchTime;
+@property(nonatomic, nonnull, readonly) FIRIAMFetchSetting *setting;
+@property(nonatomic, nonnull, readonly) FIRIAMMessageClientCache *messageCache;
+@property(nonatomic) id<FIRIAMMessageFetcher> messageFetcher;
+@property(nonatomic, nonnull, readonly) id<FIRIAMBookKeeper> fetchBookKeeper;
+@property(nonatomic, nonnull, readonly) FIRIAMActivityLogger *activityLogger;
+@property(nonatomic, nonnull, readonly) id<FIRIAMAnalyticsEventLogger> analyticsEventLogger;
+
+@property(nonatomic, nonnull, readonly) FIRIAMSDKModeManager *sdkModeManager;
+@end
+
+@implementation FIRIAMFetchFlow
+- (instancetype)initWithSetting:(FIRIAMFetchSetting *)setting
+                   messageCache:(FIRIAMMessageClientCache *)cache
+                 messageFetcher:(id<FIRIAMMessageFetcher>)messageFetcher
+                    timeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher
+                     bookKeeper:(id<FIRIAMBookKeeper>)fetchBookKeeper
+                 activityLogger:(FIRIAMActivityLogger *)activityLogger
+           analyticsEventLogger:(id<FIRIAMAnalyticsEventLogger>)analyticsEventLogger
+           FIRIAMSDKModeManager:(FIRIAMSDKModeManager *)sdkModeManager {
+  if (self = [super init]) {
+    _timeFetcher = timeFetcher;
+    _lastFetchTime = [fetchBookKeeper lastFetchTime];
+    _setting = setting;
+    _messageCache = cache;
+    _messageFetcher = messageFetcher;
+    _fetchBookKeeper = fetchBookKeeper;
+    _activityLogger = activityLogger;
+    _analyticsEventLogger = analyticsEventLogger;
+    _sdkModeManager = sdkModeManager;
+  }
+  return self;
+}
+
+- (FIRIAMAnalyticsLogEventType)fetchErrorToLogEventType:(NSError *)error {
+  if ([error.domain isEqual:NSURLErrorDomain]) {
+    if (error.code == NSURLErrorNotConnectedToInternet) {
+      return FIRIAMAnalyticsEventFetchAPINetworkError;
+    } else {
+      // error.code could be a non 2xx status code
+      if (error.code > 0) {
+        if (error.code >= 400 && error.code < 500) {
+          return FIRIAMAnalyticsEventFetchAPIClientError;
+        } else {
+          if (error.code >= 500 && error.code < 600) {
+            return FIRIAMAnalyticsEventFetchAPIServerError;
+          }
+        }
+      }
+    }
+  }
+
+  return FIRIAMAnalyticsLogEventUnknown;
+}
+
+- (void)sendFetchIsDoneNotification {
+  [[NSNotificationCenter defaultCenter] postNotificationName:kFIRIAMFetchIsDoneNotification
+                                                      object:self];
+}
+
+- (void)handleSuccessullyFetchedMessages:(NSArray<FIRIAMMessageDefinition *> *)messagesInResponse
+                       withFetchWaitTime:(NSNumber *_Nullable)fetchWaitTime
+                      requestImpressions:(NSArray<FIRIAMImpressionRecord *> *)requestImpressions {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700004", @"%lu messages were fetched successfully.",
+              (unsigned long)messagesInResponse.count);
+
+  for (FIRIAMMessageDefinition *next in messagesInResponse) {
+    if (next.isTestMessage && self.sdkModeManager.currentMode != FIRIAMSDKModeTesting) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700006",
+                  @"Seeing test message in fetch response. Turn "
+                   "the current instance into a testing instance.");
+      [self.sdkModeManager becomeTestingInstance];
+    }
+  }
+
+  NSArray<NSString *> *responseMessageIDs =
+      [messagesInResponse valueForKeyPath:@"renderData.messageID"];
+  NSArray<NSString *> *impressionMessageIDs = [requestImpressions valueForKey:@"messageID"];
+
+  // We are going to clear impression records for those IDs that are in both impressionMessageIDs
+  // and responseMessageIDs. This is to avoid incorrectly clearing impressions records that come
+  // in between the sending the request and receiving the response for the fetch operation.
+  // So we are computing intersection between responseMessageIDs and impressionMessageIDs and use
+  // that for impression log clearing.
+  NSMutableSet *idIntersection = [NSMutableSet setWithArray:responseMessageIDs];
+  [idIntersection intersectSet:[NSSet setWithArray:impressionMessageIDs]];
+
+  [self.fetchBookKeeper clearImpressionsWithMessageList:[idIntersection allObjects]];
+  [self.messageCache setMessageData:messagesInResponse];
+
+  [self.sdkModeManager registerOneMoreFetch];
+  [self.fetchBookKeeper recordNewFetchWithFetchCount:messagesInResponse.count
+                              withTimestampInSeconds:[self.timeFetcher currentTimestampInSeconds]
+                                   nextFetchWaitTime:fetchWaitTime];
+}
+
+- (void)checkAndFetch {
+  NSTimeInterval intervalFromLastFetchInSeconds =
+      [self.timeFetcher currentTimestampInSeconds] - self.fetchBookKeeper.lastFetchTime;
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700005",
+              @"Interval from last time fetch is %lf seconds", intervalFromLastFetchInSeconds);
+
+  BOOL fetchIsAllowedNow = NO;
+
+  if (intervalFromLastFetchInSeconds >= self.fetchBookKeeper.nextFetchWaitTime) {
+    // it's enough wait time interval from last fetch.
+    fetchIsAllowedNow = YES;
+  } else {
+    FIRIAMSDKMode sdkMode = [self.sdkModeManager currentMode];
+    if (sdkMode == FIRIAMSDKModeNewlyInstalled || sdkMode == FIRIAMSDKModeTesting) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700007",
+                  @"OK to fetch due to current SDK mode being %@",
+                  FIRIAMDescriptonStringForSDKMode(sdkMode));
+      fetchIsAllowedNow = YES;
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700008",
+                  @"Interval from last time fetch is %lf seconds, smaller than fetch wait time %lf",
+                  intervalFromLastFetchInSeconds, self.fetchBookKeeper.nextFetchWaitTime);
+    }
+  }
+
+  if (fetchIsAllowedNow) {
+    // we are allowed to fetch in-app message from time interval wise
+
+    FIRIAMActivityRecord *record =
+        [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch
+                                              isSuccessful:YES
+                                                withDetail:@"OK to do a fetch"
+                                                 timestamp:nil];
+    [self.activityLogger addLogRecord:record];
+
+    NSArray<FIRIAMImpressionRecord *> *impressions = [self.fetchBookKeeper getImpressions];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700001", @"Go ahead to fetch messages");
+
+    NSTimeInterval fetchStartTime = [[NSDate date] timeIntervalSince1970];
+
+    [self.messageFetcher
+        fetchMessagesWithImpressionList:impressions
+                         withCompletion:^(NSArray<FIRIAMMessageDefinition *> *_Nullable messages,
+                                          NSNumber *_Nullable nextFetchWaitTime,
+                                          NSInteger discardedMessageCount,
+                                          NSError *_Nullable error) {
+                           if (error) {
+                             FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM700002",
+                                           @"Error happened during message fetching %@", error);
+
+                             FIRIAMAnalyticsLogEventType eventType =
+                                 [self fetchErrorToLogEventType:error];
+
+                             [self.analyticsEventLogger logAnalyticsEventForType:eventType
+                                                                   forCampaignID:@"all"
+                                                                withCampaignName:@"all"
+                                                                   eventTimeInMs:nil
+                                                                      completion:^(BOOL success){
+                                                                          // nothing to do
+                                                                      }];
+
+                             FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc]
+                                 initWithActivityType:FIRIAMActivityTypeFetchMessage
+                                         isSuccessful:NO
+                                           withDetail:error.description
+                                            timestamp:nil];
+                             [self.activityLogger addLogRecord:record];
+                           } else {
+                             double fetchOperationLatencyInMills =
+                                 ([[NSDate date] timeIntervalSince1970] - fetchStartTime) * 1000;
+                             NSString *impressionListString =
+                                 [impressions componentsJoinedByString:@","];
+                             NSString *activityLogDetail = @"";
+
+                             if (discardedMessageCount > 0) {
+                               activityLogDetail = [NSString
+                                   stringWithFormat:
+                                       @"%lu messages fetched with impression list as [%@]"
+                                        " and %lu messages are discarded due to data being "
+                                        "invalid. It took"
+                                        " %lf milliseconds",
+                                       (unsigned long)messages.count, impressionListString,
+                                       (unsigned long)discardedMessageCount,
+                                       fetchOperationLatencyInMills];
+                             } else {
+                               activityLogDetail = [NSString
+                                   stringWithFormat:
+                                       @"%lu messages fetched with impression list as [%@]. It took"
+                                        " %lf milliseconds",
+                                       (unsigned long)messages.count, impressionListString,
+                                       fetchOperationLatencyInMills];
+                             }
+
+                             FIRIAMActivityRecord *record = [[FIRIAMActivityRecord alloc]
+                                 initWithActivityType:FIRIAMActivityTypeFetchMessage
+                                         isSuccessful:YES
+                                           withDetail:activityLogDetail
+                                            timestamp:nil];
+                             [self.activityLogger addLogRecord:record];
+
+                             // Now handle the fetched messages.
+                             [self handleSuccessullyFetchedMessages:messages
+                                                  withFetchWaitTime:nextFetchWaitTime
+                                                 requestImpressions:impressions];
+                           }
+                           // Send this regardless whether fetch is successful or not.
+                           [self sendFetchIsDoneNotification];
+                         }];
+
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM700003",
+                @"Only %lf seconds from last fetch time. No action.",
+                intervalFromLastFetchInSeconds);
+    // for no fetch case, we still send out the notification so that and display flow can continue
+    // from here.
+    [self sendFetchIsDoneNotification];
+    FIRIAMActivityRecord *record =
+        [[FIRIAMActivityRecord alloc] initWithActivityType:FIRIAMActivityTypeCheckForFetch
+                                              isSuccessful:NO
+                                                withDetail:@"Abort due to check time interval "
+                                                            "not reached yet"
+                                                 timestamp:nil];
+    [self.activityLogger addLogRecord:record];
+  }
+}
+@end

+ 22 - 0
Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.h

@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import "FIRIAMFetchFlow.h"
+
+// an implementation of FIRIAMDisplayExecutor by triggering the display when app is foregrounded
+@interface FIRIAMFetchOnAppForegroundFlow : FIRIAMFetchFlow
+- (void)start;
+- (void)stop;
+@end

+ 51 - 0
Firebase/InAppMessaging/Flows/FIRIAMFetchOnAppForegroundFlow.m

@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMFetchOnAppForegroundFlow.h"
+@implementation FIRIAMFetchOnAppForegroundFlow
+- (void)start {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600002",
+              @"Start observing app foreground notifications for message fetching.");
+  [[NSNotificationCenter defaultCenter] addObserver:self
+                                           selector:@selector(appWillEnterForeground:)
+                                               name:UIApplicationWillEnterForegroundNotification
+                                             object:nil];
+}
+
+- (void)appWillEnterForeground:(UIApplication *)application {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600001",
+              @"App foregrounded, wake up to see if we can fetch in-app messaging.");
+  // for fetch operation, dispatch it to non main UI thread to avoid blocking. It's ok to dispatch
+  // to a concurrent global queue instead of serial queue since app open event won't happen at
+  // fast speed to cause race conditions
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    [self checkAndFetch];
+  });
+}
+
+- (void)stop {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM600003",
+              @"Stop observing app foreground notifications.");
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)dealloc {
+  [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+@end

+ 91 - 0
Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.h

@@ -0,0 +1,91 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMFetchResponseParser.h"
+#import "FIRIAMMessageDefinition.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@class FIRIAMServerMsgFetchStorage;
+@class FIRIAMDisplayCheckOnAnalyticEventsFlow;
+
+@interface FIRIAMContextualTrigger
+@property(nonatomic, copy, readonly) NSString *eventName;
+@end
+
+@interface FIRIAMContextualTriggerListener
++ (void)listenForTriggers:(NSArray<FIRIAMContextualTrigger *> *)triggers
+             withCallback:(void (^)(FIRIAMContextualTrigger *matchedTrigger))callback;
+@end
+
+@protocol FIRIAMCacheDataObserver
+- (void)dataChanged;
+@end
+
+// This class serves as an in-memory cache of the messages that would be searched for finding next
+// message to be rendered. Its content can be loaded from client persistent storage upon SDK
+// initialization and then updated whenever a new fetch is made to server to receive the last
+// list. In the case a message has been rendered, it's removed from the cache so that it's not
+// considered next time for the message search.
+//
+// This class is also responsible for setting up and tearing down appropriate analytics event
+// listening flow based on whether the current active event list contains any analytics event
+// trigger based messages.
+//
+// This class exists so that we can do message match more efficiently (in-memory search vs search
+// in local persistent storage) by using appropriate in-memory data structure.
+@interface FIRIAMMessageClientCache : NSObject
+
+// used to inform the analytics event display check flow about whether it should start/stop
+// analytics event listening based on the latest message definitions
+// make it weak to avoid retaining cycle
+@property(nonatomic, weak, nullable)
+    FIRIAMDisplayCheckOnAnalyticEventsFlow *analycisEventDislayCheckFlow;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithBookkeeper:(id<FIRIAMBookKeeper>)bookKeeper
+               usingResponseParser:(FIRIAMFetchResponseParser *)responseParser;
+
+// set an observer for watching for data changes in the cache
+- (void)setDataObserver:(id<FIRIAMCacheDataObserver>)observer;
+
+// Returns YES if there are any test messages in the cache.
+- (BOOL)hasTestMessage;
+
+// read all the messages as a copy stored in cache
+- (NSArray<FIRIAMMessageDefinition *> *)allRegularMessages;
+
+// clients that are to display messages should use nextOnAppOpenDisplayMsg or
+// nextOnFirebaseAnalyticEventDisplayMsg to fetch the next eligible message and use
+// removeMessageWithId to remove it from cache once the message has been correctly rendered
+
+// Fetch next eligible messages that are appropriate for display at app open time
+- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg;
+// Fetch next eligible message that matches the event triggering condition
+- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName;
+
+// Call this after a message has been rendered to remove it from the cache.
+- (void)removeMessageWithId:(NSString *)messgeId;
+
+// reset messages data
+- (void)setMessageData:(NSArray<FIRIAMMessageDefinition *> *)messages;
+// load messages from persistent storage
+- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
+                               withCompletion:(void (^)(BOOL success))completion;
+@end
+NS_ASSUME_NONNULL_END

+ 223 - 0
Firebase/InAppMessaging/Flows/FIRIAMMessageClientCache.m

@@ -0,0 +1,223 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
+#import "FIRIAMDisplayTriggerDefinition.h"
+#import "FIRIAMFetchResponseParser.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMServerMsgFetchStorage.h"
+
+@interface FIRIAMMessageClientCache ()
+
+// messages not for client-side testing
+@property(nonatomic) NSMutableArray<FIRIAMMessageDefinition *> *regularMessages;
+// messages for client-side testing
+@property(nonatomic) NSMutableArray<FIRIAMMessageDefinition *> *testMessages;
+@property(nonatomic, weak) id<FIRIAMCacheDataObserver> observer;
+@property(nonatomic) NSMutableSet<NSString *> *firebaseAnalyticEventsToWatch;
+@property(nonatomic) id<FIRIAMBookKeeper> bookKeeper;
+@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser;
+
+@end
+
+// Methods doing read and write operations on messages field is synchronized to avoid
+// race conditions like change the array while iterating through it
+@implementation FIRIAMMessageClientCache
+- (instancetype)initWithBookkeeper:(id<FIRIAMBookKeeper>)bookKeeper
+               usingResponseParser:(FIRIAMFetchResponseParser *)responseParser {
+  if (self = [super init]) {
+    _bookKeeper = bookKeeper;
+    _responseParser = responseParser;
+  }
+  return self;
+}
+
+- (void)setDataObserver:(id<FIRIAMCacheDataObserver>)observer {
+  self.observer = observer;
+}
+
+// reset messages data
+- (void)setMessageData:(NSArray<FIRIAMMessageDefinition *> *)messages {
+  @synchronized(self) {
+    NSSet<NSString *> *impressionSet =
+        [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
+
+    NSMutableArray<FIRIAMMessageDefinition *> *regularMessages = [[NSMutableArray alloc] init];
+    self.testMessages = [[NSMutableArray alloc] init];
+
+    // split between test vs non-test messages
+    for (FIRIAMMessageDefinition *next in messages) {
+      if (next.isTestMessage) {
+        [self.testMessages addObject:next];
+      } else {
+        [regularMessages addObject:next];
+      }
+    }
+
+    // while resetting the whole message set, we do prefiltering based on the impressions
+    // data to get rid of messages we don't care so that the future searches are more efficient
+    NSPredicate *notImpressedPredicate =
+        [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
+          FIRIAMMessageDefinition *message = (FIRIAMMessageDefinition *)evaluatedObject;
+          return ![impressionSet containsObject:message.renderData.messageID];
+        }];
+
+    self.regularMessages =
+        [[regularMessages filteredArrayUsingPredicate:notImpressedPredicate] mutableCopy];
+    [self setupAnalyticsEventListening];
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160001",
+              @"There are %lu test messages and %lu regular messages and "
+               "%lu Firebase Analytics events to watch after "
+               "resetting the message cache",
+              (unsigned long)self.testMessages.count, (unsigned long)self.regularMessages.count,
+              (unsigned long)self.firebaseAnalyticEventsToWatch.count);
+  [self.observer dataChanged];
+}
+
+// triggered after self.messages are updated so that we can correctly enable/disable listening
+// on analytics event based on current fiam message set
+- (void)setupAnalyticsEventListening {
+  self.firebaseAnalyticEventsToWatch = [[NSMutableSet alloc] init];
+  for (FIRIAMMessageDefinition *nextMessage in self.regularMessages) {
+    // if it's event based triggering, add it to the watch set
+    for (FIRIAMDisplayTriggerDefinition *nextTrigger in nextMessage.renderTriggers) {
+      if (nextTrigger.triggerType == FIRIAMRenderTriggerOnFirebaseAnalyticsEvent) {
+        [self.firebaseAnalyticEventsToWatch addObject:nextTrigger.firebaseEventName];
+      }
+    }
+  }
+
+  if (self.analycisEventDislayCheckFlow) {
+    if ([self.firebaseAnalyticEventsToWatch count] > 0) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160010",
+                  @"There are analytics event trigger based messages, enable listening");
+      [self.analycisEventDislayCheckFlow start];
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160011",
+                  @"No analytics event trigger based messages, disable listening");
+      [self.analycisEventDislayCheckFlow stop];
+    }
+  }
+}
+
+- (NSArray<FIRIAMMessageDefinition *> *)allRegularMessages {
+  return [self.regularMessages copy];
+}
+
+- (BOOL)hasTestMessage {
+  return self.testMessages.count > 0;
+}
+
+- (nullable FIRIAMMessageDefinition *)nextOnAppOpenDisplayMsg {
+  // search from the start to end in the list (which implies the display priority) for the
+  // first match (some messages in the cache may not be eligible for the current display
+  // message fetch
+  NSSet<NSString *> *impressionSet =
+      [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
+
+  @synchronized(self) {
+    // always first check test message which always have higher prirority
+    if (self.testMessages.count > 0) {
+      FIRIAMMessageDefinition *testMessage = self.testMessages[0];
+      // always remove test message right away when being fetched for display
+      [self.testMessages removeObjectAtIndex:0];
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160007",
+                  @"Returning a test message for app foreground display");
+      return testMessage;
+    }
+
+    for (FIRIAMMessageDefinition *next in self.regularMessages) {
+      // message being active and message not impressed yet
+      if ([next messageHasStarted] && ![next messageHasExpired] &&
+          ![impressionSet containsObject:next.renderData.messageID] &&
+          [next messageRenderedOnAppForegroundEvent]) {
+        return next;
+      }
+    }
+  }
+  return nil;
+}
+
+- (nullable FIRIAMMessageDefinition *)nextOnFirebaseAnalyticEventDisplayMsg:(NSString *)eventName {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160005",
+              @"Inside nextOnFirebaseAnalyticEventDisplay for checking contextual trigger match");
+  if (![self.firebaseAnalyticEventsToWatch containsObject:eventName]) {
+    return nil;
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM160006",
+              @"There could be a potential message match for analytics event %@", eventName);
+  NSSet<NSString *> *impressionSet =
+      [NSSet setWithArray:[self.bookKeeper getMessageIDsFromImpressions]];
+  @synchronized(self) {
+    for (FIRIAMMessageDefinition *next in self.regularMessages) {
+      // message being active and message not impressed yet and the contextual trigger condition
+      // match
+      if ([next messageHasStarted] && ![next messageHasExpired] &&
+          ![impressionSet containsObject:next.renderData.messageID] &&
+          [next messageRenderedOnAnalyticsEvent:eventName]) {
+        return next;
+      }
+    }
+  }
+  return nil;
+}
+
+- (void)removeMessageWithId:(NSString *)messageID {
+  FIRIAMMessageDefinition *msgToRemove = nil;
+  @synchronized(self) {
+    for (FIRIAMMessageDefinition *next in self.regularMessages) {
+      if ([next.renderData.messageID isEqualToString:messageID]) {
+        msgToRemove = next;
+        break;
+      }
+    }
+
+    if (msgToRemove) {
+      [self.regularMessages removeObject:msgToRemove];
+      [self setupAnalyticsEventListening];
+    }
+  }
+
+  // triggers the observer outside synchronization block
+  if (msgToRemove) {
+    [self.observer dataChanged];
+  }
+}
+
+- (void)loadMessageDataFromServerFetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
+                               withCompletion:(void (^)(BOOL success))completion {
+  [fetchStorage readResponseDictionary:^(NSDictionary *_Nonnull response, BOOL success) {
+    if (success) {
+      NSInteger discardCount;
+      NSNumber *fetchWaitTime;
+      NSArray<FIRIAMMessageDefinition *> *messagesFromStorage =
+          [self.responseParser parseAPIResponseDictionary:response
+                                        discardedMsgCount:&discardCount
+                                   fetchWaitTimeInSeconds:&fetchWaitTime];
+      [self setMessageData:messagesFromStorage];
+      completion(YES);
+    } else {
+      completion(NO);
+    }
+  }];
+}
+@end

+ 55 - 0
Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.h

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRIAMClientInfoFetcher.h"
+#import "FIRIAMFetchFlow.h"
+#import "FIRIAMFetchResponseParser.h"
+#import "FIRIAMSDKSettings.h"
+#import "FIRIAMServerMsgFetchStorage.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+// implementation of FIRIAMMessageFetcher by making Restful API requests to firebase
+// in-app messaging services
+@interface FIRIAMMsgFetcherUsingRestful : NSObject <FIRIAMMessageFetcher>
+/**
+ * Create an instance which uses NSURLSession to make the restful api call.
+ *
+ * @param serverHost API server host.
+ * @param fbProjectNumber project number used for the API call. It's the GCM_SENDER_ID
+ *                         field in GoogleService-Info.plist.
+ * @param fbAppId It's the GOOGLE_APP_ID field in GoogleService-Info.plist.
+ * @param apiKey API key.
+ * @param fetchStorage used to persist the fetched response.
+ * @param clientInfoFetcher used to fetch iid info for the current app.
+ * @param URLSession can be nil in which case the class would create NSURLSession
+ *                   internally to perform the network request. Having it here so that
+ *                   it's easier for doing mocking with unit testing.
+ */
+- (instancetype)initWithHost:(NSString *)serverHost
+                HTTPProtocol:(NSString *)HTTPProtocol
+                     project:(NSString *)fbProjectNumber
+                 firebaseApp:(NSString *)fbAppId
+                      APIKey:(NSString *)apiKey
+                fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
+           instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher
+             usingURLSession:(nullable NSURLSession *)URLSession
+              responseParser:(FIRIAMFetchResponseParser *)responseParser;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 272 - 0
Firebase/InAppMessaging/Flows/FIRIAMMsgFetcherUsingRestful.m

@@ -0,0 +1,272 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMFetchFlow.h"
+#import "FIRIAMFetchResponseParser.h"
+#import "FIRIAMMessageContentDataWithImageURL.h"
+#import "FIRIAMMessageDefinition.h"
+#import "FIRIAMMsgFetcherUsingRestful.h"
+#import "FIRIAMSDKSettings.h"
+
+static NSInteger const SuccessHTTPStatusCode = 200;
+
+@interface FIRIAMMsgFetcherUsingRestful ()
+@property(readonly) NSURLSession *URLSession;
+@property(readonly, copy, nonatomic) NSString *serverHostName;
+@property(readonly, copy, nonatomic) NSString *appBundleID;
+@property(readonly, copy, nonatomic) NSString *httpProtocol;
+@property(readonly, copy, nonatomic) NSString *fbProjectNumber;
+@property(readonly, copy, nonatomic) NSString *apiKey;
+@property(readonly, copy, nonatomic) NSString *firebaseAppId;
+@property(readonly, nonatomic) FIRIAMServerMsgFetchStorage *fetchStorage;
+@property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher;
+@property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser;
+@end
+
+@implementation FIRIAMMsgFetcherUsingRestful
+- (instancetype)initWithHost:(NSString *)serverHost
+                HTTPProtocol:(NSString *)HTTPProtocol
+                     project:(NSString *)fbProjectNumber
+                 firebaseApp:(NSString *)fbAppId
+                      APIKey:(NSString *)apiKey
+                fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
+           instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher
+             usingURLSession:(nullable NSURLSession *)URLSession
+              responseParser:(FIRIAMFetchResponseParser *)responseParser {
+  if (self = [super init]) {
+    _URLSession = URLSession ? URLSession : [NSURLSession sharedSession];
+    _serverHostName = [serverHost copy];
+    _fbProjectNumber = [fbProjectNumber copy];
+    _firebaseAppId = [fbAppId copy];
+    _httpProtocol = [HTTPProtocol copy];
+    _apiKey = [apiKey copy];
+    _clientInfoFetcher = clientInfoFetcher;
+    _fetchStorage = fetchStorage;
+    _appBundleID = [NSBundle mainBundle].bundleIdentifier;
+    _responseParser = responseParser;
+  }
+  return self;
+}
+
+- (void)updatePostFetchData:(NSMutableDictionary *)postData
+         withImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressionList
+           instanceIDString:(nonnull NSString *)IIDValue
+                   IIDToken:(nonnull NSString *)IIDToken {
+  NSMutableArray *impressionListForPost = [[NSMutableArray alloc] init];
+  for (FIRIAMImpressionRecord *nextImpressionRecord in impressionList) {
+    NSDictionary *nextImpression = @{
+      @"campaign_id" : nextImpressionRecord.messageID,
+      @"impression_timestamp_millis" : @(nextImpressionRecord.impressionTimeInSeconds * 1000)
+    };
+    [impressionListForPost addObject:nextImpression];
+  }
+  [postData setObject:impressionListForPost forKey:@"already_seen_campaigns"];
+
+  if (IIDValue) {
+    NSDictionary *clientAppInfo = @{
+      @"gmp_app_id" : self.firebaseAppId,
+      @"app_instance_id" : IIDValue,
+      @"app_instance_id_token" : IIDToken
+    };
+    [postData setObject:clientAppInfo forKey:@"requesting_client_app"];
+  }
+
+  NSMutableArray *clientSignals = [@{} mutableCopy];
+
+  // set client signal fields only when they are present
+  if ([self.clientInfoFetcher getAppVersion]) {
+    [clientSignals setValue:[self.clientInfoFetcher getAppVersion] forKey:@"app_version"];
+  }
+
+  if ([self.clientInfoFetcher getOSVersion]) {
+    [clientSignals setValue:[self.clientInfoFetcher getOSVersion] forKey:@"platform_version"];
+  }
+
+  if ([self.clientInfoFetcher getDeviceLanguageCode]) {
+    [clientSignals setValue:[self.clientInfoFetcher getDeviceLanguageCode] forKey:@"language_code"];
+  }
+
+  if ([self.clientInfoFetcher getTimezone]) {
+    [clientSignals setValue:[self.clientInfoFetcher getTimezone] forKey:@"time_zone"];
+  }
+
+  [postData setObject:clientSignals forKey:@"client_signals"];
+}
+
+- (void)fetchMessagesWithImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressonList
+                           withIIDvalue:(NSString *)iidValue
+                               IIDToken:(NSString *)iidToken
+                             completion:(FIRIAMFetchMessageCompletionHandler)completion {
+  NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
+  [request setHTTPMethod:@"POST"];
+
+  if (_appBundleID.length) {
+    // Handle the case in which the API key is being restricted to specific iOS app bundle,
+    // which can be set on Google Cloud console side for API key credentials.
+    [request addValue:_appBundleID forHTTPHeaderField:@"X-Ios-Bundle-Identifier"];
+  }
+
+  [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
+  [request addValue:@"application/json" forHTTPHeaderField:@"Accept"];
+
+  NSMutableDictionary *postFetchDict = [[NSMutableDictionary alloc] init];
+  [self updatePostFetchData:postFetchDict
+         withImpressionList:impressonList
+           instanceIDString:iidValue
+                   IIDToken:iidToken];
+
+  NSData *postFetchData = [NSJSONSerialization dataWithJSONObject:postFetchDict
+                                                          options:0
+                                                            error:nil];
+
+  NSString *requestURLString = [NSString
+      stringWithFormat:@"%@://%@/v1/sdkServing/projects/%@/eligibleCampaigns:fetch?key=%@",
+                       self.httpProtocol, self.serverHostName, self.fbProjectNumber, self.apiKey];
+  [request setURL:[NSURL URLWithString:requestURLString]];
+  [request setHTTPBody:postFetchData];
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130001",
+              @"Making a restful API request for pulling messages with fetch POST body as %@ "
+               "and request headers as %@",
+              postFetchDict, request.allHTTPHeaderFields);
+
+  NSURLSessionDataTask *postDataTask = [self.URLSession
+      dataTaskWithRequest:request
+        completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+          if (error) {
+            FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130002",
+                          @"Internal error: encountered error in pulling messages from server"
+                           ":%@",
+                          error);
+            completion(nil, nil, 0, error);
+          } else {
+            if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+              NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+              if (httpResponse.statusCode == SuccessHTTPStatusCode) {
+                // got response data successfully
+                FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130007",
+                            @"Fetch API response headers are %@", [httpResponse allHeaderFields]);
+
+                NSError *errorJson = nil;
+                NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data
+                                                                             options:kNilOptions
+                                                                               error:&errorJson];
+                if (errorJson) {
+                  FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130003",
+                                @"Failed to parse the response body as JSON string %@", errorJson);
+                  completion(nil, nil, 0, errorJson);
+                } else {
+                  NSInteger discardCount;
+                  NSNumber *nextFetchWaitTimeFromResponse;
+                  NSArray<FIRIAMMessageDefinition *> *messages = [self.responseParser
+                      parseAPIResponseDictionary:responseDict
+                               discardedMsgCount:&discardCount
+                          fetchWaitTimeInSeconds:&nextFetchWaitTimeFromResponse];
+
+                  if (messages) {
+                    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130012",
+                                @"API request for fetching messages and parsing the response was "
+                                 "successful.");
+                    [self.fetchStorage
+                        saveResponseDictionary:responseDict
+                                withCompletion:^(BOOL success) {
+                                  if (!success)
+                                    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130010",
+                                                  @"Failed to persist server fetch response");
+                                }];
+                    // always report success regardless of whether we are able to persist into
+                    // storage. they should get fixed in the next fetch cycle if it happens.
+                    completion(messages, nextFetchWaitTimeFromResponse, discardCount, nil);
+                  } else {
+                    NSString *errorDesc =
+                        @"Failed to recognize the fiam messages in the server response";
+                    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130011", @"%@", errorDesc);
+                    NSError *error =
+                        [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                                            code:0
+                                        userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
+                    completion(nil, nil, 0, error);
+                  }
+                }
+              } else {
+                NSString *responseBody = [[NSString alloc] initWithData:data
+                                                               encoding:NSUTF8StringEncoding];
+
+                FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130004",
+                              @"Failed restful api request to fetch in-app messages: seeing http "
+                              @"status code as %ld with body as %@",
+                              (long)httpResponse.statusCode, responseBody);
+
+                NSError *error = [NSError errorWithDomain:NSURLErrorDomain
+                                                     code:httpResponse.statusCode
+                                                 userInfo:nil];
+                completion(nil, nil, 0, error);
+              }
+            } else {
+              NSString *errorDesc = @"Got a non http response type from fetch endpoint";
+              FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130005", @"%@", errorDesc);
+
+              NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                                                   code:0
+                                               userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
+              completion(nil, nil, 0, error);
+            }
+          }
+        }];
+
+  if (postDataTask == nil) {
+    NSString *errorDesc =
+        @"Internal error: NSURLSessionDataTask failed to be created due to possibly "
+         "incorrect parameters";
+    FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130006", @"%@", errorDesc);
+    NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                                         code:0
+                                     userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
+    completion(nil, nil, 0, error);
+  } else {
+    [postDataTask resume];
+  }
+}
+
+#pragma mark - protocol FIRIAMMessageFetcher
+- (void)fetchMessagesWithImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressonList
+                         withCompletion:(FIRIAMFetchMessageCompletionHandler)completion {
+  // First step is to fetch the instance id value and token on the fly. We are not caching the data
+  // since the fetch operation frequency is low enough that we are not concerned about its impact
+  // on server load and this guarantees that we always have an up-to-date iid values and tokens.
+  [self.clientInfoFetcher
+      fetchFirebaseIIDDataWithProjectNumber:self.fbProjectNumber
+                             withCompletion:^(NSString *_Nullable iid, NSString *_Nullable token,
+                                              NSError *_Nullable error) {
+                               if (error) {
+                                 FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130008",
+                                               @"Not able to get iid value and/or token for "
+                                               @"talking to server: %@",
+                                               error.localizedDescription);
+                                 completion(nil, nil, 0, error);
+                               } else {
+                                 [self fetchMessagesWithImpressionList:impressonList
+                                                          withIIDvalue:iid
+                                                              IIDToken:token
+                                                            completion:completion];
+                               }
+                             }];
+}
+@end

+ 30 - 0
Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// A class that will persist response data fetched from server side into a local file on
+// client side. This file can be used as the cache for messages after the app has been
+// killed and before it's up for next server fetch.
+@interface FIRIAMServerMsgFetchStorage : NSObject
+- (void)saveResponseDictionary:(NSDictionary *)response
+                withCompletion:(void (^)(BOOL success))completion;
+- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion;
+
+@end
+NS_ASSUME_NONNULL_END

+ 64 - 0
Firebase/InAppMessaging/Flows/FIRIAMServerMsgFetchStorage.m

@@ -0,0 +1,64 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMServerMsgFetchStorage.h"
+@implementation FIRIAMServerMsgFetchStorage
+- (NSString *)determineCacheFilePath {
+  NSString *cachePath =
+      NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
+  NSString *filePath = [NSString stringWithFormat:@"%@/firebase-iam-messages-cache", cachePath];
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150004",
+              @"Persistent file path for fetch response data is %@", filePath);
+  return filePath;
+}
+
+- (void)saveResponseDictionary:(NSDictionary *)response
+                withCompletion:(void (^)(BOOL success))completion {
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    if ([response writeToFile:[self determineCacheFilePath] atomically:YES]) {
+      completion(YES);
+    } else {
+      completion(NO);
+    }
+  });
+}
+
+- (void)readResponseDictionary:(void (^)(NSDictionary *response, BOOL success))completion {
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    NSString *storageFilePath = [self determineCacheFilePath];
+    if ([[NSFileManager defaultManager] fileExistsAtPath:storageFilePath]) {
+      NSDictionary *dictFromFile =
+          [[NSMutableDictionary dictionaryWithContentsOfFile:[self determineCacheFilePath]] copy];
+      if (dictFromFile) {
+        FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150001",
+                    @"Loaded response from fetch storage successfully.");
+        completion(dictFromFile, YES);
+      } else {
+        FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM150002",
+                      @"Not able to read response from fetch storage.");
+        completion(dictFromFile, NO);
+      }
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM150003",
+                  @"Local fetch storage file not existent yet: first time launch of the app.");
+      completion(nil, YES);
+    }
+  });
+}
+@end

+ 80 - 0
Firebase/InAppMessaging/Public/FIRInAppMessaging.h

@@ -0,0 +1,80 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRApp;
+
+#import "FIRInAppMessagingRendering.h"
+
+NS_ASSUME_NONNULL_BEGIN
+/**
+ * The root object for in-app messaging iOS SDK.
+ *
+ * Note: Firebase InApp Messaging depends on using a Firebase Instance ID & token pair to be able
+ * to retrieve FIAM messages defined for the current app instance. By default, Firebase in-app
+ * messaging SDK would obtain the ID & token pair on app/SDK startup. As a result of using
+ * ID & token pair, some device client data (linked to the instance ID) would be collected and sent
+ * over to Firebase backend periodically.
+ *
+ * The app can tune the default data collection behavior via certain controls. They are listed in
+ * descending order below. If a higher-priority setting exists, lower level settings are ignored.
+ *
+ *   1. Dynamically turn on/off data collection behavior by setting the
+ *     `automaticDataCollectionEnabled` property on the `FIRInAppMessaging` instance to true/false
+ *      Swift or YES/NO (objective-c).
+ *   2. Set `FirebaseInAppMessagingAutomaticDataCollectionEnabled` to false in the app's plist file.
+ *   3. Global Firebase data collection setting.
+ **/
+NS_SWIFT_NAME(InAppMessaging)
+@interface FIRInAppMessaging : NSObject
+/** @fn inAppMessaging
+    @brief Gets the singleton FIRInAppMessaging object constructed from default Firebase App
+    settings.
+*/
++ (FIRInAppMessaging *)inAppMessaging NS_SWIFT_NAME(inAppMessaging());
+
+/**
+ *  Unavailable. Use +inAppMessaging instead.
+ */
+- (instancetype)init __attribute__((unavailable("Use +inAppMessaging instead.")));
+
+/**
+ * A boolean flag that can be used to suppress messaging display at runtime. It's
+ * initialized to false at app startup. Once set to true, fiam SDK would stop rendering any
+ * new messages until it's set back to false.
+ */
+@property(nonatomic) BOOL messageDisplaySuppressed;
+
+/**
+ * A boolean flag that can be set at runtime to allow/disallow fiam SDK automatically
+ * collect user data on app startup. Settings made via this property is persisted across app
+ * restarts and has higher priority over FirebaseInAppMessagingAutomaticDataCollectionEnabled
+ * flag (if present) in plist file.
+ */
+@property(nonatomic) BOOL automaticDataCollectionEnabled;
+
+/**
+ * This is the display component that will be used by FirebaseInAppMessaging to render messages.
+ * If it's nil (the default case when FirebaseIAppMessaging SDK starts), FirebaseInAppMessaging
+ * would only perform other non-rendering flows (fetching messages for example). SDK
+ * FirebaseInAppMessagingDisplay would set itself as the display component if it's included by
+ * the app. Any other custom implementation of FIRInAppMessagingDisplay would need to set this
+ * property so that it can be used for rendering fiam message UIs.
+ */
+@property(nonatomic) id<FIRInAppMessagingDisplay> messageDisplayComponent;
+@end
+NS_ASSUME_NONNULL_END

+ 251 - 0
Firebase/InAppMessaging/Public/FIRInAppMessagingRendering.h

@@ -0,0 +1,251 @@
+/*
+ * 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/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** Contains the display information for an action button.
+ */
+NS_SWIFT_NAME(InAppMessagingActionButton)
+@interface FIRInAppMessagingActionButton : NSObject
+
+/**
+ * Gets the text string for the button
+ */
+@property(nonatomic, nonnull, copy, readonly) NSString *buttonText;
+
+/**
+ * Gets the button's text color.
+ */
+@property(nonatomic, copy, nonnull, readonly) UIColor *buttonTextColor;
+
+/**
+ * Gets the button's background color
+ */
+@property(nonatomic, copy, nonnull, readonly) UIColor *buttonBackgroundColor;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithButtonText:(NSString *)btnText
+                   buttonTextColor:(UIColor *)textColor
+                   backgroundColor:(UIColor *)bkgColor NS_DESIGNATED_INITIALIZER;
+@end
+
+/** Contain display data for an image for a fiam message.
+ */
+NS_SWIFT_NAME(InAppMessagingImageData)
+@interface FIRInAppMessagingImageData : NSObject
+@property(nonatomic, nonnull, copy, readonly) NSString *imageURL;
+
+/**
+ * Gets the downloaded image data. It can be null if headless component fails to load it.
+ */
+@property(nonatomic, readonly, nullable) NSData *imageRawData;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithImageURL:(NSString *)imageURL
+                       imageData:(NSData *)imageData NS_DESIGNATED_INITIALIZER;
+@end
+
+/**
+ * Base class representing a FIAM message to be displayed. Don't create instance
+ * of this class directly. Instantiate one of its subclasses instead.
+ */
+NS_SWIFT_NAME(InAppMessagingDisplayMessageBase)
+@interface FIRInAppMessagingDisplayMessageBase : NSObject
+@property(nonatomic, copy, nonnull, readonly) NSString *messageID;
+@property(nonatomic, readonly) BOOL renderAsTestMessage;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage;
+@end
+
+/** Class for defining a modal message for display.
+ */
+NS_SWIFT_NAME(InAppMessagingModalDisplay)
+@interface FIRInAppMessagingModalDisplay : FIRInAppMessagingDisplayMessageBase
+
+/**
+ * Gets the title for a modal fiam message.
+ */
+@property(nonatomic, nonnull, copy, readonly) NSString *title;
+
+/**
+ * Gets the image data for a modal fiam message.
+ */
+@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData;
+
+/**
+ * Gets the body text for a modal fiam message.
+ */
+@property(nonatomic, nullable, copy, readonly) NSString *bodyText;
+
+/**
+ * Gets the action button metadata for a modal fiam message.
+ */
+@property(nonatomic, nullable, readonly) FIRInAppMessagingActionButton *actionButton;
+
+/**
+ * Gets the background color for a modal fiam message.
+ */
+@property(nonatomic, copy, nonnull) UIColor *displayBackgroundColor;
+
+/**
+ * Gets the color for text in modal fiam message. It would apply to both title and body text.
+ */
+@property(nonatomic, copy, nonnull) UIColor *textColor;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        titleText:(NSString *)title
+                         bodyText:(NSString *)bodyText
+                        textColor:(UIColor *)textColor
+                  backgroundColor:(UIColor *)backgroundColor
+                        imageData:(nullable FIRInAppMessagingImageData *)imageData
+                     actionButton:(nullable FIRInAppMessagingActionButton *)actionButton
+    NS_DESIGNATED_INITIALIZER;
+@end
+
+/** Class for defining a banner message for display.
+ */
+NS_SWIFT_NAME(InAppMessagingBannerDisplay)
+@interface FIRInAppMessagingBannerDisplay : FIRInAppMessagingDisplayMessageBase
+// Title is always required for modal messages.
+@property(nonatomic, nonnull, copy, readonly) NSString *title;
+
+// Image, body, action URL are all optional for banner messages.
+@property(nonatomic, nullable, copy, readonly) FIRInAppMessagingImageData *imageData;
+@property(nonatomic, nullable, copy, readonly) NSString *bodyText;
+
+/**
+ * Gets banner's background color
+ */
+@property(nonatomic, copy, nonnull, readonly) UIColor *displayBackgroundColor;
+
+/**
+ * Gets the color for text in banner fiam message. It would apply to both title and body text.
+ */
+@property(nonatomic, copy, nonnull) UIColor *textColor;
+
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        titleText:(NSString *)title
+                         bodyText:(NSString *)bodyText
+                        textColor:(UIColor *)textColor
+                  backgroundColor:(UIColor *)backgroundColor
+                        imageData:(nullable FIRInAppMessagingImageData *)imageData
+    NS_DESIGNATED_INITIALIZER;
+@end
+
+/** Class for defining a image-only message for display.
+ */
+NS_SWIFT_NAME(InAppMessagingImageOnlyDisplay)
+@interface FIRInAppMessagingImageOnlyDisplay : FIRInAppMessagingDisplayMessageBase
+
+/**
+ * Gets the image for this message
+ */
+@property(nonatomic, nonnull, copy, readonly) FIRInAppMessagingImageData *imageData;
+- (instancetype)init NS_UNAVAILABLE;
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        imageData:(FIRInAppMessagingImageData *)imageData NS_DESIGNATED_INITIALIZER;
+@end
+
+typedef NS_ENUM(NSInteger, FIRInAppMessagingDismissType) {
+  FIRInAppMessagingDismissTypeUserSwipe,     // user swipes away the banner view
+  FIRInAppMessagingDismissTypeUserTapClose,  // user clicks on close buttons
+  FIRInAppMessagingDismissTypeAuto,          // automatic dismiss from banner view
+  FIRInAppMessagingDismissUnspecified,       // message is dismissed, but not belonging to any
+                                             // above dismiss category.
+};
+
+// enum integer value used in as code for NSError reported from displayErrorEncountered: callback
+typedef NS_ENUM(NSInteger, FIAMDisplayRenderErrorType) {
+  FIAMDisplayRenderErrorTypeImageDataInvalid,  // provided image data is not valid for image
+                                               // rendering
+  FIAMDisplayRenderErrorTypeUnspecifiedError,  // error not classified, mainly unexpected
+                                               // failure cases
+};
+
+/**
+ * A protocol defining those callbacks to be triggered by the message display component
+ * under appropriate conditions.
+ */
+NS_SWIFT_NAME(InAppMessagingDisplayDelegate)
+@protocol FIRInAppMessagingDisplayDelegate <NSObject>
+/**
+ * Called when the message is dismissed. Should be called from main thread.
+ * @param dismissType specifies how the message is closed.
+ */
+- (void)messageDismissedWithType:(FIRInAppMessagingDismissType)dismissType
+    NS_SWIFT_NAME(messageDismissed(dismissType:));
+
+/**
+ * Called when the message's action button is followed by the user.
+ */
+- (void)messageClicked;
+
+/**
+ * Use this to mark a message as having gone through enough impression so that
+ * headless component can make appropriate impression tracking for it.
+ *
+ * Calling this is optional.
+ *
+ * When messageDismissedWithType: or messageClicked is
+ * triggered, the message would be marked as having a valid impression implicitly.
+ * Use impressionDetected if the UI implementation would like to mark valid
+ * impression in additional cases. One example is that the message is displayed for
+ * N seconds and then the app is killed by the user. Neither
+ * onMessageDismissedWithType or onMessageClicked would be triggered
+ * in this case. But if the app regards this as a valid impression and does not
+ * want the user to see the same message again, call impressionDetected to mark
+ * a valid impression.
+ */
+- (void)impressionDetected;
+
+/**
+ * Called when the display component could not render the message due to various reason.
+ * It's essential for display component to call this when error does arise. On seeing
+ * this, the headless component of fiam would assume that a prior attempt to render a
+ * message has finished and therefore it's ready to render a new one when conditions are
+ * met. Missing this callback in failed rendering attempt would make headless
+ * component think a fiam message is still being rendered and therefore suppress any
+ * future message rendering.
+ */
+- (void)displayErrorEncountered:(NSError *)error;
+@end
+
+/**
+ * The protocol that a FIAM display component must implement.
+ */
+NS_SWIFT_NAME(InAppMessagingDisplay)
+@protocol FIRInAppMessagingDisplay
+
+/**
+ * Method for rendering a specified message on client side. It's called from main thread.
+ * @param messageForDisplay the message object. It would be of one of the three message
+ *   types at runtime.
+ * @param displayDelegate the callback object used to trigger notifications about certain
+ *        conditions related to message rendering.
+ */
+- (void)displayMessage:(FIRInAppMessagingDisplayMessageBase *)messageForDisplay
+       displayDelegate:(id<FIRInAppMessagingDisplayDelegate>)displayDelegate;
+@end
+NS_ASSUME_NONNULL_END

+ 108 - 0
Firebase/InAppMessaging/RenderingObjects/FIRInAppMessagingRenderingDataClasses.m

@@ -0,0 +1,108 @@
+/*
+ * 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/Foundation.h>
+
+#import "FIRInAppMessagingRendering.h"
+
+@implementation FIRInAppMessagingDisplayMessageBase
+
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage {
+  if (self = [super init]) {
+    _messageID = messageID;
+    _renderAsTestMessage = renderAsTestMessage;
+  }
+  return self;
+}
+@end
+
+@implementation FIRInAppMessagingBannerDisplay
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        titleText:(NSString *)title
+                         bodyText:(NSString *)bodyText
+                        textColor:(UIColor *)textColor
+                  backgroundColor:(UIColor *)backgroundColor
+                        imageData:(nullable FIRInAppMessagingImageData *)imageData {
+  if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) {
+    _title = title;
+    _bodyText = bodyText;
+    _textColor = textColor;
+    _displayBackgroundColor = backgroundColor;
+    _imageData = imageData;
+  }
+  return self;
+}
+@end
+
+@implementation FIRInAppMessagingModalDisplay
+
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        titleText:(NSString *)title
+                         bodyText:(NSString *)bodyText
+                        textColor:(UIColor *)textColor
+                  backgroundColor:(UIColor *)backgroundColor
+                        imageData:(nullable FIRInAppMessagingImageData *)imageData
+                     actionButton:(nullable FIRInAppMessagingActionButton *)actionButton {
+  if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) {
+    _title = title;
+    _bodyText = bodyText;
+    _textColor = textColor;
+    _displayBackgroundColor = backgroundColor;
+    _imageData = imageData;
+    _actionButton = actionButton;
+  }
+  return self;
+}
+@end
+
+@implementation FIRInAppMessagingImageOnlyDisplay
+
+- (instancetype)initWithMessageID:(NSString *)messageID
+              renderAsTestMessage:(BOOL)renderAsTestMessage
+                        imageData:(FIRInAppMessagingImageData *)imageData {
+  if (self = [super initWithMessageID:messageID renderAsTestMessage:renderAsTestMessage]) {
+    _imageData = imageData;
+  }
+  return self;
+}
+@end
+
+@implementation FIRInAppMessagingActionButton
+
+- (instancetype)initWithButtonText:(NSString *)btnText
+                   buttonTextColor:(UIColor *)textColor
+                   backgroundColor:(UIColor *)bkgColor {
+  if (self = [super init]) {
+    _buttonText = btnText;
+    _buttonTextColor = textColor;
+    _buttonBackgroundColor = bkgColor;
+  }
+  return self;
+}
+@end
+
+@implementation FIRInAppMessagingImageData
+- (instancetype)initWithImageURL:(NSString *)imageURL imageData:(NSData *)imageData {
+  if (self = [super init]) {
+    _imageURL = imageURL;
+    _imageRawData = imageData;
+  }
+  return self;
+}
+@end

+ 46 - 0
Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.h

@@ -0,0 +1,46 @@
+/*
+ * 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/Foundation.h>
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+// A class for handling action url following.
+// It tries to handle these cases:
+//    1 Follow a universal link.
+//    2 Follow a custom url scheme link.
+//    3 Follow other types of links.
+@interface FIRIAMActionURLFollower : NSObject
+
+// Create an FIRIAMActionURLFollower object by inspecting the app's main bundle info.
++ (instancetype)actionURLFollower;
+
+- (instancetype)init NS_UNAVAILABLE;
+
+// initialize the instance with an array of supported custom url schemes and
+// the main application object
+- (instancetype)initWithCustomURLSchemeArray:(NSArray<NSString *> *)customURLScheme
+                             withApplication:(UIApplication *)application NS_DESIGNATED_INITIALIZER;
+
+/**
+ * Follow a given URL. Report success in the completion block parameter. Notice that
+ * it can not always be fully sure about whether the operation is successful. So it's a clue
+ * in some cases.
+ * Check its implementation about the details in the following logic.
+ */
+- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion;
+@end
+NS_ASSUME_NONNULL_END

+ 244 - 0
Firebase/InAppMessaging/Runtime/FIRIAMActionURLFollower.m

@@ -0,0 +1,244 @@
+/*
+ * 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/Foundation.h>
+#import <UIKit/UIKit.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMActionURLFollower.h"
+
+@interface FIRIAMActionURLFollower ()
+@property(nonatomic, readonly, nonnull, copy) NSSet<NSString *> *appCustomURLSchemesSet;
+@property(nonatomic, readonly) BOOL isOldAppDelegateOpenURLDefined;
+@property(nonatomic, readonly) BOOL isNewAppDelegateOpenURLDefined;
+@property(nonatomic, readonly) BOOL isContinueUserActivityMethodDefined;
+
+@property(nonatomic, readonly, nullable) id<UIApplicationDelegate> appDelegate;
+@property(nonatomic, readonly, nonnull) UIApplication *mainApplication;
+@end
+
+@implementation FIRIAMActionURLFollower
+
++ (FIRIAMActionURLFollower *)actionURLFollower {
+  static FIRIAMActionURLFollower *URLFollower;
+  static dispatch_once_t onceToken;
+
+  dispatch_once(&onceToken, ^{
+    NSMutableArray<NSString *> *customSchemeURLs = [[NSMutableArray alloc] init];
+
+    // Reading the custom url list from the environment.
+    NSBundle *appBundle = [NSBundle mainBundle];
+    if (appBundle) {
+      id URLTypesID = [appBundle objectForInfoDictionaryKey:@"CFBundleURLTypes"];
+      if ([URLTypesID isKindOfClass:[NSArray class]]) {
+        NSArray *urlTypesArray = (NSArray *)URLTypesID;
+
+        for (id nextURLType in urlTypesArray) {
+          if ([nextURLType isKindOfClass:[NSDictionary class]]) {
+            NSDictionary *nextURLTypeDict = (NSDictionary *)nextURLType;
+            id nextSchemeArray = nextURLTypeDict[@"CFBundleURLSchemes"];
+            if (nextSchemeArray && [nextSchemeArray isKindOfClass:[NSArray class]]) {
+              [customSchemeURLs addObjectsFromArray:nextSchemeArray];
+            }
+          }
+        }
+      }
+    }
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM300010",
+                @"Detected %d custom url schems from environment", (int)customSchemeURLs.count);
+
+    if ([NSThread isMainThread]) {
+      // We can not dispatch sychronously to main queue if we are already in main queue. That
+      // can cause deadlock.
+      URLFollower = [[FIRIAMActionURLFollower alloc]
+          initWithCustomURLSchemeArray:customSchemeURLs
+                       withApplication:UIApplication.sharedApplication];
+    } else {
+      // If we are not on main thread, dispatch it to main queue since it invovles calling UIKit
+      // methods, which are required to be carried out on main queue.
+      dispatch_sync(dispatch_get_main_queue(), ^{
+        URLFollower = [[FIRIAMActionURLFollower alloc]
+            initWithCustomURLSchemeArray:customSchemeURLs
+                         withApplication:UIApplication.sharedApplication];
+      });
+    }
+  });
+  return URLFollower;
+}
+
+- (instancetype)initWithCustomURLSchemeArray:(NSArray<NSString *> *)customURLScheme
+                             withApplication:(UIApplication *)application {
+  if (self = [super init]) {
+    _appCustomURLSchemesSet = [NSSet setWithArray:customURLScheme];
+    _mainApplication = application;
+    _appDelegate = [application delegate];
+
+    if (_appDelegate) {
+      _isOldAppDelegateOpenURLDefined = [_appDelegate
+          respondsToSelector:@selector(application:openURL:sourceApplication:annotation:)];
+
+      _isNewAppDelegateOpenURLDefined =
+          [_appDelegate respondsToSelector:@selector(application:openURL:options:)];
+
+      _isContinueUserActivityMethodDefined = [_appDelegate
+          respondsToSelector:@selector(application:continueUserActivity:restorationHandler:)];
+    }
+  }
+  return self;
+}
+
+- (void)followActionURL:(NSURL *)actionURL withCompletionBlock:(void (^)(BOOL success))completion {
+  // So this is the logic of the url following flow
+  //  1 If it's a http or https link
+  //     1.1 If delegate implements application:continueUserActivity:restorationHandler: and calling
+  //       it returns YES: the flow stops here: we have finished the url-following action
+  //     1.2 In other cases: fall through to step 3
+  //  2 If the URL scheme matches any element in appCustomURLSchemes
+  //     2.1 Triggers application:openURL:options: or
+  //     application:openURL:sourceApplication:annotation:
+  //          depending on their availability.
+  //  3 Use UIApplication openURL: or openURL:options:completionHandler: to have iOS system to deal
+  //     with the url following.
+  //
+  //  The rationale for doing step 1 and 2 instead of simply doing step 3 for all cases are:
+  //     I)  calling UIApplication openURL with the universal link targeted for current app would
+  //         not cause the link being treated as a universal link. See apple doc at
+  // https://developer.apple.com/library/content/documentation/General/Conceptual/AppSearch/UniversalLinks.html
+  //         So step 1 is trying to handle this gracefully
+  //     II) If there are other apps on the same device declaring the same custom url scheme as for
+  //         the current app, doing step 3 directly have the risk of triggering another app for
+  //         handling the custom scheme url: See the note about "If more than one third-party" from
+  // https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/Inter-AppCommunication/Inter-AppCommunication.html
+  //         So step 2 is to optimize user experience by short-circuiting the engagement with iOS
+  //         system
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"Following action url %@", actionURL);
+
+  if ([self.class isHttpOrHttpsScheme:actionURL]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240001", @"Try to treat it as a universal link.");
+    if ([self followURLWithContinueUserActivity:actionURL]) {
+      completion(YES);
+      return;  // following the url has been fully handled by App Delegate's
+               // continueUserActivity method
+    }
+  } else if ([self isCustomSchemeForCurrentApp:actionURL]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240002", @"Custom URL scheme matches.");
+    if ([self followURLWithAppDelegateOpenURLActivity:actionURL]) {
+      completion(YES);
+      return;  // following the url has been fully handled by App Delegate's openURL method
+    }
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240003", @"Open the url via iOS.");
+  [self followURLViaIOS:actionURL withCompletionBlock:completion];
+}
+
+// Try to handle the url as a custom scheme url link by triggering
+// application:openURL:options: on App's delegate object directly.
+// @returns YES if that delegate method is defined and returns YES.
+- (BOOL)followURLWithAppDelegateOpenURLActivity:(NSURL *)url {
+  if (self.isNewAppDelegateOpenURLDefined) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM210008",
+                @"iOS 9+ version of App Delegate's application:openURL:options: method detected");
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability"
+    return [self.appDelegate application:self.mainApplication openURL:url options:@{}];
+#pragma clang pop
+  }
+
+  // if we come here, we can try to trigger the older version of openURL method on the app's
+  // delegate
+  if (self.isOldAppDelegateOpenURLDefined) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240009",
+                @"iOS 9 below version of App Delegate's openURL method detected");
+    NSString *appBundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
+    BOOL handled = [self.appDelegate application:self.mainApplication
+                                         openURL:url
+                               sourceApplication:appBundleIdentifier
+                                      annotation:@{}];
+    return handled;
+  }
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240010",
+              @"No approriate openURL method defined for App Delegate");
+  return NO;
+}
+
+// Try to handle the url as a universal link by triggering
+// application:continueUserActivity:restorationHandler: on App's delegate object directly.
+// @returns YES if that delegate method is defined and seeing a YES being returned from
+// trigging it
+- (BOOL)followURLWithContinueUserActivity:(NSURL *)url {
+  if (self.isContinueUserActivityMethodDefined) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004",
+                @"App delegate responds to application:continueUserActivity:restorationHandler:."
+                 "Simulating action url opening from a web browser.");
+    NSUserActivity *userActivity =
+        [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb];
+    userActivity.webpageURL = url;
+    BOOL handled = [self.appDelegate application:self.mainApplication
+                            continueUserActivity:userActivity
+                              restorationHandler:^(NSArray *restorableObjects) {
+                                // mimic system behavior of triggering restoreUserActivityState:
+                                // method on each element of restorableObjects
+                                for (id nextRestoreObject in restorableObjects) {
+                                  if ([nextRestoreObject isKindOfClass:[UIResponder class]]) {
+                                    UIResponder *responder = (UIResponder *)nextRestoreObject;
+                                    [responder restoreUserActivityState:userActivity];
+                                  }
+                                }
+                              }];
+    if (handled) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240005",
+                  @"App handling acton URL returns YES, no more further action taken");
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240004", @"App handling acton URL returns NO.");
+    }
+    return handled;
+  } else {
+    return NO;
+  }
+}
+
+- (void)followURLViaIOS:(NSURL *)url withCompletionBlock:(void (^)(BOOL success))completion {
+  if ([self.mainApplication respondsToSelector:@selector(openURL:options:completionHandler:)]) {
+    NSDictionary *options = @{};
+    [self.mainApplication
+                  openURL:url
+                  options:options
+        completionHandler:^(BOOL success) {
+          FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240006", @"openURL result is %d", success);
+          completion(success);
+        }];
+  } else {
+    // fallback to the older version of openURL
+    BOOL success = [self.mainApplication openURL:url];
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM240007", @"openURL result is %d", success);
+    completion(success);
+  }
+}
+
+- (BOOL)isCustomSchemeForCurrentApp:(NSURL *)url {
+  NSString *schemeInLowerCase = [url.scheme lowercaseString];
+  return [self.appCustomURLSchemesSet containsObject:schemeInLowerCase];
+}
+
++ (BOOL)isHttpOrHttpsScheme:(NSURL *)url {
+  NSString *schemeInLowerCase = [url.scheme lowercaseString];
+  return
+      [schemeInLowerCase isEqualToString:@"https"] || [schemeInLowerCase isEqualToString:@"http"];
+}
+@end

+ 56 - 0
Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.h

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMDisplayExecutor.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMSDKSettings.h"
+#import "FIRIAMServerMsgFetchStorage.h"
+
+NS_ASSUME_NONNULL_BEGIN
+// A class for managing the objects/dependencies for supporting different fiam flows at runtime
+@interface FIRIAMRuntimeManager : NSObject
+@property(nonatomic, nonnull) FIRIAMSDKSettings *currentSetting;
+@property(nonatomic, nonnull) FIRIAMActivityLogger *activityLogger;
+@property(nonatomic, nonnull) FIRIAMBookKeeperViaUserDefaults *bookKeeper;
+@property(nonatomic, nonnull) FIRIAMMessageClientCache *messageCache;
+@property(nonatomic, nonnull) FIRIAMServerMsgFetchStorage *fetchResultStorage;
+@property(nonatomic, nonnull) FIRIAMDisplayExecutor *displayExecutor;
+
+// Initialize fiam SDKs and start various flows with specified settings.
+- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings;
+
+// Pause runtime flows/functions to disable SDK functions at runtime
+- (void)pause;
+
+// Resume runtime flows/functions.
+- (void)resume;
+
+// allows app to programmatically turn on/off auto data collection for fiam, which also implies
+// running/stopping fiam functionalities
+@property(nonatomic) BOOL automaticDataCollectionEnabled;
+
+// Get the global singleton instance
++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance;
+
+// a method used to suppress or allow message being displayed based on the parameter
+// @param shouldSuppress if true, no new message is rendered by the sdk.
+- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress;
+@end
+NS_ASSUME_NONNULL_END

+ 431 - 0
Firebase/InAppMessaging/Runtime/FIRIAMRuntimeManager.m

@@ -0,0 +1,431 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMAnalyticsEventLoggerImpl.h"
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMClearcutHttpRequestSender.h"
+#import "FIRIAMClearcutLogStorage.h"
+#import "FIRIAMClearcutLogger.h"
+#import "FIRIAMClearcutUploader.h"
+#import "FIRIAMClientInfoFetcher.h"
+#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
+#import "FIRIAMDisplayCheckOnAppForegroundFlow.h"
+#import "FIRIAMDisplayCheckOnFetchDoneNotificationFlow.h"
+#import "FIRIAMDisplayExecutor.h"
+#import "FIRIAMFetchOnAppForegroundFlow.h"
+#import "FIRIAMFetchResponseParser.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMMsgFetcherUsingRestful.h"
+#import "FIRIAMRuntimeManager.h"
+#import "FIRIAMSDKModeManager.h"
+#import "FIRInAppMessaging.h"
+
+@interface FIRInAppMessaging ()
+@property(nonatomic, readwrite, strong) id<FIRAnalyticsInterop> _Nullable analytics;
+@end
+
+// A enum indicating 3 different possiblities of a setting about auto data collection.
+typedef NS_ENUM(NSInteger, FIRIAMAutoDataCollectionSetting) {
+  // This indicates that the config is not explicitly set.
+  FIRIAMAutoDataCollectionSettingNone = 0,
+
+  // This indicates that the setting explicitly enables the auto data collection.
+  FIRIAMAutoDataCollectionSettingEnabled = 1,
+
+  // This indicates that the setting explicitly disables the auto data collection.
+  FIRIAMAutoDataCollectionSettingDisabled = 2,
+};
+
+@interface FIRIAMRuntimeManager () <FIRIAMTestingModeListener>
+@property(nonatomic, nonnull) FIRIAMMsgFetcherUsingRestful *restfulFetcher;
+@property(nonatomic, nonnull) FIRIAMDisplayCheckOnAppForegroundFlow *displayOnAppForegroundFlow;
+@property(nonatomic, nonnull) FIRIAMDisplayCheckOnFetchDoneNotificationFlow *displayOnFetchDoneFlow;
+@property(nonatomic, nonnull)
+    FIRIAMDisplayCheckOnAnalyticEventsFlow *displayOnFIRAnalyticEventsFlow;
+
+@property(nonatomic, nonnull) FIRIAMFetchOnAppForegroundFlow *fetchOnAppForegroundFlow;
+@property(nonatomic, nonnull) FIRIAMClientInfoFetcher *clientInfoFetcher;
+@property(nonatomic, nonnull) FIRIAMFetchResponseParser *responseParser;
+@end
+
+static NSString *const _userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting =
+    @"firebase-iam-sdk-auto-data-collection";
+
+@implementation FIRIAMRuntimeManager {
+  // since we allow the SDK feature to be disabled/enabled at runtime, we need a field to track
+  // its state on this
+  BOOL _running;
+}
++ (FIRIAMRuntimeManager *)getSDKRuntimeInstance {
+  static FIRIAMRuntimeManager *managerInstance = nil;
+  static dispatch_once_t onceToken;
+
+  dispatch_once(&onceToken, ^{
+    managerInstance = [[FIRIAMRuntimeManager alloc] init];
+  });
+
+  return managerInstance;
+}
+
+// For protocol FIRIAMTestingModeListener.
+- (void)testingModeSwitchedOn {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180015",
+              @"Dynamically switch to the display flow for testing mode instance.");
+
+  [self.displayOnAppForegroundFlow stop];
+  [self.displayOnFetchDoneFlow start];
+}
+
+- (FIRIAMAutoDataCollectionSetting)FIAMProgrammaticAutoDataCollectionSetting {
+  id settingEntry = [[NSUserDefaults standardUserDefaults]
+      objectForKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting];
+
+  if (![settingEntry isKindOfClass:[NSNumber class]]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180014",
+                @"No auto data collection enable setting entry detected."
+                 "So no FIAM programmatic setting from the app.");
+    return FIRIAMAutoDataCollectionSettingNone;
+  } else {
+    if ([(NSNumber *)settingEntry boolValue]) {
+      return FIRIAMAutoDataCollectionSettingEnabled;
+    } else {
+      return FIRIAMAutoDataCollectionSettingDisabled;
+    }
+  }
+}
+
+// the key for the plist entry to suppress auto start
+static NSString *const kFirebaseInAppMessagingAutoDataCollectionKey =
+    @"FirebaseInAppMessagingAutomaticDataCollectionEnabled";
+
+- (FIRIAMAutoDataCollectionSetting)FIAMPlistAutoDataCollectionSetting {
+  id fiamAutoDataCollectionPlistEntry = [[NSBundle mainBundle]
+      objectForInfoDictionaryKey:kFirebaseInAppMessagingAutoDataCollectionKey];
+
+  if ([fiamAutoDataCollectionPlistEntry isKindOfClass:[NSNumber class]]) {
+    BOOL fiamDataCollectionEnabledPlistSetting =
+        [(NSNumber *)fiamAutoDataCollectionPlistEntry boolValue];
+
+    if (fiamDataCollectionEnabledPlistSetting) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180011",
+                  @"Auto data collection is explicitly enabled in FIAM plist entry.");
+      return FIRIAMAutoDataCollectionSettingEnabled;
+    } else {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180012",
+                  @"Auto data collection is explicitly disabled in FIAM plist entry.");
+      return FIRIAMAutoDataCollectionSettingDisabled;
+    }
+  } else {
+    return FIRIAMAutoDataCollectionSettingNone;
+  }
+}
+
+// Whether data collection is enabled by FIAM programmatic flag.
+- (BOOL)automaticDataCollectionEnabled {
+  return
+      [self FIAMProgrammaticAutoDataCollectionSetting] != FIRIAMAutoDataCollectionSettingDisabled;
+}
+
+// Sets FIAM's programmatic flag for auto data collection.
+- (void)setAutomaticDataCollectionEnabled:(BOOL)automaticDataCollectionEnabled {
+  if (automaticDataCollectionEnabled) {
+    [self resume];
+  } else {
+    [self pause];
+  }
+}
+
+- (BOOL)shouldRunSDKFlowsOnStartup {
+  // This can be controlled at 3 different levels in decsending priority. If a higher-priority
+  // setting exists, the lower level settings are ignored.
+  //   1. Setting made by the app by setting FIAM SDK's automaticDataCollectionEnabled flag.
+  //   2. FIAM specific data collection setting in plist file.
+  //   3. Global Firebase auto data collecting setting (carried over by currentSetting property).
+
+  FIRIAMAutoDataCollectionSetting programmaticSetting =
+      [self FIAMProgrammaticAutoDataCollectionSetting];
+
+  if (programmaticSetting == FIRIAMAutoDataCollectionSettingEnabled) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180010",
+                @"FIAM auto data-collection is explicitly enabled, start SDK flows.");
+    return true;
+  } else if (programmaticSetting == FIRIAMAutoDataCollectionSettingDisabled) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180013",
+                @"FIAM auto data-collection is explicitly disabled, do not start SDK flows.");
+    return false;
+  } else {
+    // No explicit setting from fiam's programmatic setting. Checking next level down.
+    FIRIAMAutoDataCollectionSetting fiamPlistDataCollectionSetting =
+        [self FIAMPlistAutoDataCollectionSetting];
+
+    if (fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingNone) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180018",
+                  @"No programmatic or plist setting at FIAM level. Fallback to global Firebase "
+                   "level setting.");
+      return self.currentSetting.isFirebaseAutoDataCollectionEnabled;
+    } else {
+      return fiamPlistDataCollectionSetting == FIRIAMAutoDataCollectionSettingEnabled;
+    }
+  }
+}
+
+- (void)resume {
+  // persist the setting
+  [[NSUserDefaults standardUserDefaults]
+      setObject:@(YES)
+         forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting];
+
+  @synchronized(self) {
+    if (!_running) {
+      [self.fetchOnAppForegroundFlow start];
+      [self.displayOnAppForegroundFlow start];
+      [self.displayOnFIRAnalyticEventsFlow start];
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180019",
+                  @"Start Firebase In-App Messaging flows from inactive.");
+      _running = YES;
+    } else {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180004",
+                    @"Runtime is already active, resume is just a no-op");
+    }
+  }
+}
+
+- (void)pause {
+  // persist the setting
+  [[NSUserDefaults standardUserDefaults]
+      setObject:@(NO)
+         forKey:_userDefaultsKeyForFIAMProgammaticAutoDataCollectionSetting];
+
+  @synchronized(self) {
+    if (_running) {
+      [self.fetchOnAppForegroundFlow stop];
+      [self.displayOnAppForegroundFlow stop];
+      [self.displayOnFIRAnalyticEventsFlow stop];
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180006",
+                  @"Shutdown Firebase In-App Messaging flows.");
+      _running = NO;
+    } else {
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM180005",
+                    @"No runtime active yet, pause is just a no-op");
+    }
+  }
+}
+
+- (void)setShouldSuppressMessageDisplay:(BOOL)shouldSuppress {
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180003", @"Message display suppress set to %@",
+              @(shouldSuppress));
+  self.displayExecutor.suppressMessageDisplay = shouldSuppress;
+}
+
+- (void)startRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings {
+  dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul), ^{
+    [self internalStartRuntimeWithSDKSettings:settings];
+  });
+}
+
+- (void)internalStartRuntimeWithSDKSettings:(FIRIAMSDKSettings *)settings {
+  if (_running) {
+    // Runtime has been started previously. Stop all the flows first.
+    [self.fetchOnAppForegroundFlow stop];
+    [self.displayOnAppForegroundFlow stop];
+    [self.displayOnFIRAnalyticEventsFlow stop];
+  }
+
+  self.currentSetting = settings;
+
+  FIRIAMTimerWithNSDate *timeFetcher = [[FIRIAMTimerWithNSDate alloc] init];
+  NSTimeInterval start = [timeFetcher currentTimestampInSeconds];
+
+  self.activityLogger =
+      [[FIRIAMActivityLogger alloc] initWithMaxCountBeforeReduce:settings.loggerMaxCountBeforeReduce
+                                             withSizeAfterReduce:settings.loggerSizeAfterReduce
+                                                     verboseMode:settings.loggerInVerboseMode
+                                                   loadFromCache:YES];
+
+  self.responseParser = [[FIRIAMFetchResponseParser alloc] initWithTimeFetcher:timeFetcher];
+
+  self.bookKeeper = [[FIRIAMBookKeeperViaUserDefaults alloc]
+      initWithUserDefaults:[NSUserDefaults standardUserDefaults]];
+
+  self.messageCache = [[FIRIAMMessageClientCache alloc] initWithBookkeeper:self.bookKeeper
+                                                       usingResponseParser:self.responseParser];
+  self.fetchResultStorage = [[FIRIAMServerMsgFetchStorage alloc] init];
+  self.clientInfoFetcher = [[FIRIAMClientInfoFetcher alloc] init];
+
+  self.restfulFetcher =
+      [[FIRIAMMsgFetcherUsingRestful alloc] initWithHost:settings.apiServerHost
+                                            HTTPProtocol:settings.apiHttpProtocol
+                                                 project:settings.firebaseProjectNumber
+                                             firebaseApp:settings.firebaseAppId
+                                                  APIKey:settings.apiKey
+                                            fetchStorage:self.fetchResultStorage
+                                       instanceIDFetcher:self.clientInfoFetcher
+                                         usingURLSession:nil
+                                          responseParser:self.responseParser];
+
+  // start fetch on app foreground flow
+  FIRIAMFetchSetting *fetchSetting = [[FIRIAMFetchSetting alloc] init];
+  fetchSetting.fetchMinIntervalInMinutes = settings.fetchMinIntervalInMinutes;
+
+  // start render on app foreground flow
+  FIRIAMDisplaySetting *appForegroundDisplaysetting = [[FIRIAMDisplaySetting alloc] init];
+  appForegroundDisplaysetting.displayMinIntervalInMinutes =
+      settings.appFGRenderMinIntervalInMinutes;
+
+  // clearcut log expires after 14 days: give up on attempting to deliver them any more
+  NSInteger ctLogExpiresInSeconds = 14 * 24 * 60 * 60;
+
+  FIRIAMClearcutLogStorage *ctLogStorage =
+      [[FIRIAMClearcutLogStorage alloc] initWithExpireAfterInSeconds:ctLogExpiresInSeconds
+                                                     withTimeFetcher:timeFetcher];
+
+  FIRIAMClearcutHttpRequestSender *clearcutRequestSender = [[FIRIAMClearcutHttpRequestSender alloc]
+      initWithClearcutHost:settings.clearcutServerHost
+          usingTimeFetcher:timeFetcher
+        withOSMajorVersion:[self.clientInfoFetcher getOSMajorVersion]];
+
+  FIRIAMClearcutUploader *ctUploader =
+      [[FIRIAMClearcutUploader alloc] initWithRequestSender:clearcutRequestSender
+                                                timeFetcher:timeFetcher
+                                                 logStorage:ctLogStorage
+                                              usingStrategy:settings.clearcutStrategy
+                                          usingUserDefaults:nil];
+
+  FIRIAMClearcutLogger *clearcutLogger =
+      [[FIRIAMClearcutLogger alloc] initWithFBProjectNumber:settings.firebaseProjectNumber
+                                                    fbAppId:settings.firebaseAppId
+                                          clientInfoFetcher:self.clientInfoFetcher
+                                           usingTimeFetcher:timeFetcher
+                                              usingUploader:ctUploader];
+
+  FIRIAMAnalyticsEventLoggerImpl *analyticsEventLogger = [[FIRIAMAnalyticsEventLoggerImpl alloc]
+      initWithClearcutLogger:clearcutLogger
+            usingTimeFetcher:timeFetcher
+           usingUserDefaults:nil
+                   analytics:[FIRInAppMessaging inAppMessaging].analytics];
+
+  FIRIAMSDKModeManager *sdkModeManager =
+      [[FIRIAMSDKModeManager alloc] initWithUserDefaults:NSUserDefaults.standardUserDefaults
+                                     testingModeListener:self];
+
+  self.fetchOnAppForegroundFlow =
+      [[FIRIAMFetchOnAppForegroundFlow alloc] initWithSetting:fetchSetting
+                                                 messageCache:self.messageCache
+                                               messageFetcher:self.restfulFetcher
+                                                  timeFetcher:timeFetcher
+                                                   bookKeeper:self.bookKeeper
+                                               activityLogger:self.activityLogger
+                                         analyticsEventLogger:analyticsEventLogger
+                                         FIRIAMSDKModeManager:sdkModeManager];
+
+  FIRIAMActionURLFollower *actionFollower = [FIRIAMActionURLFollower actionURLFollower];
+
+  self.displayExecutor = [[FIRIAMDisplayExecutor alloc] initWithSetting:appForegroundDisplaysetting
+                                                           messageCache:self.messageCache
+                                                            timeFetcher:timeFetcher
+                                                             bookKeeper:self.bookKeeper
+                                                      actionURLFollower:actionFollower
+                                                         activityLogger:self.activityLogger
+                                                   analyticsEventLogger:analyticsEventLogger];
+
+  // Setting the display component. It's needed in case headless SDK is initialized after
+  // the display component is already set on FIRInAppMessaging.
+  self.displayExecutor.messageDisplayComponent =
+      FIRInAppMessaging.inAppMessaging.messageDisplayComponent;
+
+  // Both display flows are created on startup. But they would only be turned on (started) based on
+  // the sdk mode for the current instance
+  self.displayOnFetchDoneFlow = [[FIRIAMDisplayCheckOnFetchDoneNotificationFlow alloc]
+      initWithDisplayFlow:self.displayExecutor];
+  self.displayOnAppForegroundFlow =
+      [[FIRIAMDisplayCheckOnAppForegroundFlow alloc] initWithDisplayFlow:self.displayExecutor];
+
+  self.displayOnFIRAnalyticEventsFlow =
+      [[FIRIAMDisplayCheckOnAnalyticEventsFlow alloc] initWithDisplayFlow:self.displayExecutor];
+
+  self.messageCache.analycisEventDislayCheckFlow = self.displayOnFIRAnalyticEventsFlow;
+  [self.messageCache
+      loadMessageDataFromServerFetchStorage:self.fetchResultStorage
+                             withCompletion:^(BOOL success) {
+                               // start flows regardless whether we can load messages from fetch
+                               // storage successfully
+                               FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180001",
+                                           @"Message loading from fetch storage was done.");
+
+                               if ([self shouldRunSDKFlowsOnStartup]) {
+                                 FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180008",
+                                             @"Start SDK runtime components.");
+
+                                 [self.clientInfoFetcher
+                                     fetchFirebaseIIDDataWithProjectNumber:
+                                         self.currentSetting.firebaseProjectNumber
+                                                            withCompletion:^(
+                                                                NSString *_Nullable iid,
+                                                                NSString *_Nullable token,
+                                                                NSError *_Nullable error) {
+                                                              // Always dump the instance id into
+                                                              // log on startup to help developers
+                                                              // to find it for their app instance.
+                                                              FIRLogDebug(kFIRLoggerInAppMessaging,
+                                                                          @"I-IAM180017",
+                                                                          @"Starting "
+                                                                          @"InAppMessaging runtime "
+                                                                          @"with "
+                                                                           "Instance ID %@",
+                                                                          iid);
+                                                            }];
+
+                                 [self.fetchOnAppForegroundFlow start];
+                                 [self.displayOnFIRAnalyticEventsFlow start];
+
+                                 self->_running = YES;
+
+                                 if (sdkModeManager.currentMode == FIRIAMSDKModeTesting) {
+                                   FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180007",
+                                               @"InAppMessaging testing mode enabled. App "
+                                                "foreground messages will be displayed following "
+                                                "fetch");
+                                   [self.displayOnFetchDoneFlow start];
+                                 } else {
+                                   FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180020",
+                                               @"Start regular display flow for non-testing "
+                                                "instance mode");
+                                   [self.displayOnAppForegroundFlow start];
+
+                                   // Simulate app going into foreground on startup
+                                   [self.displayExecutor checkAndDisplayNextAppForegroundMessage];
+                                 }
+
+                                 // One-time triggering of checks for both fetch flow
+                                 // upon SDK/app startup.
+                                 [self.fetchOnAppForegroundFlow checkAndFetch];
+                               } else {
+                                 FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180009",
+                                             @"No FIAM SDK startup due to settings.");
+                               }
+                             }];
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM180002",
+              @"Firebase In-App Messaging SDK version %@ finished startup in %lf seconds "
+               "with these settings: %@",
+              [self.clientInfoFetcher getIAMSDKVersion],
+              (double)([timeFetcher currentTimestampInSeconds] - start), settings);
+}
+@end

+ 73 - 0
Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.h

@@ -0,0 +1,73 @@
+/*
+ * 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/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode;
+
+/**
+ * At runtime a FIAM SDK client can function in one of the following modes:
+ *  1 Regular. This SDK client instance will conform to regular fetch minimal interval time policy.
+ *  2 Newly installed. This is a mode a newly installed SDK stays in until the first
+ *    kFIRIAMMaxFetchInNewlyInstalledMode fetches have finished. In this mode, there is no
+ *    minimal time interval between fetches: a fetch would be triggered as long as the app goes
+ *    into foreground state.
+ *  3 Testing Instance. This app instance is targeted for test on device feature for fiam. When
+ *    it's in this mode, no minimal time interval between fetches is applied. SDK turns itself
+ *    into this mode on seeing test-on-client messages are returned in fetch responses.
+ */
+
+typedef NS_ENUM(NSInteger, FIRIAMSDKMode) {
+  FIRIAMSDKModeRegular,
+  FIRIAMSDKModeTesting,
+  FIRIAMSDKModeNewlyInstalled
+};
+
+// turn the sdk mode enum integer value into a descriptive string
+NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode);
+
+extern NSString *const kFIRIAMUserDefaultKeyForSDKMode;
+extern NSString *const kFIRIAMUserDefaultKeyForServerFetchCount;
+extern NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode;
+
+@protocol FIRIAMTestingModeListener <NSObject>
+// Triggered when the current app switches into testing mode from a using testing mode
+- (void)testingModeSwitchedOn;
+@end
+
+// A class for tracking and updating the SDK mode. The tracked mode related info is persisted
+// so that it can be restored beyond app restarts
+@interface FIRIAMSDKModeManager : NSObject
+
+- (instancetype)init NS_UNAVAILABLE;
+
+// having NSUserDefaults as passed-in to help with unit testing
+- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults
+                 testingModeListener:(id<FIRIAMTestingModeListener>)testingModeListener;
+
+// returns the current SDK mode
+- (FIRIAMSDKMode)currentMode;
+
+// turn the current SDK into 'Testing Instance' mode.
+- (void)becomeTestingInstance;
+// inform the manager that one more fetch is done. This is to allow
+// the manager to potentially graduate from the newly installed mode.
+- (void)registerOneMoreFetch;
+
+@end
+NS_ASSUME_NONNULL_END

+ 113 - 0
Firebase/InAppMessaging/Runtime/FIRIAMSDKModeManager.m

@@ -0,0 +1,113 @@
+/*
+ * 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 <FirebaseCore/FIRLogger.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMSDKModeManager.h"
+
+NSString *FIRIAMDescriptonStringForSDKMode(FIRIAMSDKMode mode) {
+  switch (mode) {
+    case FIRIAMSDKModeTesting:
+      return @"Testing Instance";
+    case FIRIAMSDKModeRegular:
+      return @"Regular";
+    case FIRIAMSDKModeNewlyInstalled:
+      return @"Newly Installed";
+    default:
+      FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM290003", @"Unknown sdk mode value %d",
+                    (int)mode);
+      return @"Unknown";
+  }
+}
+
+@interface FIRIAMSDKModeManager ()
+@property(nonatomic, nonnull, readonly) NSUserDefaults *userDefaults;
+// Make it weak so that we don't depend on its existence to avoid circular reference.
+@property(nonatomic, readonly, weak) id<FIRIAMTestingModeListener> testingModeListener;
+@end
+
+NSString *const kFIRIAMUserDefaultKeyForSDKMode = @"firebase-iam-sdk-mode";
+NSString *const kFIRIAMUserDefaultKeyForServerFetchCount = @"firebase-iam-server-fetch-count";
+NSInteger const kFIRIAMMaxFetchInNewlyInstalledMode = 5;
+
+@implementation FIRIAMSDKModeManager {
+  FIRIAMSDKMode _sdkMode;
+  NSInteger _fetchCount;
+}
+
+- (instancetype)initWithUserDefaults:(NSUserDefaults *)userDefaults
+                 testingModeListener:(id<FIRIAMTestingModeListener>)testingModeListener {
+  if (self = [super init]) {
+    _userDefaults = userDefaults;
+    _testingModeListener = testingModeListener;
+
+    id modeEntry = [_userDefaults objectForKey:kFIRIAMUserDefaultKeyForSDKMode];
+    if (modeEntry == nil) {
+      // no entry yet, it's a newly installed sdk instance
+      _sdkMode = FIRIAMSDKModeNewlyInstalled;
+
+      // initialize the mode and fetch count in the persistent storage
+      [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode]
+                        forKey:kFIRIAMUserDefaultKeyForSDKMode];
+      [_userDefaults setInteger:0 forKey:kFIRIAMUserDefaultKeyForServerFetchCount];
+    } else {
+      _sdkMode = [(NSNumber *)modeEntry integerValue];
+      _fetchCount = [_userDefaults integerForKey:kFIRIAMUserDefaultKeyForServerFetchCount];
+    }
+
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290001",
+                @"SDK is in mode of %@ and has seen %d fetches.",
+                FIRIAMDescriptonStringForSDKMode(_sdkMode), (int)_fetchCount);
+  }
+  return self;
+}
+
+// inform the manager that one more fetch is done. This is to allow
+// the manager to potentially graduate from the newly installed mode.
+- (void)registerOneMoreFetch {
+  // we only care about the fetch count when sdk is in newly installed mode (so that it may
+  // graduate from that after certain number of fetches).
+  if (_sdkMode == FIRIAMSDKModeNewlyInstalled) {
+    if (++_fetchCount >= kFIRIAMMaxFetchInNewlyInstalledMode) {
+      FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290002",
+                  @"Coming out of newly installed mode since there have been %d fetches",
+                  (int)_fetchCount);
+
+      _sdkMode = FIRIAMSDKModeRegular;
+      [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode]
+                        forKey:kFIRIAMUserDefaultKeyForSDKMode];
+    } else {
+      [_userDefaults setInteger:_fetchCount forKey:kFIRIAMUserDefaultKeyForServerFetchCount];
+    }
+  }
+}
+
+- (void)becomeTestingInstance {
+  _sdkMode = FIRIAMSDKModeTesting;
+  [_userDefaults setObject:[NSNumber numberWithInt:_sdkMode]
+                    forKey:kFIRIAMUserDefaultKeyForSDKMode];
+
+  FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM290004",
+              @"Test mode enabled, notifying test mode listener.");
+  [self.testingModeListener testingModeSwitchedOn];
+}
+
+// returns the current SDK mode
+- (FIRIAMSDKMode)currentMode {
+  return _sdkMode;
+}
+@end

+ 25 - 0
Firebase/InAppMessaging/Runtime/FIRIAMSDKRuntimeErrorCodes.h

@@ -0,0 +1,25 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FIRIAMSDKRuntimeError) {
+  // fail to crawl the image url
+  FIRIAMSDKRuntimeErrorImageNotFetchable = 0,
+
+  // crawling image url sees non-image type data being returned
+  FIRIAMSDKRuntimeErrorNonImageMimetypeFromImageURL = 1
+};

+ 53 - 0
Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.h

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+@class FIRIAMClearcutStrategy;
+
+NS_ASSUME_NONNULL_BEGIN
+@interface FIRIAMSDKSettings : NSObject
+// settings related to communicating with in-app messaging server
+@property(nonatomic, copy) NSString *firebaseProjectNumber;
+@property(nonatomic, copy) NSString *firebaseAppId;
+@property(nonatomic, copy) NSString *apiKey;
+@property(nonatomic, copy) NSString *apiServerHost;
+@property(nonatomic, copy) NSString *apiHttpProtocol;  // http or https. It should be always
+                                                       // https on production. Allow http to
+                                                       // faciliate testing in non-prod environment
+@property(nonatomic) NSTimeInterval fetchMinIntervalInMinutes;
+
+// settings related to activity logger
+@property(nonatomic) NSInteger loggerMaxCountBeforeReduce;
+@property(nonatomic) NSInteger loggerSizeAfterReduce;
+@property(nonatomic) BOOL loggerInVerboseMode;
+
+// settings for controlling rendering frequency for messages rendered from app foreground triggers
+@property(nonatomic) NSTimeInterval appFGRenderMinIntervalInMinutes;
+
+// host name for clearcut servers
+@property(nonatomic, copy) NSString *clearcutServerHost;
+// clearcut strategy
+@property(nonatomic, strong) FIRIAMClearcutStrategy *clearcutStrategy;
+
+// The global flag at whole Firebase level for automatic data collection. On FIAM SDK startup,
+// it would be retreived from FIRApp's corresponding setting.
+@property(nonatomic, getter=isFirebaseAutoDataCollectionEnabled)
+    BOOL firebaseAutoDataCollectionEnabled;
+
+- (NSString *)description;
+@end
+NS_ASSUME_NONNULL_END

+ 35 - 0
Firebase/InAppMessaging/Runtime/FIRIAMSDKSettings.m

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMSDKSettings.h"
+
+@implementation FIRIAMSDKSettings
+
+- (NSString *)description {
+  return
+      [NSString stringWithFormat:@"APIServer:%@;ProjectNumber:%@; API_Key:%@;Clearcut Server:%@; "
+                                  "Fetch Minimal Interval:%lu seconds; Activity Logger Max:%lu; "
+                                  "Foreground Display Trigger Minimal Interval:%lu seconds;\n"
+                                  "Clearcut strategy:%@;Global Firebase auto data collection %@\n",
+                                 self.apiServerHost, self.firebaseProjectNumber, self.apiKey,
+                                 self.clearcutServerHost,
+                                 (unsigned long)(self.fetchMinIntervalInMinutes * 60),
+                                 (unsigned long)self.loggerMaxCountBeforeReduce,
+                                 (unsigned long)(self.appFGRenderMinIntervalInMinutes * 60),
+                                 self.clearcutStrategy,
+                                 self.firebaseAutoDataCollectionEnabled ? @"enabled" : @"disabled"];
+}
+@end

+ 33 - 0
Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMSDKSettings.h"
+#import "FIRInAppMessaging.h"
+
+/**
+ *  This category extends FIRInAppMessaging with the configurations from FIRApp
+ */
+@interface FIRInAppMessaging (Bootstrap)
+
++ (NSString *)getFiamServerHost;
++ (void)setFiamServerHostWithName:(NSString *)serverHost;
+
++ (NSString *)getServer;
+
++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings;
+
++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app;
+@end

+ 137 - 0
Firebase/InAppMessaging/Runtime/FIRInAppMessaging+Bootstrap.m

@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRInAppMessaging+Bootstrap.h"
+
+#import <FirebaseAnalyticsInterop/FIRAnalyticsInterop.h>
+#import <FirebaseCore/FIRAppInternal.h>
+#import <FirebaseCore/FIRLogger.h>
+#import <FirebaseCore/FIROptionsInternal.h>
+#import <GoogleUtilities/GULAppEnvironmentUtil.h>
+
+#import "FIRCore+InAppMessaging.h"
+#import "FIRIAMClearcutUploader.h"
+#import "FIRIAMRuntimeManager.h"
+#import "FIRIAMSDKSettings.h"
+#import "FIROptionsInternal.h"
+#import "NSString+FIRInterlaceStrings.h"
+
+@implementation FIRInAppMessaging (Bootstrap)
+
+static FIRIAMSDKSettings *_sdkSetting = nil;
+
+static NSString *_fiamServerHostName = @"firebaseinappmessaging.googleapis.com";
+
++ (NSString *)getFiamServerHost {
+  return _fiamServerHostName;
+}
+
++ (void)setFiamServerHostWithName:(NSString *)serverHost {
+  _fiamServerHostName = serverHost;
+}
+
++ (NSString *)getServer {
+  // Override to change to test server.
+  NSString *serverHostNameFirstComponent = @"pa.ogepscm";
+  NSString *serverHostNameSecondComponent = @"lygolai.o";
+  return [NSString fir_interlaceString:serverHostNameFirstComponent
+                            withString:serverHostNameSecondComponent];
+}
+
++ (void)bootstrapIAMFromFIRApp:(FIRApp *)app {
+  FIROptions *options = app.options;
+  NSError *error;
+
+  if (!options.GCMSenderID.length) {
+    error =
+        [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                            code:0
+                        userInfo:@{
+                          NSLocalizedDescriptionKey : @"Google Sender ID must not be nil or empty."
+                        }];
+
+    [self exitAppWithFatalError:error];
+  }
+
+  if (!options.APIKey.length) {
+    error = [NSError
+        errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                   code:0
+               userInfo:@{NSLocalizedDescriptionKey : @"API key must not be nil or empty."}];
+
+    [self exitAppWithFatalError:error];
+  }
+
+  if (!options.googleAppID.length) {
+    error =
+        [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
+                            code:0
+                        userInfo:@{NSLocalizedDescriptionKey : @"Google App ID must not be nil."}];
+    [self exitAppWithFatalError:error];
+  }
+
+  // following are the default sdk settings to be used by hosting app
+  _sdkSetting = [[FIRIAMSDKSettings alloc] init];
+  _sdkSetting.apiServerHost = [FIRInAppMessaging getFiamServerHost];
+  _sdkSetting.clearcutServerHost = [FIRInAppMessaging getServer];
+  _sdkSetting.apiHttpProtocol = @"https";
+  _sdkSetting.firebaseAppId = options.googleAppID;
+  _sdkSetting.firebaseProjectNumber = options.GCMSenderID;
+  _sdkSetting.apiKey = options.APIKey;
+  _sdkSetting.fetchMinIntervalInMinutes = 24 * 60;  // fetch at most once every 24 hours
+  _sdkSetting.loggerMaxCountBeforeReduce = 100;
+  _sdkSetting.loggerSizeAfterReduce = 50;
+  _sdkSetting.appFGRenderMinIntervalInMinutes = 24 * 60;  // render at most one message from
+                                                          // app-foreground trigger every 24 hours
+  _sdkSetting.loggerInVerboseMode = NO;
+
+  // TODO: once Firebase Core supports sending notifications at global Firebase level setting
+  // change, FIAM SDK would listen to it and respond to it. Until then, FIAM SDK only checks
+  // the setting once upon App/SDK startup.
+  _sdkSetting.firebaseAutoDataCollectionEnabled = app.isDataCollectionDefaultEnabled;
+
+  if ([GULAppEnvironmentUtil isSimulator]) {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170004",
+                @"Running in simulator. Do realtime clearcut uploading.");
+    _sdkSetting.clearcutStrategy =
+        [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:0
+                                                maxWaitTimeInMills:0
+                                         failureBackoffTimeInMills:60 * 60 * 1000  // 60 mins
+                                                     batchSendSize:50];
+  } else {
+    FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM170005",
+                @"Not running in simulator. Use regular clearcut uploading strategy.");
+    _sdkSetting.clearcutStrategy =
+        [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 60 * 1000        // 5 mins
+                                                maxWaitTimeInMills:12 * 60 * 60 * 1000  // 12 hours
+                                         failureBackoffTimeInMills:60 * 60 * 1000       // 60 mins
+                                                     batchSendSize:50];
+  }
+
+  [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting];
+}
+
++ (void)bootstrapIAMWithSettings:(FIRIAMSDKSettings *)settings {
+  _sdkSetting = settings;
+  [[FIRIAMRuntimeManager getSDKRuntimeInstance] startRuntimeWithSDKSettings:_sdkSetting];
+}
+
++ (void)exitAppWithFatalError:(NSError *)error {
+  [NSException raise:kFirebaseInAppMessagingErrorDomain
+              format:@"Error happened %@", error.localizedDescription];
+}
+
+@end

+ 29 - 0
Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import "FIRIAMTimeFetcher.h"
+
+NS_ASSUME_NONNULL_BEGIN
+// A class via which we can track elapsed time with the capability to pause and resume
+// the tracking
+@interface FIRIAMElapsedTimeTracker : NSObject
+- (NSTimeInterval)trackedTimeSoFar;
+- (void)pause;
+- (void)resume;
+- (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher;
+@end
+NS_ASSUME_NONNULL_END

+ 56 - 0
Firebase/InAppMessaging/Util/FIRIAMElapsedTimeTracker.m

@@ -0,0 +1,56 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMElapsedTimeTracker.h"
+@interface FIRIAMElapsedTimeTracker ()
+@property(nonatomic) NSTimeInterval totalTrackedTimeSoFar;
+@property(nonatomic) NSTimeInterval lastTrackingStartPoint;
+@property(nonatomic, nonnull) id<FIRIAMTimeFetcher> timeFetcher;
+@property(nonatomic) BOOL tracking;
+@end
+
+@implementation FIRIAMElapsedTimeTracker
+
+- (NSTimeInterval)trackedTimeSoFar {
+  if (_tracking) {
+    return self.totalTrackedTimeSoFar + [self.timeFetcher currentTimestampInSeconds] -
+           self.lastTrackingStartPoint;
+  } else {
+    return self.totalTrackedTimeSoFar;
+  }
+}
+
+- (void)pause {
+  self.tracking = NO;
+  self.totalTrackedTimeSoFar +=
+      [self.timeFetcher currentTimestampInSeconds] - self.lastTrackingStartPoint;
+}
+
+- (void)resume {
+  self.tracking = YES;
+  self.lastTrackingStartPoint = [self.timeFetcher currentTimestampInSeconds];
+}
+
+- (instancetype)initWithTimeFetcher:(id<FIRIAMTimeFetcher>)timeFetcher {
+  if (self = [super init]) {
+    _tracking = YES;
+    _timeFetcher = timeFetcher;
+    _totalTrackedTimeSoFar = 0;
+    _lastTrackingStartPoint = [timeFetcher currentTimestampInSeconds];
+  }
+  return self;
+}
+@end

+ 28 - 0
Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+
+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 FIRIAMTimeFetcher
+- (NSTimeInterval)currentTimestampInSeconds;
+@end
+
+@interface FIRIAMTimerWithNSDate : NSObject <FIRIAMTimeFetcher>
+@end
+NS_ASSUME_NONNULL_END

+ 23 - 0
Firebase/InAppMessaging/Util/FIRIAMTimeFetcher.m

@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "FIRIAMTimeFetcher.h"
+
+@implementation FIRIAMTimerWithNSDate
+- (NSTimeInterval)currentTimestampInSeconds {
+  return [[NSDate date] timeIntervalSince1970];
+}
+@end

+ 30 - 0
Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 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/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+// Extension on NSString that combines two strings.
+@interface NSString (FIRInterlaceStrings)
+
+// Returns a combined string created from iterating over both strings alternately,
+// beginning with stringOne's first character.
++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 42 - 0
Firebase/InAppMessaging/Util/NSString+FIRInterlaceStrings.m

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019 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 "NSString+FIRInterlaceStrings.h"
+
+@implementation NSString (InterlaceStrings)
+
++ (NSString *)fir_interlaceString:(NSString *)stringOne withString:(NSString *)stringTwo {
+  NSMutableString *interlacedString = [NSMutableString string];
+
+  NSUInteger count = MAX(stringOne.length, stringTwo.length);
+
+  for (NSUInteger i = 0; i < count; i++) {
+    if (i < stringOne.length) {
+      NSString *firstComponentChar =
+          [NSString stringWithFormat:@"%c", [stringOne characterAtIndex:i]];
+      [interlacedString appendString:firstComponentChar];
+    }
+    if (i < stringTwo.length) {
+      NSString *secondComponentChar =
+          [NSString stringWithFormat:@"%c", [stringTwo characterAtIndex:i]];
+      [interlacedString appendString:secondComponentChar];
+    }
+  }
+
+  return interlacedString;
+}
+
+@end

+ 31 - 0
Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+// Extension on UIColor to support conversion from a color hex string in the format
+// of #XXXXXX
+@interface UIColor (HexString)
+
+// Constructing UIColor object from a string with '#XXXXXX' format where 'XXXXXX' is
+// the 6-digit hex value string of the rgb color.
+//
+// @param hexString hex string for the color.
+// @return a UIColor parsed out of the hex string. Nil returned if the hexString is nil or does
+// not conform the desired format.
++ (nullable UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString;
+@end

+ 39 - 0
Firebase/InAppMessaging/Util/UIColor+FIRIAMHexString.m

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "UIColor+FIRIAMHexString.h"
+
+@implementation UIColor (HexString)
++ (UIColor *)firiam_colorWithHexString:(nullable NSString *)hexString {
+  if (hexString.length < 7) {
+    return nil;
+  }
+
+  unsigned rgbValue = 0;
+  NSScanner *scanner = [NSScanner scannerWithString:hexString];
+  [scanner setScanLocation:1];  // bypass '#' character
+
+  if (![scanner scanHexInt:&rgbValue]) {
+    // no valid heximal value is detected
+    return nil;
+  }
+
+  return [UIColor colorWithRed:((rgbValue & 0xFF0000) >> 16) / 255.0
+                         green:((rgbValue & 0xFF00) >> 8) / 255.0
+                          blue:(rgbValue & 0xFF) / 255.0
+                         alpha:1.0];
+}
+@end

BIN
Firebase/InAppMessaging/firebase_28dp.png


+ 39 - 0
FirebaseInAppMessaging.podspec

@@ -0,0 +1,39 @@
+Pod::Spec.new do |s|
+  s.name             = 'FirebaseInAppMessaging'
+  s.version          = '0.12.0'
+  s.summary          = 'Firebase In-App Messaging for iOS'
+
+  s.description      = <<-DESC
+FirebaseInAppMessaging is the headless component of Firebase In-App Messaging on iOS client side.
+See more product details at https://firebase.google.com/products/in-app-messaging/ about Firebase In-App Messaging.
+                       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/InAppMessaging/"
+  s.source_files = base_dir + '**/*.[mh]'
+  s.public_header_files = base_dir + 'Public/*.h'
+
+  s.pod_target_xcconfig = { 'GCC_PREPROCESSOR_DEFINITIONS' =>
+      '$(inherited) ' +
+      'FIRInAppMessaging_LIB_VERSION=' + String(s.version)
+  }
+
+  s.dependency 'FirebaseCore'
+  s.ios.dependency 'FirebaseAnalytics'
+  s.ios.dependency 'FirebaseAnalyticsInterop'
+  s.dependency 'FirebaseInstanceID'
+end

+ 36 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.h

@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "FIRIAMBookKeeper.h"
+#import "FIRIAMMsgFetcherUsingRestful.h"
+
+NS_ASSUME_NONNULL_BEGIN
+@interface AppDelegate : UIResponder <UIApplicationDelegate>
+@property(strong, nonatomic) UIWindow *window;
+@property(strong, nonatomic) FIRIAMActivityLogger *activityLogger;
+@property(nonatomic, nullable) FIRIAMBookKeeperViaUserDefaults *bookKeeper;
+
+@property(nonatomic, nullable) FIRIAMMsgFetcherUsingRestful *restfulFetcher;
+@property(nonatomic, copy) NSString *backendServer;
+@property(nonatomic, copy) NSString *projectId;
+@property(nonatomic, copy) NSString *apiKey;
+
+@property(nonatomic) NSInteger displayIntervalInSeconds;
+@property(nonatomic) NSInteger fetchIntervalInSeconds;
+@end
+NS_ASSUME_NONNULL_END

+ 118 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AppDelegate.m

@@ -0,0 +1,118 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "AppDelegate.h"
+#import "FIRIAMClearcutUploader.h"
+#import "FIRIAMRuntimeManager.h"
+#import "FIRInAppMessaging+Bootstrap.h"
+#import "NSString+FIRInterlaceStrings.h"
+
+#import <FirebaseCore/FirebaseCore.h>
+#import <FirebaseDynamicLinks/FirebaseDynamicLinks.h>
+
+@interface FIRInAppMessaging (Testing)
++ (void)disableAutoBootstrapWithFIRApp;
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  // Override point for customization after application launch.
+  NSLog(@"application started");
+
+  [FIRInAppMessaging disableAutoBootstrapWithFIRApp];
+  [FIROptions defaultOptions].deepLinkURLScheme = @"fiam-testing";
+  [FIRApp configure];
+
+  FIRIAMSDKSettings *sdkSetting = [[FIRIAMSDKSettings alloc] init];
+
+  sdkSetting.apiServerHost = @"firebaseinappmessaging.googleapis.com";
+
+  NSString *serverHostNameFirstComponent = @"pa.ogepscm";
+  NSString *serverHostNameSecondComponent = @"lygolai.o";
+
+  sdkSetting.clearcutServerHost = [NSString fir_interlaceString:serverHostNameFirstComponent
+                                                     withString:serverHostNameSecondComponent];
+  sdkSetting.apiHttpProtocol = @"https";
+  sdkSetting.fetchMinIntervalInMinutes = 0.1;  // ok to refetch every 6 seconds
+  sdkSetting.loggerMaxCountBeforeReduce = 800;
+  sdkSetting.loggerSizeAfterReduce = 600;
+  sdkSetting.appFGRenderMinIntervalInMinutes = 0.1;
+  sdkSetting.loggerInVerboseMode = YES;
+  sdkSetting.conversionTrackingExpiresInSeconds = 180;
+  sdkSetting.firebaseAutoDataCollectionEnabled = NO;
+
+  sdkSetting.clearcutStrategy =
+      [[FIRIAMClearcutStrategy alloc] initWithMinWaitTimeInMills:5 * 1000   // 5 seconds
+                                              maxWaitTimeInMills:30 * 1000  // 30 seconds
+                                       failureBackoffTimeInMills:60 * 1000  // 60 seconds
+                                                   batchSendSize:50];
+
+  [FIRInAppMessaging bootstrapIAMWithSettings:sdkSetting];
+  return YES;
+}
+
+- (BOOL)application:(UIApplication *)application
+    continueUserActivity:(NSUserActivity *)userActivity
+      restorationHandler:(void (^)(NSArray *))restorationHandler {
+  NSLog(@"handle page url %@", userActivity.webpageURL);
+  BOOL handled = [[FIRDynamicLinks dynamicLinks]
+      handleUniversalLink:userActivity.webpageURL
+               completion:^(FIRDynamicLink *_Nullable dynamicLink, NSError *_Nullable error) {
+                 if (dynamicLink) {
+                   NSLog(@"dynamic link recogized with url as %@", dynamicLink.url.absoluteString);
+                   [self showDeepLink:dynamicLink.url.absoluteString forUrlType:@"universal link"];
+                 } else {
+                   NSLog(@"error happened %@", error);
+                 }
+               }];
+  return handled;
+}
+
+- (void)showDeepLink:(NSString *)url forUrlType:(NSString *)urlType {
+  NSString *message = [NSString stringWithFormat:@"App wants to open a %@ : %@", urlType, url];
+  UIAlertController *alert =
+      [UIAlertController alertControllerWithTitle:@"Deep link recognized"
+                                          message:message
+                                   preferredStyle:UIAlertControllerStyleAlert];
+
+  UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:@"OK"
+                                                          style:UIAlertActionStyleDefault
+                                                        handler:^(UIAlertAction *action){
+                                                        }];
+
+  [alert addAction:defaultAction];
+  [UIApplication.sharedApplication.keyWindow.rootViewController presentViewController:alert
+                                                                             animated:YES
+                                                                           completion:nil];
+}
+
+- (BOOL)application:(UIApplication *)app
+            openURL:(NSURL *)url
+            options:(NSDictionary<NSString *, id> *)options {
+  return [self application:app openURL:url sourceApplication:@"source app" annotation:@{}];
+}
+
+- (BOOL)application:(UIApplication *)application
+              openURL:(NSURL *)url
+    sourceApplication:(NSString *)sourceApplication
+           annotation:(id)annotation {
+  NSLog(@"handle link with custom scheme: %@", url.absoluteString);
+  [self showDeepLink:url.absoluteString forUrlType:@"custom scheme url"];
+  return YES;
+}
+@end

+ 93 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,93 @@
+{
+  "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"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 20 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.h

@@ -0,0 +1,20 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#import <UIKit/UIKit.h>
+
+@interface AutoDisplayFlowViewController : UIViewController <UITextFieldDelegate>
+
+@end

+ 170 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayFlowViewController.m

@@ -0,0 +1,170 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "AppDelegate.h"
+
+#import "AutoDisplayFlowViewController.h"
+#import "AutoDisplayMesagesTableVC.h"
+
+#import "FIRIAMDisplayCheckOnAppForegroundFlow.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMMessageContentDataWithImageURL.h"
+#import "FIRIAMMessageDefinition.h"
+
+#import "FIRIAMActivityLogger.h"
+#import "FIRIAMDisplayCheckOnAnalyticEventsFlow.h"
+#import "FIRIAMFetchOnAppForegroundFlow.h"
+#import "FIRIAMMessageClientCache.h"
+#import "FIRIAMMsgFetcherUsingRestful.h"
+
+#import "FIRIAMRuntimeManager.h"
+#import "FIRInAppMessaging.h"
+
+#import <FirebaseAnalytics/FIRAnalytics.h>
+
+@interface AutoDisplayFlowViewController ()
+@property(weak, nonatomic) IBOutlet UISwitch *autoDisplayFlowSwitch;
+
+@property(nonatomic, weak) AutoDisplayMesagesTableVC *messageTableVC;
+@property(weak, nonatomic) IBOutlet UITextField *autoDisplayIntervalText;
+@property(weak, nonatomic) IBOutlet UITextField *autoFetchIntervalText;
+@property(weak, nonatomic) IBOutlet UITextField *eventNameText;
+@property(nonatomic) FIRIAMRuntimeManager *sdkRuntime;
+@property(weak, nonatomic) IBOutlet UIButton *disableEnableSDKBtn;
+@property(weak, nonatomic) IBOutlet UIButton *changeDataCollectionBtn;
+@end
+
+@implementation AutoDisplayFlowViewController
+- (IBAction)clearClientStorage:(id)sender {
+  [self.sdkRuntime.fetchResultStorage
+      saveResponseDictionary:@{}
+              withCompletion:^(BOOL success) {
+                [self.sdkRuntime.messageCache
+                    loadMessageDataFromServerFetchStorage:self.sdkRuntime.fetchResultStorage
+                                           withCompletion:^(BOOL success) {
+                                             NSLog(@"load from storage result is %d", success);
+                                           }];
+              }];
+}
+- (IBAction)disableEnableClicked:(id)sender {
+  FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging];
+  sdk.messageDisplaySuppressed = !sdk.messageDisplaySuppressed;
+  [self setupDisableEnableButtonLabel];
+}
+
+- (void)setupDisableEnableButtonLabel {
+  FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging];
+  NSString *title = sdk.messageDisplaySuppressed ? @"allow rendering" : @"disallow rendering";
+  [self.disableEnableSDKBtn setTitle:title forState:UIControlStateNormal];
+}
+
+- (IBAction)triggerAnalyticEventTapped:(id)sender {
+  NSLog(@"triggering an analytics event: %@", self.eventNameText.text);
+
+  [FIRAnalytics logEventWithName:self.eventNameText.text parameters:@{}];
+}
+
+- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
+  UITouch *touch = [touches anyObject];
+  if (![touch.view isMemberOfClass:[UITextField class]]) {
+    [touch.view endEditing:YES];
+  }
+}
+- (IBAction)changeAutoDataCollection:(id)sender {
+  FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging];
+  sdk.automaticDataCollectionEnabled = !sdk.automaticDataCollectionEnabled;
+  [self setupChangeAutoDataCollectionButtonLabel];
+}
+
+- (void)setupChangeAutoDataCollectionButtonLabel {
+  FIRInAppMessaging *sdk = [FIRInAppMessaging inAppMessaging];
+  NSString *title = sdk.automaticDataCollectionEnabled ? @"disable data-col" : @"enable data-col";
+  [self.changeDataCollectionBtn setTitle:title forState:UIControlStateNormal];
+}
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+
+  double delayInSeconds = 2.0;
+  dispatch_time_t setupTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
+  dispatch_after(setupTime, dispatch_get_main_queue(), ^(void) {
+    // code to be executed on the main queue after delay
+    self.sdkRuntime = [FIRIAMRuntimeManager getSDKRuntimeInstance];
+    self.messageTableVC.messageCache = self.sdkRuntime.messageCache;
+    [self.sdkRuntime.messageCache setDataObserver:self.messageTableVC];
+    [self.messageTableVC.tableView reloadData];
+    [self setupDisableEnableButtonLabel];
+    [self setupChangeAutoDataCollectionButtonLabel];
+  });
+
+  NSLog(@"done with set data observer");
+
+  self.autoFetchIntervalText.text = [[NSNumber
+      numberWithDouble:self.sdkRuntime.currentSetting.fetchMinIntervalInMinutes * 60] stringValue];
+  self.autoDisplayIntervalText.text =
+      [[NSNumber numberWithDouble:self.sdkRuntime.currentSetting.appFGRenderMinIntervalInMinutes *
+                                  60] stringValue];
+}
+
+- (IBAction)dumpImpressionsToConsole:(id)sender {
+  NSArray *impressions = [self.sdkRuntime.bookKeeper getImpressions];
+  NSLog(@"impressions are %@", [impressions componentsJoinedByString:@","]);
+}
+- (IBAction)clearImpressionRecord:(id)sender {
+  [self.sdkRuntime.bookKeeper cleanupImpressions];
+}
+
+- (IBAction)changeAutoFetchDisplaySettings:(id)sender {
+  FIRIAMSDKSettings *setting = self.sdkRuntime.currentSetting;
+
+  // set fetch interval
+  double intervalValue = self.autoFetchIntervalText.text.doubleValue / 60;
+  if (intervalValue < 0.0001) {
+    intervalValue = 1;
+    self.autoFetchIntervalText.text = [[NSNumber numberWithDouble:intervalValue * 60] stringValue];
+  }
+  setting.fetchMinIntervalInMinutes = intervalValue;
+
+  // set app foreground display interval
+  double displayIntervalValue = self.autoDisplayIntervalText.text.doubleValue / 60;
+
+  if (displayIntervalValue < 0.0001) {
+    displayIntervalValue = 1;
+    self.autoDisplayIntervalText.text =
+        [[NSNumber numberWithDouble:displayIntervalValue * 60] stringValue];
+  }
+  setting.appFGRenderMinIntervalInMinutes = displayIntervalValue;
+
+  [self.sdkRuntime startRuntimeWithSDKSettings:setting];
+}
+
+- (void)didReceiveMemoryWarning {
+  [super didReceiveMemoryWarning];
+  // Dispose of any resources that can be recreated.
+}
+
+#pragma mark - Navigation
+// In a storyboard-based application, you will often want to do a little preparation before
+// navigation
+- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
+  // Get the new view controller using [segue destinationViewController].
+  // Pass the selected object to the new view controller.
+
+  if ([segue.identifier isEqualToString:@"message-table-segue"]) {
+    self.messageTableVC = (AutoDisplayMesagesTableVC *)[segue destinationViewController];
+  }
+}
+@end

+ 23 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.h

@@ -0,0 +1,23 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <UIKit/UIKit.h>
+
+#import "FIRIAMMessageClientCache.h"
+
+@interface AutoDisplayMesagesTableVC : UITableViewController <FIRIAMCacheDataObserver>
+@property(nonatomic) FIRIAMMessageClientCache *messageCache;
+@end

+ 137 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/AutoDisplayMesagesTableVC.m

@@ -0,0 +1,137 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "AutoDisplayMesagesTableVC.h"
+#import "FIRIAMDisplayTriggerDefinition.h"
+#import "FIRIAMMessageContentData.h"
+
+@interface AutoDisplayMesagesTableVC ()
+@end
+
+@implementation AutoDisplayMesagesTableVC
+
+- (void)dataChanged {
+  dispatch_async(dispatch_get_main_queue(), ^{
+    [self.tableView reloadData];
+  });
+}
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+
+  // Uncomment the following line to preserve selection between presentations.
+  // self.clearsSelectionOnViewWillAppear = NO;
+
+  // Uncomment the following line to display an Edit button in the navigation bar for this view
+  // controller. self.navigationItem.rightBarButtonItem = self.editButtonItem;
+}
+
+- (void)didReceiveMemoryWarning {
+  [super didReceiveMemoryWarning];
+  // Dispose of any resources that can be recreated.
+}
+
+#pragma mark - Table view data source
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+  return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+  NSArray<FIRIAMMessageDefinition *> *messages = self.messageCache.allRegularMessages;
+  if (messages) {
+    return messages.count;
+  } else {
+    return 0;
+  }
+}
+
+static NSString *CellIdentifier = @"CellIdentifier";
+
+- (NSString *)viewModeDisplayString:(FIRIAMRenderingMode)viewMode {
+  switch (viewMode) {
+    case FIRIAMRenderAsBannerView:
+      return @"Banner";
+    case FIRIAMRenderAsModalView:
+      return @"Modal";
+    case FIRIAMRenderAsImageOnlyView:
+      return @"Image";
+    default:
+      return @"Unknown";
+  }
+}
+
+- (NSString *)triggerDisplayString:(NSArray<FIRIAMDisplayTriggerDefinition *> *)triggers {
+  NSMutableString *s = [[NSMutableString alloc] init];
+  for (FIRIAMDisplayTriggerDefinition *trigger in triggers) {
+    [s appendString:[self triggerDisplayStringForOneTrigger:trigger]];
+    [s appendString:@","];
+  }
+  return [s copy];
+}
+
+- (NSString *)triggerDisplayStringForOneTrigger:
+    (FIRIAMDisplayTriggerDefinition *)triggerDefinition {
+  switch (triggerDefinition.triggerType) {
+    case FIRIAMRenderTriggerOnAppForeground:
+      return @"app_foreground";
+    case FIRIAMRenderTriggerOnFirebaseAnalyticsEvent:
+      return triggerDefinition.firebaseEventName;
+    default:
+      return @"Unknown";
+  }
+}
+- (UITableViewCell *)tableView:(UITableView *)tableView
+         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+  NSArray<FIRIAMMessageDefinition *> *messageDefs = self.messageCache.allRegularMessages;
+
+  NSInteger rowIndex = [indexPath row];
+  if (messageDefs.count > rowIndex) {
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+
+    if (cell == nil) {
+      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
+                                    reuseIdentifier:CellIdentifier];
+    }
+
+    UILabel *titleLabel = (UILabel *)[cell.contentView viewWithTag:10];
+    UILabel *modeLabel = (UILabel *)[cell.contentView viewWithTag:20];
+    UIImageView *imageView = (UIImageView *)[cell.contentView viewWithTag:30];
+    UILabel *triggerLabel = (UILabel *)[cell.contentView viewWithTag:40];
+
+    titleLabel.text = messageDefs[rowIndex].renderData.contentData.titleText;
+    modeLabel.text = [self
+        viewModeDisplayString:messageDefs[rowIndex].renderData.renderingEffectSettings.viewMode];
+
+    triggerLabel.text = [self triggerDisplayString:messageDefs[rowIndex].renderTriggers];
+
+    [messageDefs[rowIndex].renderData.contentData
+        loadImageDataWithBlock:^(NSData *_Nullable imageData, NSError *error) {
+          if (error) {
+            NSLog(@"error in loading image: %@", error.localizedDescription);
+          } else {
+            UIImage *image = [UIImage imageWithData:imageData];
+            dispatch_async(dispatch_get_main_queue(), ^{
+              [imageView setImage:image];
+            });
+          }
+        }];
+    return cell;
+  } else {
+    return nil;
+  }
+}
+
+@end

+ 27 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <dependencies>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11106"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="Llm-lL-Icb"/>
+                        <viewControllerLayoutGuide type="bottom" id="xb3-aO-Qok"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

+ 395 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/Base.lproj/Main.storyboard

@@ -0,0 +1,395 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14113" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Ebq-cZ-Ywc">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14088"/>
+        <capability name="Constraints to layout margins" minToolsVersion="6.0"/>
+        <capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--Fetch & Display Flows-->
+        <scene sceneID="gh4-xG-6d0">
+            <objects>
+                <viewController id="pEd-Cp-eSk" customClass="AutoDisplayFlowViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="ApJ-fZ-bbG"/>
+                        <viewControllerLayoutGuide type="bottom" id="fcu-KV-0co"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="27y-ro-ncL">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Nbf-aL-D4Q">
+                                <rect key="frame" x="0.0" y="320.5" width="375" height="287.5"/>
+                                <connections>
+                                    <segue destination="PsL-F0-NPD" kind="embed" identifier="message-table-segue" id="Ruf-dB-QUH"/>
+                                </connections>
+                            </containerView>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Messages To Display" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2lf-ei-PaY">
+                                <rect key="frame" x="0.0" y="292" width="375" height="20.5"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FB6-Ej-jmi">
+                                <rect key="frame" x="0.0" y="235" width="375" height="2"/>
+                                <color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="2" id="xBA-ln-idE"/>
+                                </constraints>
+                            </view>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QzN-45-brV">
+                                <rect key="frame" x="0.0" y="618" width="375" height="2"/>
+                                <color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="2" id="qn7-el-C36"/>
+                                </constraints>
+                            </view>
+                            <button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TWl-ic-ODQ">
+                                <rect key="frame" x="14" y="247" width="138" height="30"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                <state key="normal" title="Trigger analytics event"/>
+                                <connections>
+                                    <action selector="triggerAnalyticEventTapped:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="IEI-K3-ihJ"/>
+                                </connections>
+                            </button>
+                            <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="3kb-Du-qxH" userLabel="Top Half View">
+                                <rect key="frame" x="0.0" y="40" width="375" height="190"/>
+                                <subviews>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Display every" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="jOj-Sz-eSe">
+                                        <rect key="frame" x="8" y="8" width="81" height="16"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                        <nil key="textColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="60" borderStyle="roundedRect" textAlignment="right" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="tTK-0J-lFc">
+                                        <rect key="frame" x="109" y="1" width="112" height="30"/>
+                                        <nil key="textColor"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                        <textInputTraits key="textInputTraits"/>
+                                    </textField>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="secs when foreground" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="yhR-Gb-Fis">
+                                        <rect key="frame" x="241" y="9" width="126" height="14"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                                        <nil key="textColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" horizontalCompressionResistancePriority="751" text="Fetch every" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AYz-Nz-twr">
+                                        <rect key="frame" x="8" y="49.5" width="71" height="16"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                        <nil key="textColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" horizontalHuggingPriority="249" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="10" borderStyle="roundedRect" textAlignment="right" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="BDa-nI-3WI">
+                                        <rect key="frame" x="109" y="42.5" width="112" height="30"/>
+                                        <nil key="textColor"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                        <textInputTraits key="textInputTraits"/>
+                                    </textField>
+                                    <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="secs when foreground" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Cpz-Qh-H0O">
+                                        <rect key="frame" x="241" y="47" width="126" height="14.5"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="12"/>
+                                        <nil key="textColor"/>
+                                        <nil key="highlightedColor"/>
+                                    </label>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="ZUt-bH-BSp">
+                                        <rect key="frame" x="10" y="90" width="355" height="28"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                        <state key="normal" title="Apply Displa/Fetch Frequency Change"/>
+                                        <connections>
+                                            <action selector="changeAutoFetchDisplaySettings:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="3BJ-aG-3AT"/>
+                                        </connections>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="unO-PY-Ja6">
+                                        <rect key="frame" x="101" y="158" width="97" height="24"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                        <state key="normal" title="Clear Client Storage"/>
+                                        <connections>
+                                            <action selector="clearClientStorage:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="uVD-WV-FCT"/>
+                                        </connections>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Erp-ss-RqT">
+                                        <rect key="frame" x="10" y="158" width="81" height="24"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                        <state key="normal" title="Clear Impression"/>
+                                        <connections>
+                                            <action selector="clearImpressionRecord:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="ngv-At-BDz"/>
+                                        </connections>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="nUy-hl-LzV">
+                                        <rect key="frame" x="213" y="158" width="59" height="24"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                        <state key="normal" title="disable SDK"/>
+                                        <connections>
+                                            <action selector="disableEnableClicked:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="zfv-0Y-GVj"/>
+                                        </connections>
+                                    </button>
+                                    <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="KCx-gs-cUi">
+                                        <rect key="frame" x="287" y="158" width="76" height="24"/>
+                                        <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                        <state key="normal" title="enable data-col"/>
+                                        <connections>
+                                            <action selector="changeAutoDataCollection:" destination="pEd-Cp-eSk" eventType="touchUpInside" id="SEG-zG-Uut"/>
+                                        </connections>
+                                    </button>
+                                </subviews>
+                                <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                                <constraints>
+                                    <constraint firstAttribute="trailing" secondItem="yhR-Gb-Fis" secondAttribute="trailing" constant="8" id="1Pc-Db-eGC"/>
+                                    <constraint firstAttribute="bottom" secondItem="Erp-ss-RqT" secondAttribute="bottom" constant="8" id="45D-eI-ubC"/>
+                                    <constraint firstItem="jOj-Sz-eSe" firstAttribute="top" secondItem="3kb-Du-qxH" secondAttribute="top" constant="8" id="5Rb-Vg-nFV"/>
+                                    <constraint firstItem="tTK-0J-lFc" firstAttribute="centerY" secondItem="jOj-Sz-eSe" secondAttribute="centerY" id="6Xt-sE-6yg"/>
+                                    <constraint firstAttribute="height" constant="190" id="9dh-YW-rHe"/>
+                                    <constraint firstItem="AYz-Nz-twr" firstAttribute="top" secondItem="jOj-Sz-eSe" secondAttribute="bottom" constant="25.333333333333336" id="ACy-rq-NPK"/>
+                                    <constraint firstItem="tTK-0J-lFc" firstAttribute="leading" secondItem="jOj-Sz-eSe" secondAttribute="trailing" constant="20" id="ASJ-KD-XHo"/>
+                                    <constraint firstItem="unO-PY-Ja6" firstAttribute="top" secondItem="Erp-ss-RqT" secondAttribute="top" id="BHu-hz-Qig"/>
+                                    <constraint firstItem="Cpz-Qh-H0O" firstAttribute="top" secondItem="yhR-Gb-Fis" secondAttribute="bottom" constant="24" id="GCe-bk-B7o"/>
+                                    <constraint firstItem="ZUt-bH-BSp" firstAttribute="leading" secondItem="3kb-Du-qxH" secondAttribute="leading" constant="10" id="I9g-Vp-i0f"/>
+                                    <constraint firstItem="unO-PY-Ja6" firstAttribute="leading" secondItem="Erp-ss-RqT" secondAttribute="trailing" constant="10" id="M8G-es-Oyi"/>
+                                    <constraint firstItem="KCx-gs-cUi" firstAttribute="bottom" secondItem="nUy-hl-LzV" secondAttribute="bottom" id="Mcg-A1-vnX"/>
+                                    <constraint firstItem="nUy-hl-LzV" firstAttribute="leading" secondItem="unO-PY-Ja6" secondAttribute="trailing" constant="15" id="NGu-z9-kRt"/>
+                                    <constraint firstItem="yhR-Gb-Fis" firstAttribute="leading" secondItem="tTK-0J-lFc" secondAttribute="trailing" constant="20" id="Omn-JV-LAt"/>
+                                    <constraint firstItem="ZUt-bH-BSp" firstAttribute="centerX" secondItem="3kb-Du-qxH" secondAttribute="centerX" id="PLW-kz-soN"/>
+                                    <constraint firstItem="BDa-nI-3WI" firstAttribute="trailing" secondItem="tTK-0J-lFc" secondAttribute="trailing" id="QJ9-2l-v5D"/>
+                                    <constraint firstItem="ZUt-bH-BSp" firstAttribute="top" secondItem="BDa-nI-3WI" secondAttribute="bottom" constant="17.333333333333329" id="U1i-HM-lr6"/>
+                                    <constraint firstItem="AYz-Nz-twr" firstAttribute="leading" secondItem="jOj-Sz-eSe" secondAttribute="leading" id="YCS-cl-bxb"/>
+                                    <constraint firstItem="BDa-nI-3WI" firstAttribute="leading" secondItem="tTK-0J-lFc" secondAttribute="leading" id="d1h-oK-ox0"/>
+                                    <constraint firstItem="nUy-hl-LzV" firstAttribute="bottom" secondItem="unO-PY-Ja6" secondAttribute="bottom" id="efd-OH-Yg9"/>
+                                    <constraint firstItem="Cpz-Qh-H0O" firstAttribute="leading" secondItem="yhR-Gb-Fis" secondAttribute="leading" id="fLI-os-Afm"/>
+                                    <constraint firstItem="yhR-Gb-Fis" firstAttribute="centerY" secondItem="jOj-Sz-eSe" secondAttribute="centerY" id="fYi-FN-QBR"/>
+                                    <constraint firstItem="KCx-gs-cUi" firstAttribute="leading" secondItem="nUy-hl-LzV" secondAttribute="trailing" constant="15" id="gCm-J5-RhN"/>
+                                    <constraint firstItem="KCx-gs-cUi" firstAttribute="top" secondItem="nUy-hl-LzV" secondAttribute="top" id="heU-7o-tI9"/>
+                                    <constraint firstItem="jOj-Sz-eSe" firstAttribute="leading" secondItem="3kb-Du-qxH" secondAttribute="leading" constant="8" id="muA-WB-8PQ"/>
+                                    <constraint firstItem="unO-PY-Ja6" firstAttribute="bottom" secondItem="Erp-ss-RqT" secondAttribute="bottom" id="ohr-CS-Xkc"/>
+                                    <constraint firstItem="BDa-nI-3WI" firstAttribute="baseline" secondItem="AYz-Nz-twr" secondAttribute="baseline" id="pwA-4B-T8t"/>
+                                    <constraint firstItem="Cpz-Qh-H0O" firstAttribute="trailing" secondItem="yhR-Gb-Fis" secondAttribute="trailing" id="tcc-gN-feq"/>
+                                    <constraint firstItem="nUy-hl-LzV" firstAttribute="top" secondItem="unO-PY-Ja6" secondAttribute="top" id="ujd-gw-I2g"/>
+                                    <constraint firstItem="Erp-ss-RqT" firstAttribute="leading" secondItem="3kb-Du-qxH" secondAttribute="leading" constant="10" id="yst-Bp-eIN"/>
+                                </constraints>
+                            </view>
+                            <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="test_event" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="M0T-f0-qfn">
+                                <rect key="frame" x="182" y="247" width="173" height="30"/>
+                                <nil key="textColor"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="13"/>
+                                <textInputTraits key="textInputTraits"/>
+                            </textField>
+                        </subviews>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                        <constraints>
+                            <constraint firstItem="2lf-ei-PaY" firstAttribute="leading" secondItem="27y-ro-ncL" secondAttribute="leading" id="0g2-Zm-MAm"/>
+                            <constraint firstItem="FB6-Ej-jmi" firstAttribute="top" secondItem="3kb-Du-qxH" secondAttribute="bottom" constant="5" id="19L-XJ-rNE"/>
+                            <constraint firstAttribute="trailing" secondItem="2lf-ei-PaY" secondAttribute="trailing" id="2sG-IE-v1C"/>
+                            <constraint firstAttribute="trailing" secondItem="Nbf-aL-D4Q" secondAttribute="trailing" id="3nJ-jP-296"/>
+                            <constraint firstItem="M0T-f0-qfn" firstAttribute="bottom" secondItem="TWl-ic-ODQ" secondAttribute="bottom" id="68L-bx-rfi"/>
+                            <constraint firstItem="QzN-45-brV" firstAttribute="leading" secondItem="Nbf-aL-D4Q" secondAttribute="leading" id="68c-cK-w0l"/>
+                            <constraint firstItem="QzN-45-brV" firstAttribute="top" secondItem="Nbf-aL-D4Q" secondAttribute="bottom" constant="10" id="CCo-W9-Nwb"/>
+                            <constraint firstItem="Nbf-aL-D4Q" firstAttribute="top" secondItem="2lf-ei-PaY" secondAttribute="bottom" constant="8" id="Cvc-tl-a8s"/>
+                            <constraint firstItem="FB6-Ej-jmi" firstAttribute="leading" secondItem="27y-ro-ncL" secondAttribute="leading" id="E6W-9R-2GN"/>
+                            <constraint firstItem="3kb-Du-qxH" firstAttribute="top" secondItem="ApJ-fZ-bbG" secondAttribute="bottom" constant="20" id="GnX-rq-jq9"/>
+                            <constraint firstItem="Nbf-aL-D4Q" firstAttribute="height" relation="lessThanOrEqual" secondItem="27y-ro-ncL" secondAttribute="height" multiplier="1:2" id="HBN-Cy-dFb"/>
+                            <constraint firstItem="3kb-Du-qxH" firstAttribute="leading" secondItem="27y-ro-ncL" secondAttribute="leading" id="MQn-1u-8Is"/>
+                            <constraint firstItem="3kb-Du-qxH" firstAttribute="centerX" secondItem="27y-ro-ncL" secondAttribute="centerX" id="Um3-SX-kub"/>
+                            <constraint firstItem="TWl-ic-ODQ" firstAttribute="top" secondItem="FB6-Ej-jmi" secondAttribute="bottom" constant="10" id="YYp-3Y-QXH"/>
+                            <constraint firstItem="FB6-Ej-jmi" firstAttribute="width" secondItem="27y-ro-ncL" secondAttribute="width" id="aGR-qE-35f"/>
+                            <constraint firstAttribute="trailing" secondItem="M0T-f0-qfn" secondAttribute="trailing" constant="20" id="cHW-QJ-D5z"/>
+                            <constraint firstItem="M0T-f0-qfn" firstAttribute="top" secondItem="TWl-ic-ODQ" secondAttribute="top" id="hHq-ch-Cw9"/>
+                            <constraint firstItem="fcu-KV-0co" firstAttribute="top" secondItem="Nbf-aL-D4Q" secondAttribute="bottom" constant="10" id="iF9-YE-7Bh"/>
+                            <constraint firstItem="M0T-f0-qfn" firstAttribute="leading" secondItem="TWl-ic-ODQ" secondAttribute="trailing" constant="30" id="oK4-kd-ngD"/>
+                            <constraint firstItem="QzN-45-brV" firstAttribute="trailing" secondItem="Nbf-aL-D4Q" secondAttribute="trailing" id="ore-FG-3Z5"/>
+                            <constraint firstItem="Nbf-aL-D4Q" firstAttribute="leading" secondItem="27y-ro-ncL" secondAttribute="leading" id="rHM-kR-Ebu"/>
+                            <constraint firstItem="2lf-ei-PaY" firstAttribute="top" secondItem="FB6-Ej-jmi" secondAttribute="bottom" constant="55" id="xhl-Hf-WDB"/>
+                            <constraint firstItem="TWl-ic-ODQ" firstAttribute="leading" secondItem="27y-ro-ncL" secondAttribute="leading" constant="14" id="zTT-qI-tld"/>
+                        </constraints>
+                    </view>
+                    <tabBarItem key="tabBarItem" title="Fetch &amp; Display Flows" id="Nzu-an-zJc"/>
+                    <connections>
+                        <outlet property="autoDisplayIntervalText" destination="BDa-nI-3WI" id="fEx-4O-aNP"/>
+                        <outlet property="autoFetchIntervalText" destination="tTK-0J-lFc" id="TT2-Ib-Hrj"/>
+                        <outlet property="changeDataCollectionBtn" destination="KCx-gs-cUi" id="MWc-Kk-IWX"/>
+                        <outlet property="disableEnableSDKBtn" destination="nUy-hl-LzV" id="Sm9-9g-Uc5"/>
+                        <outlet property="eventNameText" destination="M0T-f0-qfn" id="PJw-AR-bWa"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="1YW-qT-fHJ" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="339.13043478260875" y="1420.108695652174"/>
+        </scene>
+        <!--Viewing Logs-->
+        <scene sceneID="1fw-sb-9xA">
+            <objects>
+                <viewController id="nu8-Ds-Fz1" customClass="LogDumpViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="NqW-PZ-fID"/>
+                        <viewControllerLayoutGuide type="bottom" id="4QO-z6-XzH"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ap7-Ba-fBV">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="sW6-Cb-xEh">
+                                <rect key="frame" x="20" y="35" width="81" height="30"/>
+                                <state key="normal" title="Activity Log"/>
+                                <connections>
+                                    <action selector="dumActivityLogs:" destination="nu8-Ds-Fz1" eventType="touchUpInside" id="BiP-UR-aiJ"/>
+                                </connections>
+                            </button>
+                            <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="Log Dump" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="DQB-Yw-yUi">
+                                <rect key="frame" x="20" y="80" width="335" height="533"/>
+                                <color key="backgroundColor" white="0.87141927083333337" alpha="1" colorSpace="calibratedWhite"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="14"/>
+                                <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
+                            </textView>
+                            <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hv5-EM-k16">
+                                <rect key="frame" x="231" y="35" width="128" height="30"/>
+                                <state key="normal" title="Impression History"/>
+                                <connections>
+                                    <action selector="dumpImpressList:" destination="nu8-Ds-Fz1" eventType="touchUpInside" id="EsS-CF-FWw"/>
+                                </connections>
+                            </button>
+                        </subviews>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                        <constraints>
+                            <constraint firstItem="sW6-Cb-xEh" firstAttribute="top" secondItem="NqW-PZ-fID" secondAttribute="bottom" constant="15" id="6DA-FW-mAK"/>
+                            <constraint firstItem="DQB-Yw-yUi" firstAttribute="leading" secondItem="Ap7-Ba-fBV" secondAttribute="leading" constant="20" id="6pK-1H-cXJ"/>
+                            <constraint firstItem="DQB-Yw-yUi" firstAttribute="top" secondItem="sW6-Cb-xEh" secondAttribute="bottom" constant="15" id="CYj-rX-vKt"/>
+                            <constraint firstItem="DQB-Yw-yUi" firstAttribute="centerX" secondItem="Ap7-Ba-fBV" secondAttribute="centerX" id="CrK-n8-1lJ"/>
+                            <constraint firstItem="sW6-Cb-xEh" firstAttribute="leading" secondItem="Ap7-Ba-fBV" secondAttribute="leading" constant="20" id="MGr-yR-bM9"/>
+                            <constraint firstAttribute="trailing" secondItem="hv5-EM-k16" secondAttribute="trailing" constant="16" id="e9u-Lp-lGv"/>
+                            <constraint firstItem="hv5-EM-k16" firstAttribute="baseline" secondItem="sW6-Cb-xEh" secondAttribute="baseline" id="flZ-lL-e42"/>
+                            <constraint firstItem="4QO-z6-XzH" firstAttribute="top" secondItem="DQB-Yw-yUi" secondAttribute="bottom" constant="5" id="wzo-UQ-a5y"/>
+                        </constraints>
+                    </view>
+                    <tabBarItem key="tabBarItem" title="Viewing Logs" id="Nnn-zb-noK"/>
+                    <connections>
+                        <outlet property="logTextView" destination="DQB-Yw-yUi" id="sZI-WT-fU1"/>
+                    </connections>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="c3j-np-NcL" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="946" y="500"/>
+        </scene>
+        <!--Second View Controller-->
+        <scene sceneID="Egu-aw-S0d">
+            <objects>
+                <viewController storyboardIdentifier="DeepLinkingView" useStoryboardIdentifierAsRestorationIdentifier="YES" id="US6-BO-dB7" customClass="SecondViewController" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="R8m-T3-Cez"/>
+                        <viewControllerLayoutGuide type="bottom" id="H48-Lo-aod"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="tsX-ZM-a3w">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <subviews>
+                            <label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="This is a deep linking view in app" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gFH-Ov-0hM">
+                                <rect key="frame" x="26" y="154" width="323" height="20.5"/>
+                                <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                <nil key="textColor"/>
+                                <nil key="highlightedColor"/>
+                            </label>
+                        </subviews>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                        <constraints>
+                            <constraint firstItem="gFH-Ov-0hM" firstAttribute="centerX" secondItem="tsX-ZM-a3w" secondAttribute="centerX" id="6Hp-4v-Lc3"/>
+                            <constraint firstItem="gFH-Ov-0hM" firstAttribute="leading" secondItem="tsX-ZM-a3w" secondAttribute="leadingMargin" constant="10" id="Cbr-dN-cQW"/>
+                            <constraint firstItem="gFH-Ov-0hM" firstAttribute="top" secondItem="R8m-T3-Cez" secondAttribute="bottom" constant="134" id="nSY-Lo-oRo"/>
+                        </constraints>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="BpB-dL-xbj" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="3412" y="105"/>
+        </scene>
+        <!--Tab Bar Controller-->
+        <scene sceneID="pSH-oS-ggK">
+            <objects>
+                <tabBarController automaticallyAdjustsScrollViewInsets="NO" id="Ebq-cZ-Ywc" sceneMemberID="viewController">
+                    <toolbarItems/>
+                    <tabBar key="tabBar" contentMode="scaleToFill" id="kvL-FD-WNk">
+                        <rect key="frame" x="0.0" y="0.0" width="1000" height="1000"/>
+                        <autoresizingMask key="autoresizingMask"/>
+                        <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="calibratedWhite"/>
+                    </tabBar>
+                    <connections>
+                        <segue destination="pEd-Cp-eSk" kind="relationship" relationship="viewControllers" id="Nhl-bm-Fwd"/>
+                        <segue destination="nu8-Ds-Fz1" kind="relationship" relationship="viewControllers" id="Kp0-V8-Czg"/>
+                    </connections>
+                </tabBarController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="euT-LS-VkP" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="-562.31884057971024" y="635.86956521739137"/>
+        </scene>
+        <!--Auto Display Mesages TableVC-->
+        <scene sceneID="q5h-FN-tiy">
+            <objects>
+                <tableViewController id="PsL-F0-NPD" customClass="AutoDisplayMesagesTableVC" sceneMemberID="viewController">
+                    <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="qfr-gm-4ZH">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="287.5"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
+                        <prototypes>
+                            <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="CellIdentifier" id="Jfn-Cu-nDc">
+                                <rect key="frame" x="0.0" y="28" width="375" height="44"/>
+                                <autoresizingMask key="autoresizingMask"/>
+                                <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Jfn-Cu-nDc" id="6o7-w6-ur1">
+                                    <rect key="frame" x="0.0" y="0.0" width="375" height="43.5"/>
+                                    <autoresizingMask key="autoresizingMask"/>
+                                    <subviews>
+                                        <label opaque="NO" userInteractionEnabled="NO" tag="10" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ALb-TI-LRG">
+                                            <rect key="frame" x="66" y="11" width="125" height="22"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <label opaque="NO" userInteractionEnabled="NO" tag="20" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="View Mode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="POn-7q-Bn9">
+                                            <rect key="frame" x="201" y="11" width="54" height="22"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                        <imageView userInteractionEnabled="NO" tag="30" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="dmd-Df-jYr">
+                                            <rect key="frame" x="18" y="11" width="40" height="22"/>
+                                            <constraints>
+                                                <constraint firstAttribute="width" constant="40" id="yhC-Xm-Wds"/>
+                                            </constraints>
+                                        </imageView>
+                                        <label opaque="NO" userInteractionEnabled="NO" tag="40" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Trigger" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9KB-qI-ugU">
+                                            <rect key="frame" x="274" y="11" width="34.5" height="22"/>
+                                            <fontDescription key="fontDescription" type="system" pointSize="10"/>
+                                            <nil key="textColor"/>
+                                            <nil key="highlightedColor"/>
+                                        </label>
+                                    </subviews>
+                                    <constraints>
+                                        <constraint firstItem="dmd-Df-jYr" firstAttribute="top" secondItem="6o7-w6-ur1" secondAttribute="topMargin" id="BTx-fn-tUI"/>
+                                        <constraint firstItem="9KB-qI-ugU" firstAttribute="bottom" secondItem="POn-7q-Bn9" secondAttribute="bottom" id="C9X-1M-sek"/>
+                                        <constraint firstItem="POn-7q-Bn9" firstAttribute="leading" secondItem="ALb-TI-LRG" secondAttribute="trailing" constant="10" id="Gz1-LV-5dB"/>
+                                        <constraint firstItem="POn-7q-Bn9" firstAttribute="top" secondItem="ALb-TI-LRG" secondAttribute="top" id="Jpl-XU-mf0"/>
+                                        <constraint firstItem="ALb-TI-LRG" firstAttribute="bottom" secondItem="6o7-w6-ur1" secondAttribute="bottomMargin" id="Sae-8t-YEC"/>
+                                        <constraint firstItem="POn-7q-Bn9" firstAttribute="baseline" secondItem="ALb-TI-LRG" secondAttribute="baseline" id="a85-Dq-pk9"/>
+                                        <constraint firstItem="ALb-TI-LRG" firstAttribute="leading" secondItem="6o7-w6-ur1" secondAttribute="leadingMargin" constant="50" id="cq1-6S-hAq"/>
+                                        <constraint firstItem="9KB-qI-ugU" firstAttribute="top" secondItem="POn-7q-Bn9" secondAttribute="top" id="g5J-45-riG"/>
+                                        <constraint firstItem="9KB-qI-ugU" firstAttribute="leading" secondItem="POn-7q-Bn9" secondAttribute="trailing" constant="19" id="h40-zK-676"/>
+                                        <constraint firstItem="ALb-TI-LRG" firstAttribute="width" secondItem="6o7-w6-ur1" secondAttribute="width" multiplier="1:3" id="k8C-Hq-Hr8"/>
+                                        <constraint firstItem="dmd-Df-jYr" firstAttribute="leading" secondItem="6o7-w6-ur1" secondAttribute="leadingMargin" constant="2" id="tFb-dp-Hgn"/>
+                                        <constraint firstAttribute="bottomMargin" secondItem="dmd-Df-jYr" secondAttribute="bottom" id="uDf-gE-Bdv"/>
+                                        <constraint firstItem="ALb-TI-LRG" firstAttribute="top" secondItem="6o7-w6-ur1" secondAttribute="topMargin" id="uzM-1f-tdY"/>
+                                    </constraints>
+                                </tableViewCellContentView>
+                            </tableViewCell>
+                        </prototypes>
+                        <connections>
+                            <outlet property="dataSource" destination="PsL-F0-NPD" id="8zu-HO-pQe"/>
+                            <outlet property="delegate" destination="PsL-F0-NPD" id="P8Q-tt-CaF"/>
+                        </connections>
+                    </tableView>
+                </tableViewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="IcO-uK-bw0" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="1369.5652173913045" y="1685.8695652173915"/>
+        </scene>
+    </scenes>
+</document>

+ 28 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/GoogleService-Info.plist

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>API_KEY</key>
+	<string>correct_api_key</string>
+	<key>TRACKING_ID</key>
+	<string>correct_tracking_id</string>
+	<key>CLIENT_ID</key>
+	<string>correct_client_id</string>
+	<key>REVERSED_CLIENT_ID</key>
+	<string>correct_reversed_client_id</string>
+	<key>GOOGLE_APP_ID</key>
+	<string>1:123:ios:123abc</string>
+	<key>GCM_SENDER_ID</key>
+	<string>correct_gcm_sender_id</string>
+	<key>PLIST_VERSION</key>
+	<string>1</string>
+	<key>BUNDLE_ID</key>
+	<string>com.google.FirebaseSDKTests</string>
+	<key>PROJECT_ID</key>
+	<string>abc-xyz-123</string>
+	<key>DATABASE_URL</key>
+	<string>https://abc-xyz-123.firebaseio.com</string>
+	<key>STORAGE_BUCKET</key>
+	<string>project-id-123.storage.firebase.com</string>
+</dict>
+</plist>

+ 67 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/Info.plist

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>FirebaseInAppMessagingAutomaticDataCollectionEnabled</key>
+	<true/>
+	<key>FirebaseAutomaticDataCollectionEnabled</key>
+	<true/>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>en</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>APPL</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0.3</string>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLName</key>
+			<string>mytesting</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>fiam-testing</string>
+			</array>
+		</dict>
+	</array>
+	<key>CFBundleVersion</key>
+	<string>1.0.3-1</string>
+	<key>LSRequiresIPhoneOS</key>
+	<true/>
+	<key>NSAppTransportSecurity</key>
+	<dict>
+		<key>NSAllowsArbitraryLoads</key>
+		<true/>
+	</dict>
+	<key>UILaunchStoryboardName</key>
+	<string>LaunchScreen</string>
+	<key>UIMainStoryboardFile</key>
+	<string>Main</string>
+	<key>UIRequiredDeviceCapabilities</key>
+	<array>
+		<string>armv7</string>
+	</array>
+	<key>UISupportedInterfaceOrientations</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+	<key>UISupportedInterfaceOrientations~ipad</key>
+	<array>
+		<string>UIInterfaceOrientationPortrait</string>
+		<string>UIInterfaceOrientationPortraitUpsideDown</string>
+		<string>UIInterfaceOrientationLandscapeLeft</string>
+		<string>UIInterfaceOrientationLandscapeRight</string>
+	</array>
+</dict>
+</plist>

+ 21 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.h

@@ -0,0 +1,21 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <UIKit/UIKit.h>
+
+@interface LogDumpViewController : UIViewController
+
+@end

+ 73 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/LogDumpViewController.m

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import "LogDumpViewController.h"
+#import "AppDelegate.h"
+#import "FIRIAMRuntimeManager.h"
+
+@interface LogDumpViewController ()
+@property(weak, nonatomic) IBOutlet UITextView *logTextView;
+@end
+
+@implementation LogDumpViewController
+- (IBAction)dumpImpressList:(id)sender {
+  NSArray *impressions = [[FIRIAMRuntimeManager getSDKRuntimeInstance].bookKeeper getImpressions];
+  NSString *text = [NSString stringWithFormat:@"Message Impression History are :\n%@",
+                                              [impressions componentsJoinedByString:@"\n"]];
+  self.logTextView.text = text;
+}
+
+- (IBAction)dumActivityLogs:(id)sender {
+  NSArray<FIRIAMActivityRecord *> *records =
+      [[FIRIAMRuntimeManager getSDKRuntimeInstance].activityLogger readRecords];
+
+  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+  dateFormatter.dateStyle = NSDateFormatterShortStyle;
+  dateFormatter.timeStyle = NSDateFormatterMediumStyle;
+
+  static NSString *appBuildVersion = nil;
+  static dispatch_once_t onceToken;
+  dispatch_once(&onceToken, ^{
+    appBuildVersion = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"];
+  });
+
+  NSMutableString *dumpContent = [[NSString
+      stringWithFormat:@"App Build Version -- %@\n\n"
+                        "SDK Settings -- %@\n\n"
+                        "Activity Logs: %lu records\n\n",
+                       appBuildVersion, [FIRIAMRuntimeManager getSDKRuntimeInstance].currentSetting,
+                       (unsigned long)records.count] mutableCopy];
+
+  for (FIRIAMActivityRecord *next in records) {
+    NSString *nextRecordLog = [NSString
+        stringWithFormat:@"%@, %@, %@, %@\n", [dateFormatter stringFromDate:next.timestamp],
+                         [next displayStringForActivityType], next.success ? @"Success" : @"Failed",
+                         next.detail];
+    [dumpContent appendString:nextRecordLog];
+  }
+  self.logTextView.text = dumpContent;
+}
+
+- (void)viewDidLoad {
+  [super viewDidLoad];
+  // Do any additional setup after loading the view.
+}
+
+- (void)didReceiveMemoryWarning {
+  [super didReceiveMemoryWarning];
+  // Dispose of any resources that can be recreated.
+}
+@end

+ 34 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/Podfile

@@ -0,0 +1,34 @@
+
+use_frameworks!
+
+
+target 'InAppMessaging_Example_iOS' do
+  platform :ios, '8.0'
+
+  pod 'FirebaseCommunity/InAppMessaging', :path => '../..'
+  # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency
+  # to FirebaseCore
+  # pod 'FirebaseInstanceID', '1.0.9'
+
+  #target 'InAppMessaging_Tests_iOS' do
+  #  inherit! :search_paths
+  #  pod 'FirebaseCommunity/InAppMessaging', :path => '../..'
+  #  pod 'OCMock'
+  #end
+end
+
+
+#target 'InAppMessaging_Example_Swift_iOS' do
+#  platform :ios, '8.0'
+
+#  pod 'FirebaseCommunity/InAppMessaging', :path => '../'
+  # Lock to the 1.0.9 version of InstanceID since 1.0.10 added a dependency
+  # to FirebaseCore
+#  pod 'FirebaseInstanceID', '1.0.9'
+
+  #target 'Messaging_Tests_iOS' do
+  #  inherit! :search_paths
+  #  pod 'OCMock'
+  #end
+#end
+

+ 24 - 0
InAppMessaging/Example/App/InAppMessaging-Example-iOS/main.m

@@ -0,0 +1,24 @@
+/*
+ * Copyright 2017 Google
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#import <UIKit/UIKit.h>
+#import "AppDelegate.h"
+
+int main(int argc, char* argv[]) {
+  @autoreleasepool {
+    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
+  }
+}

+ 28 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/GoogleService-Info.plist

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+  <dict>
+    <key>API_KEY</key>
+    <string>correct_api_key</string>
+    <key>TRACKING_ID</key>
+    <string>correct_tracking_id</string>
+    <key>CLIENT_ID</key>
+    <string>correct_client_id</string>
+    <key>REVERSED_CLIENT_ID</key>
+    <string>correct_reversed_client_id</string>
+    <key>GOOGLE_APP_ID</key>
+    <string>1:123:ios:123abc</string>
+    <key>GCM_SENDER_ID</key>
+    <string>correct_gcm_sender_id</string>
+    <key>PLIST_VERSION</key>
+    <string>1</string>
+    <key>BUNDLE_ID</key>
+    <string>com.google.FirebaseSDKTests</string>
+    <key>PROJECT_ID</key>
+    <string>abc-xyz-123</string>
+    <key>DATABASE_URL</key>
+    <string>https://abc-xyz-123.firebaseio.com</string>
+    <key>STORAGE_BUCKET</key>
+    <string>project-id-123.storage.firebase.com</string>
+  </dict>
+</plist>

+ 18 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/Podfile

@@ -0,0 +1,18 @@
+# Uncomment the next line to define a global platform for your project
+platform :ios, '8.4'
+
+# uncomment the follow two lines if you are trying to test internal releases
+#source 'sso://cpdc-internal/spec.git'
+#source 'https://github.com/CocoaPods/Specs.git'
+
+use_frameworks!
+
+target 'fiam-external-ios-testing-app' do
+  # Uncomment the next line if you're using Swift or would like to use dynamic frameworks
+  # use_frameworks!
+
+  # Pods for fiam-external-ios-testing-app
+  pod 'Firebase/Core'
+  pod 'Firebase/InAppMessagingDisplay'
+  pod 'Firebase/DynamicLinks'
+end

+ 423 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app.xcodeproj/project.pbxproj

@@ -0,0 +1,423 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 48;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */; };
+		AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C61FE1B0A800378AE0 /* AppDelegate.m */; };
+		AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649C91FE1B0A800378AE0 /* ViewController.m */; };
+		AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CB1FE1B0A800378AE0 /* Main.storyboard */; };
+		AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */; };
+		AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */; };
+		AD7649D51FE1B0A800378AE0 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AD7649D41FE1B0A800378AE0 /* main.m */; };
+		AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_fiam_external_ios_testing_app.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.debug.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.debug.xcconfig"; sourceTree = "<group>"; };
+		AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "fiam-external-ios-testing-app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		AD7649C51FE1B0A800378AE0 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = "<group>"; };
+		AD7649C61FE1B0A800378AE0 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = "<group>"; };
+		AD7649C81FE1B0A800378AE0 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = "<group>"; };
+		AD7649C91FE1B0A800378AE0 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = "<group>"; };
+		AD7649CC1FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
+		AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		AD7649D11FE1B0A800378AE0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
+		AD7649D31FE1B0A800378AE0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		AD7649D41FE1B0A800378AE0 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
+		AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = SOURCE_ROOT; };
+		EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-fiam-external-ios-testing-app.release.xcconfig"; path = "Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		AD7649BF1FE1B0A800378AE0 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				7D31D8493943DCD77743922A /* Pods_fiam_external_ios_testing_app.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		2F924E232047E700385C2AFA /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				12F51E141FEC71BA1CE57DC4 /* Pods_fiam_external_ios_testing_app.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		4DC924B9E0562D822E3E68F3 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */,
+				EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */,
+			);
+			name = Pods;
+			sourceTree = "<group>";
+		};
+		AD7649B91FE1B0A800378AE0 = {
+			isa = PBXGroup;
+			children = (
+				AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */,
+				AD7649C31FE1B0A800378AE0 /* Products */,
+				4DC924B9E0562D822E3E68F3 /* Pods */,
+				2F924E232047E700385C2AFA /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		AD7649C31FE1B0A800378AE0 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		AD7649C41FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = {
+			isa = PBXGroup;
+			children = (
+				AD7649DB1FE1B57A00378AE0 /* GoogleService-Info.plist */,
+				AD7649C51FE1B0A800378AE0 /* AppDelegate.h */,
+				AD7649C61FE1B0A800378AE0 /* AppDelegate.m */,
+				AD7649C81FE1B0A800378AE0 /* ViewController.h */,
+				AD7649C91FE1B0A800378AE0 /* ViewController.m */,
+				AD7649CB1FE1B0A800378AE0 /* Main.storyboard */,
+				AD7649CE1FE1B0A800378AE0 /* Assets.xcassets */,
+				AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */,
+				AD7649D31FE1B0A800378AE0 /* Info.plist */,
+				AD7649D41FE1B0A800378AE0 /* main.m */,
+			);
+			path = "fiam-external-ios-testing-app";
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */;
+			buildPhases = (
+				89F39EB5CA1632B8B86E938F /* [CP] Check Pods Manifest.lock */,
+				AD7649BE1FE1B0A800378AE0 /* Sources */,
+				AD7649BF1FE1B0A800378AE0 /* Frameworks */,
+				AD7649C01FE1B0A800378AE0 /* Resources */,
+				AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */,
+				638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "fiam-external-ios-testing-app";
+			productName = "fiam-external-ios-testing-app";
+			productReference = AD7649C21FE1B0A800378AE0 /* fiam-external-ios-testing-app.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		AD7649BA1FE1B0A800378AE0 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				LastUpgradeCheck = 0910;
+				ORGANIZATIONNAME = "Yong Mao";
+				TargetAttributes = {
+					AD7649C11FE1B0A800378AE0 = {
+						CreatedOnToolsVersion = 9.1;
+						ProvisioningStyle = Automatic;
+					};
+				};
+			};
+			buildConfigurationList = AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */;
+			compatibilityVersion = "Xcode 8.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = AD7649B91FE1B0A800378AE0;
+			productRefGroup = AD7649C31FE1B0A800378AE0 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				AD7649C11FE1B0A800378AE0 /* fiam-external-ios-testing-app */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		AD7649C01FE1B0A800378AE0 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				AD7649D21FE1B0A800378AE0 /* LaunchScreen.storyboard in Resources */,
+				AD7649DC1FE1B57A00378AE0 /* GoogleService-Info.plist in Resources */,
+				AD7649CF1FE1B0A800378AE0 /* Assets.xcassets in Resources */,
+				AD7649CD1FE1B0A800378AE0 /* Main.storyboard in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		638CE7E9369C7DEB067D82E7 /* [CP] Copy Pods Resources */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-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-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-resources.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		89F39EB5CA1632B8B86E938F /* [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-fiam-external-ios-testing-app-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;
+		};
+		AF2C898A9A08823BD10E1650 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputPaths = (
+				"${SRCROOT}/Pods/Target Support Files/Pods-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-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-fiam-external-ios-testing-app/Pods-fiam-external-ios-testing-app-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		AD7649BE1FE1B0A800378AE0 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				AD7649CA1FE1B0A800378AE0 /* ViewController.m in Sources */,
+				AD7649D51FE1B0A800378AE0 /* main.m in Sources */,
+				AD7649C71FE1B0A800378AE0 /* AppDelegate.m in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+		AD7649CB1FE1B0A800378AE0 /* Main.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				AD7649CC1FE1B0A800378AE0 /* Base */,
+			);
+			name = Main.storyboard;
+			sourceTree = "<group>";
+		};
+		AD7649D01FE1B0A800378AE0 /* LaunchScreen.storyboard */ = {
+			isa = PBXVariantGroup;
+			children = (
+				AD7649D11FE1B0A800378AE0 /* Base */,
+			);
+			name = LaunchScreen.storyboard;
+			sourceTree = "<group>";
+		};
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+		AD7649D61FE1B0A800378AE0 /* 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_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = 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_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.1;
+				MTL_ENABLE_DEBUG_INFO = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+			};
+			name = Debug;
+		};
+		AD7649D71FE1B0A800378AE0 /* 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_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = 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_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.1;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				SDKROOT = iphoneos;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		AD7649D91FE1B0A800378AE0 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 61D649CA05E938083C88FC6D /* Pods-fiam-external-ios-testing-app.debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = EQHXZ8M8AV;
+				INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.4;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		AD7649DA1FE1B0A800378AE0 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = EE0B5FD5B23F372E4894C799 /* Pods-fiam-external-ios-testing-app.release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				CODE_SIGN_STYLE = Automatic;
+				DEVELOPMENT_TEAM = EQHXZ8M8AV;
+				INFOPLIST_FILE = "fiam-external-ios-testing-app/Info.plist";
+				IPHONEOS_DEPLOYMENT_TARGET = 8.4;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				PRODUCT_BUNDLE_IDENTIFIER = "com.google.fiam-external-ios-testing";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		AD7649BD1FE1B0A800378AE0 /* Build configuration list for PBXProject "fiam-external-ios-testing-app" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				AD7649D61FE1B0A800378AE0 /* Debug */,
+				AD7649D71FE1B0A800378AE0 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		AD7649D81FE1B0A800378AE0 /* Build configuration list for PBXNativeTarget "fiam-external-ios-testing-app" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				AD7649D91FE1B0A800378AE0 /* Debug */,
+				AD7649DA1FE1B0A800378AE0 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = AD7649BA1FE1B0A800378AE0 /* Project object */;
+}

+ 21 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.h

@@ -0,0 +1,21 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import <UIKit/UIKit.h>
+
+@interface AppDelegate : UIResponder <UIApplicationDelegate>
+
+@property(strong, nonatomic) UIWindow *window;
+
+@end

+ 70 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/AppDelegate.m

@@ -0,0 +1,70 @@
+// Copyright 2017 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#import "AppDelegate.h"
+
+@import Firebase;
+
+@interface AppDelegate ()
+
+@end
+
+@implementation AppDelegate
+
+- (BOOL)application:(UIApplication *)application
+    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
+  // Override point for customization after application launch.
+  // uncomment the following line for disabling the auto startup
+  // of the sdk
+  // [FIRInAppMessaging inAppMessaging].automaticDataCollectionEnabled = @NO;
+
+  [FIROptions defaultOptions].deepLinkURLScheme = @"com.google.InAppMessagingExampleiOS";
+  [FIRApp configure];
+  return YES;
+}
+
+- (BOOL)application:(UIApplication *)app
+            openURL:(NSURL *)url
+            options:(NSDictionary<NSString *, id> *)options {
+  NSLog(@"called here 1");
+  return [self application:app
+                   openURL:url
+         sourceApplication:options[UIApplicationOpenURLOptionsSourceApplicationKey]
+                annotation:options[UIApplicationOpenURLOptionsAnnotationKey]];
+}
+
+- (BOOL)application:(UIApplication *)application
+              openURL:(NSURL *)url
+    sourceApplication:(NSString *)sourceApplication
+           annotation:(id)annotation {
+  FIRDynamicLink *dynamicLink = [[FIRDynamicLinks dynamicLinks] dynamicLinkFromCustomSchemeURL:url];
+
+  NSLog(@"called here with %@", dynamicLink);
+  if (dynamicLink) {
+    if (dynamicLink.url) {
+      // Handle the deep link. For example, show the deep-linked content,
+      // apply a promotional offer to the user's account or show customized onboarding view.
+      // ...
+
+    } else {
+      // Dynamic link has empty deep link. This situation will happens if
+      // Firebase Dynamic Links iOS SDK tried to retrieve pending dynamic link,
+      // but pending link is not available for this device/App combination.
+      // At this point you may display default onboarding view.
+    }
+    return YES;
+  }
+  return NO;
+}
+@end

+ 93 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,93 @@
+{
+  "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"
+    }
+  ],
+  "info" : {
+    "version" : 1,
+    "author" : "xcode"
+  }
+}

+ 31 - 0
InAppMessaging/Example/ExternalAppExample/fiam-external-ios-testing-app/fiam-external-ios-testing-app/Base.lproj/LaunchScreen.storyboard

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
+    <device id="retina4_7" orientation="portrait">
+        <adaptation id="fullscreen"/>
+    </device>
+    <dependencies>
+        <deployment identifier="iOS"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13527"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
+    </dependencies>
+    <scenes>
+        <!--View Controller-->
+        <scene sceneID="EHf-IW-A2E">
+            <objects>
+                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
+                    <layoutGuides>
+                        <viewControllerLayoutGuide type="top" id="H25-Ju-sNp"/>
+                        <viewControllerLayoutGuide type="bottom" id="e2z-89-rZs"/>
+                    </layoutGuides>
+                    <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
+                        <rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
+                        <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
+                    </view>
+                </viewController>
+                <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
+            </objects>
+            <point key="canvasLocation" x="53" y="375"/>
+        </scene>
+    </scenes>
+</document>

Деякі файли не було показано, через те що забагато файлів було змінено