FIRDynamicLinkNetworking.m 16 KB

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