FIRIAMMsgFetcherUsingRestful.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import <TargetConditionals.h>
  17. #if TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)
  18. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  19. #import "FirebaseInAppMessaging/Sources/FIRCore+InAppMessaging.h"
  20. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMFetchResponseParser.h"
  21. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageContentDataWithImageURL.h"
  22. #import "FirebaseInAppMessaging/Sources/Private/Data/FIRIAMMessageDefinition.h"
  23. #import "FirebaseInAppMessaging/Sources/Private/Flows/FIRIAMMsgFetcherUsingRestful.h"
  24. #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMFetchFlow.h"
  25. #import "FirebaseInAppMessaging/Sources/Private/Runtime/FIRIAMSDKSettings.h"
  26. static NSInteger const SuccessHTTPStatusCode = 200;
  27. @interface FIRIAMMsgFetcherUsingRestful ()
  28. @property(readonly) NSURLSession *URLSession;
  29. @property(readonly, copy, nonatomic) NSString *serverHostName;
  30. @property(readonly, copy, nonatomic) NSString *appBundleID;
  31. @property(readonly, copy, nonatomic) NSString *httpProtocol;
  32. @property(readonly, copy, nonatomic) NSString *fbProjectNumber;
  33. @property(readonly, copy, nonatomic) NSString *apiKey;
  34. @property(readonly, copy, nonatomic) NSString *firebaseAppId;
  35. @property(readonly, nonatomic) FIRIAMServerMsgFetchStorage *fetchStorage;
  36. @property(readonly, nonatomic) FIRIAMClientInfoFetcher *clientInfoFetcher;
  37. @property(readonly, nonatomic) FIRIAMFetchResponseParser *responseParser;
  38. @end
  39. @implementation FIRIAMMsgFetcherUsingRestful
  40. - (instancetype)initWithHost:(NSString *)serverHost
  41. HTTPProtocol:(NSString *)HTTPProtocol
  42. project:(NSString *)fbProjectNumber
  43. firebaseApp:(NSString *)fbAppId
  44. APIKey:(NSString *)apiKey
  45. fetchStorage:(FIRIAMServerMsgFetchStorage *)fetchStorage
  46. instanceIDFetcher:(FIRIAMClientInfoFetcher *)clientInfoFetcher
  47. usingURLSession:(nullable NSURLSession *)URLSession
  48. responseParser:(FIRIAMFetchResponseParser *)responseParser {
  49. if (self = [super init]) {
  50. _URLSession = URLSession ? URLSession : [NSURLSession sharedSession];
  51. _serverHostName = [serverHost copy];
  52. _fbProjectNumber = [fbProjectNumber copy];
  53. _firebaseAppId = [fbAppId copy];
  54. _httpProtocol = [HTTPProtocol copy];
  55. _apiKey = [apiKey copy];
  56. _clientInfoFetcher = clientInfoFetcher;
  57. _fetchStorage = fetchStorage;
  58. _appBundleID = [NSBundle mainBundle].bundleIdentifier;
  59. _responseParser = responseParser;
  60. }
  61. return self;
  62. }
  63. - (void)updatePostFetchData:(NSMutableDictionary *)postData
  64. withImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressionList
  65. instanceIDString:(nonnull NSString *)IIDValue
  66. IIDToken:(nonnull NSString *)IIDToken {
  67. NSMutableArray *impressionListForPost = [[NSMutableArray alloc] init];
  68. for (FIRIAMImpressionRecord *nextImpressionRecord in impressionList) {
  69. NSDictionary *nextImpression = @{
  70. @"campaign_id" : nextImpressionRecord.messageID,
  71. @"impression_timestamp_millis" : @(nextImpressionRecord.impressionTimeInSeconds * 1000)
  72. };
  73. [impressionListForPost addObject:nextImpression];
  74. }
  75. [postData setObject:impressionListForPost forKey:@"already_seen_campaigns"];
  76. if (IIDValue) {
  77. NSDictionary *clientAppInfo = @{
  78. @"gmp_app_id" : self.firebaseAppId,
  79. @"app_instance_id" : IIDValue,
  80. @"app_instance_id_token" : IIDToken
  81. };
  82. [postData setObject:clientAppInfo forKey:@"requesting_client_app"];
  83. }
  84. NSMutableArray *clientSignals = [@{} mutableCopy];
  85. // set client signal fields only when they are present
  86. if ([self.clientInfoFetcher getAppVersion]) {
  87. [clientSignals setValue:[self.clientInfoFetcher getAppVersion] forKey:@"app_version"];
  88. }
  89. if ([self.clientInfoFetcher getOSVersion]) {
  90. [clientSignals setValue:[self.clientInfoFetcher getOSVersion] forKey:@"platform_version"];
  91. }
  92. if ([self.clientInfoFetcher getDeviceLanguageCode]) {
  93. [clientSignals setValue:[self.clientInfoFetcher getDeviceLanguageCode] forKey:@"language_code"];
  94. }
  95. if ([self.clientInfoFetcher getTimezone]) {
  96. [clientSignals setValue:[self.clientInfoFetcher getTimezone] forKey:@"time_zone"];
  97. }
  98. [postData setObject:clientSignals forKey:@"client_signals"];
  99. }
  100. - (void)fetchMessagesWithImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressonList
  101. withIIDvalue:(NSString *)iidValue
  102. IIDToken:(NSString *)iidToken
  103. completion:(FIRIAMFetchMessageCompletionHandler)completion {
  104. NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
  105. [request setHTTPMethod:@"POST"];
  106. if (_appBundleID.length) {
  107. // Handle the case in which the API key is being restricted to specific iOS app bundle,
  108. // which can be set on Google Cloud console side for API key credentials.
  109. [request addValue:_appBundleID forHTTPHeaderField:@"X-Ios-Bundle-Identifier"];
  110. }
  111. [request addValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
  112. [request addValue:@"application/json" forHTTPHeaderField:@"Accept"];
  113. [request addValue:iidToken forHTTPHeaderField:@"x-goog-firebase-installations-auth"];
  114. NSMutableDictionary *postFetchDict = [[NSMutableDictionary alloc] init];
  115. [self updatePostFetchData:postFetchDict
  116. withImpressionList:impressonList
  117. instanceIDString:iidValue
  118. IIDToken:iidToken];
  119. NSData *postFetchData = [NSJSONSerialization dataWithJSONObject:postFetchDict
  120. options:0
  121. error:nil];
  122. NSString *requestURLString = [NSString
  123. stringWithFormat:@"%@://%@/v1/sdkServing/projects/%@/eligibleCampaigns:fetch?key=%@",
  124. self.httpProtocol, self.serverHostName, self.fbProjectNumber, self.apiKey];
  125. [request setURL:[NSURL URLWithString:requestURLString]];
  126. [request setHTTPBody:postFetchData];
  127. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130001",
  128. @"Making a restful API request for pulling messages with fetch POST body as %@ "
  129. "and request headers as %@",
  130. postFetchDict, request.allHTTPHeaderFields);
  131. NSURLSessionDataTask *postDataTask = [self.URLSession
  132. dataTaskWithRequest:request
  133. completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
  134. if (error) {
  135. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130002",
  136. @"Internal error: encountered error in pulling messages from server"
  137. ":%@",
  138. error);
  139. completion(nil, nil, 0, error);
  140. } else {
  141. if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
  142. NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
  143. if (httpResponse.statusCode == SuccessHTTPStatusCode) {
  144. // got response data successfully
  145. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130007",
  146. @"Fetch API response headers are %@", [httpResponse allHeaderFields]);
  147. NSError *errorJson = nil;
  148. NSDictionary *responseDict = [NSJSONSerialization JSONObjectWithData:data
  149. options:kNilOptions
  150. error:&errorJson];
  151. if (errorJson) {
  152. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130003",
  153. @"Failed to parse the response body as JSON string %@", errorJson);
  154. completion(nil, nil, 0, errorJson);
  155. } else {
  156. NSInteger discardCount;
  157. NSNumber *nextFetchWaitTimeFromResponse;
  158. NSArray<FIRIAMMessageDefinition *> *messages = [self.responseParser
  159. parseAPIResponseDictionary:responseDict
  160. discardedMsgCount:&discardCount
  161. fetchWaitTimeInSeconds:&nextFetchWaitTimeFromResponse];
  162. if (messages) {
  163. FIRLogDebug(kFIRLoggerInAppMessaging, @"I-IAM130012",
  164. @"API request for fetching messages and parsing the response was "
  165. "successful.");
  166. [self.fetchStorage
  167. saveResponseDictionary:responseDict
  168. withCompletion:^(BOOL success) {
  169. if (!success)
  170. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130010",
  171. @"Failed to persist server fetch response");
  172. }];
  173. // always report success regardless of whether we are able to persist into
  174. // storage. they should get fixed in the next fetch cycle if it happens.
  175. completion(messages, nextFetchWaitTimeFromResponse, discardCount, nil);
  176. } else {
  177. NSString *errorDesc =
  178. @"Failed to recognize the fiam messages in the server response";
  179. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130011", @"%@", errorDesc);
  180. NSError *error =
  181. [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
  182. code:0
  183. userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
  184. completion(nil, nil, 0, error);
  185. }
  186. }
  187. } else {
  188. NSString *responseBody = [[NSString alloc] initWithData:data
  189. encoding:NSUTF8StringEncoding];
  190. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130004",
  191. @"Failed restful api request to fetch in-app messages: seeing http "
  192. @"status code as %ld with body as %@",
  193. (long)httpResponse.statusCode, responseBody);
  194. NSError *error = [NSError errorWithDomain:NSURLErrorDomain
  195. code:httpResponse.statusCode
  196. userInfo:nil];
  197. completion(nil, nil, 0, error);
  198. }
  199. } else {
  200. NSString *errorDesc = @"Got a non http response type from fetch endpoint";
  201. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130005", @"%@", errorDesc);
  202. NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
  203. code:0
  204. userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
  205. completion(nil, nil, 0, error);
  206. }
  207. }
  208. }];
  209. if (postDataTask == nil) {
  210. NSString *errorDesc =
  211. @"Internal error: NSURLSessionDataTask failed to be created due to possibly "
  212. "incorrect parameters";
  213. FIRLogWarning(kFIRLoggerInAppMessaging, @"I-IAM130006", @"%@", errorDesc);
  214. NSError *error = [NSError errorWithDomain:kFirebaseInAppMessagingErrorDomain
  215. code:0
  216. userInfo:@{NSLocalizedDescriptionKey : errorDesc}];
  217. completion(nil, nil, 0, error);
  218. } else {
  219. [postDataTask resume];
  220. }
  221. }
  222. #pragma mark - protocol FIRIAMMessageFetcher
  223. - (void)fetchMessagesWithImpressionList:(NSArray<FIRIAMImpressionRecord *> *)impressonList
  224. withCompletion:(FIRIAMFetchMessageCompletionHandler)completion {
  225. // First step is to fetch the instance id value and token on the fly. We are not caching the data
  226. // since the fetch operation frequency is low enough that we are not concerned about its impact
  227. // on server load and this guarantees that we always have an up-to-date iid values and tokens.
  228. [self.clientInfoFetcher
  229. fetchFirebaseInstallationDataWithProjectNumber:self.fbProjectNumber
  230. withCompletion:^(NSString *_Nullable FID,
  231. NSString *_Nullable FISToken,
  232. NSError *_Nullable error) {
  233. if (error) {
  234. FIRLogWarning(
  235. kFIRLoggerInAppMessaging, @"I-IAM130008",
  236. @"Not able to get iid value and/or token for "
  237. @"talking to server: %@",
  238. error.localizedDescription);
  239. completion(nil, nil, 0, error);
  240. } else {
  241. [self fetchMessagesWithImpressionList:impressonList
  242. withIIDvalue:FID
  243. IIDToken:FISToken
  244. completion:completion];
  245. }
  246. }];
  247. }
  248. @end
  249. #endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) && TARGET_OS_VISION)