FIRDynamicLinkNetworking.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. /*
  2. * Copyright 2018 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
  18. #import "FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking+Private.h"
  19. #import "FirebaseDynamicLinks/Sources/GINInvocation/GINArgument.h"
  20. #import "FirebaseDynamicLinks/Sources/GINInvocation/GINInvocation.h"
  21. #import "FirebaseDynamicLinks/Sources/Utilities/FDLDeviceHeuristicsHelper.h"
  22. #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h"
  23. NS_ASSUME_NONNULL_BEGIN
  24. NSString *const kApiaryRestBaseUrl = @"https://appinvite-pa.googleapis.com/v1";
  25. static NSString *const kiOSReopenRestBaseUrl = @"https://firebasedynamiclinks.googleapis.com/v1";
  26. // Endpoint for default retrieval process V2. (Endpoint version is V1)
  27. static NSString *const kIosPostInstallAttributionRestBaseUrl =
  28. @"https://firebasedynamiclinks.googleapis.com/v1";
  29. static NSString *const kReasonString = @"reason";
  30. static NSString *const kiOSInviteReason = @"ios_invite";
  31. NSString *const kFDLResolvedLinkDeepLinkURLKey = @"deepLink";
  32. NSString *const kFDLResolvedLinkMinAppVersionKey = @"iosMinAppVersion";
  33. static NSString *const kFDLAnalyticsDataSourceKey = @"utmSource";
  34. static NSString *const kFDLAnalyticsDataMediumKey = @"utmMedium";
  35. static NSString *const kFDLAnalyticsDataCampaignKey = @"utmCampaign";
  36. static NSString *const kHeaderIosBundleIdentifier = @"X-Ios-Bundle-Identifier";
  37. static NSString *const kGenericErrorDomain = @"com.firebase.dynamicLinks";
  38. typedef NSDictionary *_Nullable (^FIRDLNetworkingParserBlock)(
  39. NSString *requestURLString,
  40. NSData *data,
  41. NSString *_Nullable *_Nonnull matchMessagePtr,
  42. NSError *_Nullable *_Nullable errorPtr);
  43. NSString *FIRURLParameterString(NSString *key, NSString *value) {
  44. if (key.length > 0) {
  45. return [NSString stringWithFormat:@"?%@=%@", key, value];
  46. }
  47. return @"";
  48. }
  49. NSString *_Nullable FIRDynamicLinkAPIKeyParameter(NSString *apiKey) {
  50. return apiKey ? FIRURLParameterString(@"key", apiKey) : nil;
  51. }
  52. void FIRMakeHTTPRequest(NSURLRequest *request, FIRNetworkRequestCompletionHandler completion) {
  53. NSURLSessionConfiguration *sessionConfig =
  54. [NSURLSessionConfiguration defaultSessionConfiguration];
  55. NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig];
  56. NSURLSessionDataTask *dataTask =
  57. [session dataTaskWithRequest:request
  58. completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
  59. NSError *_Nullable error) {
  60. completion(data, response, error);
  61. }];
  62. [dataTask resume];
  63. }
  64. NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nullable error) {
  65. return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:error];
  66. }
  67. @implementation FIRDynamicLinkNetworking {
  68. NSString *_APIKey;
  69. NSString *_URLScheme;
  70. }
  71. - (instancetype)initWithAPIKey:(NSString *)APIKey URLScheme:(NSString *)URLScheme {
  72. NSParameterAssert(APIKey);
  73. NSParameterAssert(URLScheme);
  74. if (self = [super init]) {
  75. _APIKey = [APIKey copy];
  76. _URLScheme = [URLScheme copy];
  77. }
  78. return self;
  79. }
  80. + (nullable NSError *)extractErrorForShortLink:(NSURL *)url
  81. data:(NSData *)data
  82. response:(NSURLResponse *)response
  83. error:(nullable NSError *)error {
  84. if (error) {
  85. return error;
  86. }
  87. NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
  88. NSError *customError = nil;
  89. if (![response isKindOfClass:[NSHTTPURLResponse class]]) {
  90. customError =
  91. [NSError errorWithDomain:kGenericErrorDomain
  92. code:0
  93. userInfo:@{@"message" : @"Response should be of type NSHTTPURLResponse."}];
  94. } else if ((statusCode < 200 || statusCode >= 300) && data) {
  95. NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  96. if ([result isKindOfClass:[NSDictionary class]] && [result objectForKey:@"error"]) {
  97. id err = [result objectForKey:@"error"];
  98. customError = [NSError errorWithDomain:kGenericErrorDomain code:statusCode userInfo:err];
  99. } else {
  100. customError = [NSError
  101. errorWithDomain:kGenericErrorDomain
  102. code:0
  103. userInfo:@{
  104. @"message" :
  105. [NSString stringWithFormat:@"Failed to resolve link: %@", url.absoluteString]
  106. }];
  107. }
  108. }
  109. return customError;
  110. }
  111. #pragma mark - Public interface
  112. - (void)resolveShortLink:(NSURL *)url
  113. FDLSDKVersion:(NSString *)FDLSDKVersion
  114. completion:(FIRDynamicLinkResolverHandler)handler {
  115. NSParameterAssert(handler);
  116. if (!url) {
  117. handler(nil, nil);
  118. return;
  119. }
  120. NSDictionary *requestBody = @{
  121. @"requestedLink" : url.absoluteString,
  122. @"bundle_id" : [NSBundle mainBundle].bundleIdentifier,
  123. @"sdk_version" : FDLSDKVersion
  124. };
  125. FIRNetworkRequestCompletionHandler resolveLinkCallback =
  126. ^(NSData *data, NSURLResponse *response, NSError *error) {
  127. NSURL *resolvedURL = nil;
  128. NSError *extractedError = [FIRDynamicLinkNetworking extractErrorForShortLink:url
  129. data:data
  130. response:response
  131. error:error];
  132. if (!extractedError && data) {
  133. NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  134. if ([result isKindOfClass:[NSDictionary class]]) {
  135. id invitationIDObject = [result objectForKey:@"invitationId"];
  136. NSString *invitationIDString;
  137. if ([invitationIDObject isKindOfClass:[NSDictionary class]]) {
  138. NSDictionary *invitationIDDictionary = invitationIDObject;
  139. invitationIDString = invitationIDDictionary[@"id"];
  140. } else if ([invitationIDObject isKindOfClass:[NSString class]]) {
  141. invitationIDString = invitationIDObject;
  142. }
  143. NSString *deepLinkString = result[kFDLResolvedLinkDeepLinkURLKey];
  144. NSString *minAppVersion = result[kFDLResolvedLinkMinAppVersionKey];
  145. NSString *utmSource = result[kFDLAnalyticsDataSourceKey];
  146. NSString *utmMedium = result[kFDLAnalyticsDataMediumKey];
  147. NSString *utmCampaign = result[kFDLAnalyticsDataCampaignKey];
  148. resolvedURL = FIRDLDeepLinkURLWithInviteID(invitationIDString, deepLinkString,
  149. utmSource, utmMedium, utmCampaign, NO, nil,
  150. minAppVersion, self->_URLScheme, nil);
  151. }
  152. }
  153. handler(resolvedURL, extractedError);
  154. };
  155. NSString *requestURLString =
  156. [NSString stringWithFormat:@"%@/reopenAttribution%@", kiOSReopenRestBaseUrl,
  157. FIRDynamicLinkAPIKeyParameter(_APIKey)];
  158. [self executeOnePlatformRequest:requestBody
  159. forURL:requestURLString
  160. completionHandler:resolveLinkCallback];
  161. }
  162. - (void)retrievePendingDynamicLinkWithIOSVersion:(NSString *)IOSVersion
  163. resolutionHeight:(NSInteger)resolutionHeight
  164. resolutionWidth:(NSInteger)resolutionWidth
  165. locale:(NSString *)locale
  166. localeRaw:(NSString *)localeRaw
  167. localeFromWebView:(NSString *)localeFromWebView
  168. timezone:(NSString *)timezone
  169. modelName:(NSString *)modelName
  170. FDLSDKVersion:(NSString *)FDLSDKVersion
  171. appInstallationDate:(NSDate *_Nullable)appInstallationDate
  172. uniqueMatchVisualStyle:
  173. (FIRDynamicLinkNetworkingUniqueMatchVisualStyle)uniqueMatchVisualStyle
  174. retrievalProcessType:
  175. (FIRDynamicLinkNetworkingRetrievalProcessType)retrievalProcessType
  176. uniqueMatchLinkToCheck:(NSURL *)uniqueMatchLinkToCheck
  177. handler:
  178. (FIRPostInstallAttributionCompletionHandler)handler {
  179. NSParameterAssert(handler);
  180. NSMutableDictionary *requestBody = [@{
  181. @"bundleId" : [NSBundle mainBundle].bundleIdentifier,
  182. @"device" :
  183. [FDLDeviceHeuristicsHelper FDLDeviceInfoDictionaryFromResolutionHeight:resolutionHeight
  184. resolutionWidth:resolutionWidth
  185. locale:locale
  186. localeRaw:localeRaw
  187. localeFromWebview:localeFromWebView
  188. timeZone:timezone
  189. modelName:modelName],
  190. @"iosVersion" : IOSVersion,
  191. @"sdkVersion" : FDLSDKVersion,
  192. @"visualStyle" : @(uniqueMatchVisualStyle),
  193. @"retrievalMethod" : @(retrievalProcessType),
  194. } mutableCopy];
  195. if (appInstallationDate) {
  196. requestBody[@"appInstallationTime"] = @((NSInteger)[appInstallationDate timeIntervalSince1970]);
  197. }
  198. if (uniqueMatchLinkToCheck) {
  199. requestBody[@"uniqueMatchLinkToCheck"] = uniqueMatchLinkToCheck.absoluteString;
  200. }
  201. FIRDLNetworkingParserBlock responseParserBlock = ^NSDictionary *_Nullable(
  202. NSString *requestURLString, NSData *data, NSString **matchMessagePtr, NSError **errorPtr) {
  203. NSError *serializationError;
  204. NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data
  205. options:0
  206. error:&serializationError];
  207. if (serializationError) {
  208. if (errorPtr != nil) {
  209. *errorPtr = serializationError;
  210. }
  211. return nil;
  212. }
  213. NSString *matchMessage = result[@"matchMessage"];
  214. if (matchMessage.length) {
  215. *matchMessagePtr = matchMessage;
  216. }
  217. // Create the dynamic link parameters
  218. NSMutableDictionary *dynamicLinkParameters = [[NSMutableDictionary alloc] init];
  219. dynamicLinkParameters[kFIRDLParameterInviteId] = result[@"invitationId"];
  220. dynamicLinkParameters[kFIRDLParameterDeepLinkIdentifier] = result[@"deepLink"];
  221. if (result[@"deepLink"]) {
  222. dynamicLinkParameters[kFIRDLParameterMatchType] =
  223. FIRDLMatchTypeStringFromServerString(result[@"attributionConfidence"]);
  224. }
  225. dynamicLinkParameters[kFIRDLParameterSource] = result[@"utmSource"];
  226. dynamicLinkParameters[kFIRDLParameterMedium] = result[@"utmMedium"];
  227. dynamicLinkParameters[kFIRDLParameterCampaign] = result[@"utmCampaign"];
  228. dynamicLinkParameters[kFIRDLParameterMinimumAppVersion] = result[@"appMinimumVersion"];
  229. dynamicLinkParameters[kFIRDLParameterRequestIPVersion] = result[@"requestIpVersion"];
  230. dynamicLinkParameters[kFIRDLParameterMatchMessage] = matchMessage;
  231. return [dynamicLinkParameters copy];
  232. };
  233. [self sendRequestWithBaseURLString:kIosPostInstallAttributionRestBaseUrl
  234. requestBody:requestBody
  235. endpointPath:@"installAttribution"
  236. parserBlock:responseParserBlock
  237. completion:handler];
  238. }
  239. - (void)convertInvitation:(NSString *)invitationID
  240. handler:(nullable FIRDynamicLinkNetworkingErrorHandler)handler {
  241. if (!invitationID) {
  242. return;
  243. }
  244. NSDictionary *requestBody = @{
  245. @"invitationId" : @{@"id" : invitationID},
  246. @"containerClientId" : @{
  247. @"type" : @"IOS",
  248. }
  249. };
  250. FIRNetworkRequestCompletionHandler convertInvitationCallback =
  251. ^(NSData *data, NSURLResponse *response, NSError *error) {
  252. if (handler) {
  253. dispatch_async(dispatch_get_main_queue(), ^{
  254. handler(error);
  255. });
  256. }
  257. };
  258. NSString *requestURL = [NSString stringWithFormat:@"%@/convertInvitation%@", kApiaryRestBaseUrl,
  259. FIRDynamicLinkAPIKeyParameter(_APIKey)];
  260. [self executeOnePlatformRequest:requestBody
  261. forURL:requestURL
  262. completionHandler:convertInvitationCallback];
  263. }
  264. #pragma mark - Internal methods
  265. - (void)sendRequestWithBaseURLString:(NSString *)baseURL
  266. requestBody:(NSDictionary *)requestBody
  267. endpointPath:(NSString *)endpointPath
  268. parserBlock:(FIRDLNetworkingParserBlock)parserBlock
  269. completion:(FIRPostInstallAttributionCompletionHandler)handler {
  270. NSParameterAssert(handler);
  271. NSString *requestURLString = [NSString
  272. stringWithFormat:@"%@/%@%@", baseURL, endpointPath, FIRDynamicLinkAPIKeyParameter(_APIKey)];
  273. FIRNetworkRequestCompletionHandler completeInvitationByDeviceCallback =
  274. ^(NSData *data, NSURLResponse *response, NSError *error) {
  275. if (error || !data) {
  276. dispatch_async(dispatch_get_main_queue(), ^{
  277. handler(nil, nil, error);
  278. });
  279. return;
  280. }
  281. NSString *matchMessage = nil;
  282. NSError *parsingError = nil;
  283. NSDictionary *parsedDynamicLinkParameters =
  284. parserBlock(requestURLString, data, &matchMessage, &parsingError);
  285. dispatch_async(dispatch_get_main_queue(), ^{
  286. handler(parsedDynamicLinkParameters, matchMessage, parsingError);
  287. });
  288. };
  289. [self executeOnePlatformRequest:requestBody
  290. forURL:requestURLString
  291. completionHandler:completeInvitationByDeviceCallback];
  292. }
  293. - (void)executeOnePlatformRequest:(NSDictionary *)requestBody
  294. forURL:(NSString *)requestURLString
  295. completionHandler:(FIRNetworkRequestCompletionHandler)handler {
  296. NSURL *requestURL = [NSURL URLWithString:requestURLString];
  297. NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:requestURL];
  298. // TODO: Verify that HTTPBody and HTTPMethod are iOS 8+ and find an alternative.
  299. request.HTTPBody = FIRDataWithDictionary(requestBody, nil);
  300. request.HTTPMethod = @"POST";
  301. [request setValue:@"application/json; charset=utf-8" forHTTPHeaderField:@"Content-Type"];
  302. // Set the iOS bundleID as a request header.
  303. NSString *bundleID = [[NSBundle mainBundle] bundleIdentifier];
  304. if (bundleID) {
  305. [request setValue:bundleID forHTTPHeaderField:kHeaderIosBundleIdentifier];
  306. }
  307. FIRMakeHTTPRequest(request, handler);
  308. }
  309. @end
  310. NS_ASSUME_NONNULL_END
  311. #endif // TARGET_OS_IOS