FIRDynamicLinkNetworking.m 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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 "DynamicLinks/FIRDynamicLinkNetworking+Private.h"
  17. #import "DynamicLinks/GINInvocation/GINArgument.h"
  18. #import "DynamicLinks/GINInvocation/GINInvocation.h"
  19. #import "DynamicLinks/Utilities/FDLUtilities.h"
  20. NS_ASSUME_NONNULL_BEGIN
  21. NSString *const kApiaryRestBaseUrl = @"https://appinvite-pa.googleapis.com/v1";
  22. // IPv4 and IPv6 Endpoints.
  23. static NSString *const kApiaryRestBaseUrlIPV4 = @"https://appinvite-ipv4-pa.googleapis.com/v1";
  24. static NSString *const kApiaryRestBaseUrlIPV6 = @"https://appinvite-ipv6-pa.googleapis.com/v1";
  25. // IPv4 and IPv6 Endpoints for default retrieval process V2. (Endpoint version is V1)
  26. static NSString *const kIosPostInstallAttributionRestBaseUrlIPV4 =
  27. @"https://firebasedynamiclinks-ipv4.googleapis.com/v1";
  28. static NSString *const kIosPostInstallAttributionRestBaseUrlIPV6 =
  29. @"https://firebasedynamiclinks-ipv6.googleapis.com/v1";
  30. static NSString *const kIosPostInstallAttributionRestBaseUrlUniqueMatch =
  31. @"https://firebasedynamiclinks.googleapis.com/v1";
  32. static NSString *const kReasonString = @"reason";
  33. static NSString *const kiOSInviteReason = @"ios_invite";
  34. NSString *const kFDLResolvedLinkDeepLinkURLKey = @"deepLink";
  35. NSString *const kFDLResolvedLinkMinAppVersionKey = @"iosMinAppVersion";
  36. static NSString *const kFDLAnalyticsDataKey = @"durableLinkAnalyticsData";
  37. static NSString *const kFDLAnalyticsDataSourceKey = @"source";
  38. static NSString *const kFDLAnalyticsDataMediumKey = @"medium";
  39. static NSString *const kFDLAnalyticsDataCampaignKey = @"campaign";
  40. static NSString *const kHeaderIosBundleIdentifier = @"X-Ios-Bundle-Identifier";
  41. typedef NSDictionary *_Nullable (^FIRDLNetworkingParserBlock)(
  42. NSString *requestURLString,
  43. NSData *data,
  44. NSString *_Nullable *_Nonnull matchMessagePtr,
  45. NSError *_Nullable *_Nullable errorPtr);
  46. NSString *FIRURLParameterString(NSString *key, NSString *value) {
  47. if (key.length > 0) {
  48. return [NSString stringWithFormat:@"?%@=%@", key, value];
  49. }
  50. return @"";
  51. }
  52. NSString *_Nullable FIRDynamicLinkAPIKeyParameter(NSString *apiKey) {
  53. return apiKey ? FIRURLParameterString(@"key", apiKey) : nil;
  54. }
  55. void FIRMakeHTTPRequest(NSURLRequest *request, FIRNetworkRequestCompletionHandler completion) {
  56. NSURLSession *session = [NSURLSession sharedSession];
  57. NSURLSessionDataTask *dataTask =
  58. [session dataTaskWithRequest:request
  59. completionHandler:^(NSData *_Nullable data, NSURLResponse *_Nullable response,
  60. NSError *_Nullable error) {
  61. completion(data, error);
  62. }];
  63. [dataTask resume];
  64. }
  65. NSData *_Nullable FIRDataWithDictionary(NSDictionary *dictionary, NSError **_Nullable error) {
  66. return [NSJSONSerialization dataWithJSONObject:dictionary options:0 error:error];
  67. }
  68. @implementation FIRDynamicLinkNetworking {
  69. NSString *_APIKey;
  70. NSString *_clientID;
  71. NSString *_URLScheme;
  72. }
  73. - (instancetype)initWithAPIKey:(NSString *)APIKey
  74. clientID:(NSString *)clientID
  75. URLScheme:(NSString *)URLScheme {
  76. NSParameterAssert(APIKey);
  77. NSParameterAssert(clientID);
  78. NSParameterAssert(URLScheme);
  79. if (self = [super init]) {
  80. _APIKey = [APIKey copy];
  81. _clientID = [clientID copy];
  82. _URLScheme = [URLScheme copy];
  83. }
  84. return self;
  85. }
  86. #pragma mark - Public interface
  87. - (void)resolveShortLink:(NSURL *)url completion:(FIRDynamicLinkResolverHandler)handler {
  88. NSParameterAssert(handler);
  89. if (!url) {
  90. handler(nil, nil);
  91. return;
  92. }
  93. NSDictionary *requestBody = @{@"link" : url.absoluteString};
  94. FIRNetworkRequestCompletionHandler resolveLinkCallback = ^(NSData *data, NSError *error) {
  95. NSURL *resolvedURL;
  96. if (!error && data) {
  97. NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
  98. if ([result isKindOfClass:[NSDictionary class]]) {
  99. id invitationIDObject = [result objectForKey:@"invitationId"];
  100. NSString *invitationIDString;
  101. if ([invitationIDObject isKindOfClass:[NSDictionary class]]) {
  102. NSDictionary *invitationIDDictionary = invitationIDObject;
  103. invitationIDString = invitationIDDictionary[@"id"];
  104. } else if ([invitationIDObject isKindOfClass:[NSString class]]) {
  105. invitationIDString = invitationIDObject;
  106. }
  107. NSString *deepLinkString = result[kFDLResolvedLinkDeepLinkURLKey];
  108. NSString *minAppVersion = result[kFDLResolvedLinkMinAppVersionKey];
  109. NSDictionary *analytics = result[kFDLAnalyticsDataKey];
  110. NSString *utmSource = analytics[kFDLAnalyticsDataSourceKey];
  111. NSString *utmMedium = analytics[kFDLAnalyticsDataMediumKey];
  112. NSString *utmCampaign = analytics[kFDLAnalyticsDataCampaignKey];
  113. resolvedURL = FIRDLDeepLinkURLWithInviteID(invitationIDString, deepLinkString, utmSource,
  114. utmMedium, utmCampaign, NO, nil, minAppVersion,
  115. self->_URLScheme, nil);
  116. }
  117. }
  118. handler(resolvedURL, error);
  119. };
  120. NSString *requestURLString = [NSString stringWithFormat:@"%@/resolveLink%@", kApiaryRestBaseUrl,
  121. FIRDynamicLinkAPIKeyParameter(_APIKey)];
  122. [self executeOnePlatformRequest:requestBody
  123. forURL:requestURLString
  124. completionHandler:resolveLinkCallback];
  125. }
  126. - (void)retrievePendingDynamicLinkWithIOSVersion:(NSString *)IOSVersion
  127. resolutionHeight:(NSInteger)resolutionHeight
  128. resolutionWidth:(NSInteger)resolutionWidth
  129. locale:(NSString *)locale
  130. localeRaw:(NSString *)localeRaw
  131. localeFromWebView:(NSString *)localeFromWebView
  132. timezone:(NSString *)timezone
  133. modelName:(NSString *)modelName
  134. FDLSDKVersion:(NSString *)FDLSDKVersion
  135. appInstallationDate:(NSDate *_Nullable)appInstallationDate
  136. uniqueMatchVisualStyle:
  137. (FIRDynamicLinkNetworkingUniqueMatchVisualStyle)uniqueMatchVisualStyle
  138. retrievalProcessType:
  139. (FIRDynamicLinkNetworkingRetrievalProcessType)retrievalProcessType
  140. uniqueMatchLinkToCheck:(NSURL *)uniqueMatchLinkToCheck
  141. handler:
  142. (FIRPostInstallAttributionCompletionHandler)handler {
  143. NSParameterAssert(handler);
  144. NSMutableDictionary *requestBody = [@{
  145. @"bundleId" : [NSBundle mainBundle].bundleIdentifier,
  146. @"device" : @{
  147. @"screenResolutionHeight" : @(resolutionHeight),
  148. @"screenResolutionWidth" : @(resolutionWidth),
  149. @"languageCode" : locale,
  150. @"languageCodeRaw" : localeRaw,
  151. @"languageCodeFromWebview" : localeFromWebView,
  152. @"timezone" : timezone,
  153. @"deviceModelName" : modelName,
  154. },
  155. @"iosVersion" : IOSVersion,
  156. @"sdkVersion" : FDLSDKVersion,
  157. @"visualStyle" : @(uniqueMatchVisualStyle),
  158. @"retrievalMethod" : @(retrievalProcessType),
  159. } mutableCopy];
  160. if (appInstallationDate) {
  161. requestBody[@"appInstallationTime"] = @((NSInteger)[appInstallationDate timeIntervalSince1970]);
  162. }
  163. if (uniqueMatchLinkToCheck) {
  164. requestBody[@"uniqueMatchLinkToCheck"] = uniqueMatchLinkToCheck.absoluteString;
  165. }
  166. FIRDLNetworkingParserBlock responseParserBlock = ^NSDictionary *_Nullable(
  167. NSString *requestURLString, NSData *data, NSString **matchMessagePtr, NSError **errorPtr) {
  168. NSError *serializationError;
  169. NSDictionary *result =
  170. [NSJSONSerialization JSONObjectWithData:data options:0 error:&serializationError];
  171. if (serializationError) {
  172. *errorPtr = serializationError;
  173. return nil;
  174. }
  175. NSString *matchMessage = result[@"matchMessage"];
  176. if (matchMessage.length) {
  177. *matchMessagePtr = matchMessage;
  178. }
  179. // Create the dynamic link parameters
  180. NSMutableDictionary *dynamicLinkParameters = [[NSMutableDictionary alloc] init];
  181. dynamicLinkParameters[kFIRDLParameterInviteId] = result[@"invitationId"];
  182. dynamicLinkParameters[kFIRDLParameterDeepLinkIdentifier] = result[@"deepLink"];
  183. if (result[@"deepLink"]) {
  184. dynamicLinkParameters[kFIRDLParameterMatchType] =
  185. FIRDLMatchTypeStringFromServerString(result[@"attributionConfidence"]);
  186. }
  187. dynamicLinkParameters[kFIRDLParameterSource] = result[@"utmSource"];
  188. dynamicLinkParameters[kFIRDLParameterMedium] = result[@"utmMedium"];
  189. dynamicLinkParameters[kFIRDLParameterCampaign] = result[@"utmCampaign"];
  190. dynamicLinkParameters[kFIRDLParameterMinimumAppVersion] = result[@"appMinimumVersion"];
  191. dynamicLinkParameters[kFIRDLParameterRequestIPVersion] = result[@"requestIpVersion"];
  192. dynamicLinkParameters[kFIRDLParameterMatchMessage] = matchMessage;
  193. return [dynamicLinkParameters copy];
  194. };
  195. // If uniqueMatch link available send to the unique match endpoint,
  196. // else send requests to both IPv4 and IPv6 endpoints.
  197. NSArray *baseURLs =
  198. uniqueMatchLinkToCheck ? @[ kIosPostInstallAttributionRestBaseUrlUniqueMatch ] : @[
  199. kIosPostInstallAttributionRestBaseUrlIPV4, kIosPostInstallAttributionRestBaseUrlIPV6
  200. ];
  201. for (NSString *baseURL in baseURLs) {
  202. [self sendRequestWithBaseURLString:baseURL
  203. requestBody:requestBody
  204. endpointPath:@"installAttribution"
  205. parserBlock:responseParserBlock
  206. completion:handler];
  207. }
  208. }
  209. - (void)convertInvitation:(NSString *)invitationID
  210. handler:(nullable FIRDynamicLinkNetworkingErrorHandler)handler {
  211. if (!invitationID) {
  212. return;
  213. }
  214. NSDictionary *requestBody = @{
  215. @"invitationId" : @{@"id" : invitationID},
  216. @"containerClientId" : @{
  217. @"type" : @"IOS",
  218. @"id" : _clientID,
  219. }
  220. };
  221. FIRNetworkRequestCompletionHandler convertInvitationCallback = ^(NSData *data, NSError *error) {
  222. if (handler) {
  223. dispatch_async(dispatch_get_main_queue(), ^{
  224. handler(error);
  225. });
  226. }
  227. };
  228. NSString *requestURL = [NSString stringWithFormat:@"%@/convertInvitation%@", kApiaryRestBaseUrl,
  229. FIRDynamicLinkAPIKeyParameter(_APIKey)];
  230. [self executeOnePlatformRequest:requestBody
  231. forURL:requestURL
  232. completionHandler:convertInvitationCallback];
  233. }
  234. #pragma mark - Internal methods
  235. - (void)sendRequestWithBaseURLString:(NSString *)baseURL
  236. requestBody:(NSDictionary *)requestBody
  237. endpointPath:(NSString *)endpointPath
  238. parserBlock:(FIRDLNetworkingParserBlock)parserBlock
  239. completion:(FIRPostInstallAttributionCompletionHandler)handler {
  240. NSParameterAssert(handler);
  241. NSString *requestURLString = [NSString
  242. stringWithFormat:@"%@/%@%@", baseURL, endpointPath, FIRDynamicLinkAPIKeyParameter(_APIKey)];
  243. FIRNetworkRequestCompletionHandler completeInvitationByDeviceCallback = ^(NSData *data,
  244. NSError *error) {
  245. if (error || !data) {
  246. dispatch_async(dispatch_get_main_queue(), ^{
  247. handler(nil, nil, error);
  248. });
  249. return;
  250. }
  251. NSString *matchMessage = nil;
  252. NSError *parsingError = nil;
  253. NSDictionary *parsedDynamicLinkParameters =
  254. parserBlock(requestURLString, data, &matchMessage, &parsingError);
  255. // If request was made with pasteboard contents, verify if we got a unique match. If we got
  256. // a "none" match, we were unable to get a unique match or deduce using fingerprinting.
  257. // In this case, resend requests to IPV4 and IPV6 endpoints for fingerprinting. b/79704203
  258. if (requestBody[@"uniqueMatchLinkToCheck"] && parsedDynamicLinkParameters &&
  259. (!parsedDynamicLinkParameters[kFIRDLParameterMatchType] ||
  260. [parsedDynamicLinkParameters[kFIRDLParameterMatchType] isEqualToString:@"none"])) {
  261. NSMutableDictionary *requestBodyMutable = [requestBody mutableCopy];
  262. [requestBodyMutable removeObjectForKey:@"uniqueMatchLinkToCheck"];
  263. NSMutableArray *baseURLs =
  264. [@[ kIosPostInstallAttributionRestBaseUrlIPV4, kIosPostInstallAttributionRestBaseUrlIPV6 ]
  265. mutableCopy];
  266. if (parsedDynamicLinkParameters[kFIRDLParameterRequestIPVersion]) {
  267. if ([parsedDynamicLinkParameters[kFIRDLParameterRequestIPVersion]
  268. isEqualToString:@"IP_V4"]) {
  269. [baseURLs removeObject:kIosPostInstallAttributionRestBaseUrlIPV4];
  270. } else if ([parsedDynamicLinkParameters[kFIRDLParameterRequestIPVersion]
  271. isEqualToString:@"IP_V6"]) {
  272. [baseURLs removeObject:kIosPostInstallAttributionRestBaseUrlIPV6];
  273. }
  274. }
  275. for (NSString *baseURL in baseURLs) {
  276. [self sendRequestWithBaseURLString:baseURL
  277. requestBody:requestBodyMutable
  278. endpointPath:@"installAttribution"
  279. parserBlock:parserBlock
  280. completion:handler];
  281. }
  282. }
  283. // We want to return out the result of the unique match check irrespective of success/failure as
  284. // it is the first fingerprinting request as well.
  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