FIRDLDefaultRetrievalProcessV2.m 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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 "FirebaseDynamicLinks/Sources/FIRDLDefaultRetrievalProcessV2.h"
  17. #import <UIKit/UIKit.h>
  18. #import "FirebaseDynamicLinks/Sources/FIRDLJavaScriptExecutor.h"
  19. #import "FirebaseDynamicLinks/Sources/FIRDLRetrievalProcessResult+Private.h"
  20. #import "FirebaseDynamicLinks/Sources/FIRDynamicLink+Private.h"
  21. #import "FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h"
  22. #import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h"
  23. // The maximum number of successful fingerprint api calls.
  24. const static NSUInteger kMaximumNumberOfSuccessfulFingerprintAPICalls = 2;
  25. // Reason for this string to ensure that only FDL links, copied to clipboard by AppPreview Page
  26. // JavaScript code, are recognized and used in copy-unique-match process. If user copied FDL to
  27. // clipboard by himself, that link must not be used in copy-unique-match process.
  28. // This constant must be kept in sync with constant in the server version at
  29. // durabledeeplink/click/ios/click_page.js
  30. static NSString *expectedCopiedLinkStringSuffix = @"_icp=1";
  31. NS_ASSUME_NONNULL_BEGIN
  32. @interface FIRDLDefaultRetrievalProcessV2 () <FIRDLJavaScriptExecutorDelegate>
  33. @property(atomic, strong) NSMutableArray *requestResults;
  34. @end
  35. @implementation FIRDLDefaultRetrievalProcessV2 {
  36. FIRDynamicLinkNetworking *_networkingService;
  37. NSString *_clientID;
  38. NSString *_URLScheme;
  39. NSString *_APIKey;
  40. NSString *_FDLSDKVersion;
  41. NSString *_clipboardContentAtMatchProcessStart;
  42. FIRDLJavaScriptExecutor *_jsExecutor;
  43. NSString *_localeFromWebView;
  44. }
  45. @synthesize delegate = _delegate;
  46. #pragma mark - Initialization
  47. - (instancetype)initWithNetworkingService:(FIRDynamicLinkNetworking *)networkingService
  48. clientID:(NSString *)clientID
  49. URLScheme:(NSString *)URLScheme
  50. APIKey:(NSString *)APIKey
  51. FDLSDKVersion:(NSString *)FDLSDKVersion
  52. delegate:(id<FIRDLRetrievalProcessDelegate>)delegate {
  53. NSParameterAssert(networkingService);
  54. NSParameterAssert(clientID);
  55. NSParameterAssert(URLScheme);
  56. NSParameterAssert(APIKey);
  57. if (self = [super init]) {
  58. _networkingService = networkingService;
  59. _clientID = [clientID copy];
  60. _URLScheme = [URLScheme copy];
  61. _APIKey = [APIKey copy];
  62. _FDLSDKVersion = [FDLSDKVersion copy];
  63. self.requestResults =
  64. [[NSMutableArray alloc] initWithCapacity:kMaximumNumberOfSuccessfulFingerprintAPICalls];
  65. _delegate = delegate;
  66. }
  67. return self;
  68. }
  69. #pragma mark - FIRDLRetrievalProcessProtocol
  70. - (void)retrievePendingDynamicLink {
  71. if (_localeFromWebView) {
  72. [self retrievePendingDynamicLinkInternal];
  73. } else {
  74. [self fetchLocaleFromWebView];
  75. }
  76. }
  77. - (BOOL)isCompleted {
  78. return self.requestResults.count >= kMaximumNumberOfSuccessfulFingerprintAPICalls;
  79. }
  80. #pragma mark - FIRDLJavaScriptExecutorDelegate
  81. - (void)javaScriptExecutor:(FIRDLJavaScriptExecutor *)executor
  82. completedExecutionWithResult:(NSString *)result {
  83. _localeFromWebView = result ?: @"";
  84. _jsExecutor = nil;
  85. [self retrievePendingDynamicLinkInternal];
  86. }
  87. - (void)javaScriptExecutor:(FIRDLJavaScriptExecutor *)executor failedWithError:(NSError *)error {
  88. _localeFromWebView = @"";
  89. _jsExecutor = nil;
  90. [self retrievePendingDynamicLinkInternal];
  91. }
  92. #pragma mark - Internal methods
  93. - (void)retrievePendingDynamicLinkInternal {
  94. CGRect mainScreenBounds = [UIScreen mainScreen].bounds;
  95. NSInteger resolutionWidth = mainScreenBounds.size.width;
  96. NSInteger resolutionHeight = mainScreenBounds.size.height;
  97. if ([[[UIDevice currentDevice] model] isEqualToString:@"iPad"] &&
  98. UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
  99. // iPhone App running in compatibility mode on iPad
  100. // screen resolution reported by UIDevice/UIScreen will be wrong
  101. resolutionWidth = 0;
  102. resolutionHeight = 0;
  103. }
  104. NSURL *uniqueMatchLinkToCheck = [self uniqueMatchLinkToCheck];
  105. __weak __typeof__(self) weakSelf = self;
  106. FIRPostInstallAttributionCompletionHandler completionHandler =
  107. ^(NSDictionary *_Nullable dynamicLinkParameters, NSString *_Nullable matchMessage,
  108. NSError *_Nullable error) {
  109. __typeof__(self) strongSelf = weakSelf;
  110. if (!strongSelf) {
  111. return;
  112. }
  113. if (strongSelf.completed) {
  114. // we may abort process and return previously found dynamic link before all requests
  115. // completed
  116. return;
  117. }
  118. FIRDynamicLink *dynamicLink;
  119. if (dynamicLinkParameters.count) {
  120. dynamicLink = [[FIRDynamicLink alloc] initWithParametersDictionary:dynamicLinkParameters];
  121. }
  122. FIRDLRetrievalProcessResult *result =
  123. [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:dynamicLink
  124. error:error
  125. message:matchMessage
  126. matchSource:nil];
  127. [strongSelf.requestResults addObject:result];
  128. [strongSelf handleRequestResultsUpdated];
  129. if (!error) {
  130. [strongSelf clearUsedUniqueMatchLinkToCheckFromClipboard];
  131. }
  132. };
  133. // Disable deprecated warning for internal methods.
  134. #pragma clang diagnostic push
  135. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  136. // If not unique match, we send request twice, since there are two server calls:
  137. // one for IPv4, another for IPV6.
  138. [_networkingService
  139. retrievePendingDynamicLinkWithIOSVersion:[UIDevice currentDevice].systemVersion
  140. resolutionHeight:resolutionHeight
  141. resolutionWidth:resolutionWidth
  142. locale:FIRDLDeviceLocale()
  143. localeRaw:FIRDLDeviceLocaleRaw()
  144. localeFromWebView:_localeFromWebView
  145. timezone:FIRDLDeviceTimezone()
  146. modelName:FIRDLDeviceModelName()
  147. FDLSDKVersion:_FDLSDKVersion
  148. appInstallationDate:FIRDLAppInstallationDate()
  149. uniqueMatchVisualStyle:FIRDynamicLinkNetworkingUniqueMatchVisualStyleUnknown
  150. retrievalProcessType:
  151. FIRDynamicLinkNetworkingRetrievalProcessTypeImplicitDefault
  152. uniqueMatchLinkToCheck:uniqueMatchLinkToCheck
  153. handler:completionHandler];
  154. #pragma clang pop
  155. }
  156. - (NSArray<FIRDLRetrievalProcessResult *> *)foundResultsWithDynamicLinks {
  157. NSPredicate *predicate =
  158. [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
  159. NSDictionary<NSString *, id> *_Nullable bindings) {
  160. if ([evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]]) {
  161. FIRDLRetrievalProcessResult *result = (FIRDLRetrievalProcessResult *)evaluatedObject;
  162. return result.dynamicLink.url != nil;
  163. }
  164. return NO;
  165. }];
  166. return [self.requestResults filteredArrayUsingPredicate:predicate];
  167. }
  168. - (NSArray<FIRDLRetrievalProcessResult *> *)resultsWithErrors {
  169. NSPredicate *predicate =
  170. [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
  171. NSDictionary<NSString *, id> *_Nullable bindings) {
  172. if ([evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]]) {
  173. FIRDLRetrievalProcessResult *result = (FIRDLRetrievalProcessResult *)evaluatedObject;
  174. return result.error != nil;
  175. }
  176. return NO;
  177. }];
  178. return [self.requestResults filteredArrayUsingPredicate:predicate];
  179. }
  180. - (NSArray<FIRDLRetrievalProcessResult *> *)results {
  181. NSPredicate *predicate =
  182. [NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
  183. NSDictionary<NSString *, id> *_Nullable bindings) {
  184. return [evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]];
  185. }];
  186. return [self.requestResults filteredArrayUsingPredicate:predicate];
  187. }
  188. - (nullable FIRDLRetrievalProcessResult *)resultWithUniqueMatchedDynamicLink {
  189. // return result with unique-matched dynamic link if found
  190. NSArray<FIRDLRetrievalProcessResult *> *foundResultsWithDynamicLinks =
  191. [self foundResultsWithDynamicLinks];
  192. for (FIRDLRetrievalProcessResult *result in foundResultsWithDynamicLinks) {
  193. if (result.dynamicLink.matchType == FIRDLMatchTypeUnique) {
  194. return result;
  195. }
  196. }
  197. return nil;
  198. }
  199. - (void)handleRequestResultsUpdated {
  200. FIRDLRetrievalProcessResult *resultWithUniqueMatchedDynamicLink =
  201. [self resultWithUniqueMatchedDynamicLink];
  202. if (resultWithUniqueMatchedDynamicLink) {
  203. [self markCompleted];
  204. [self.delegate retrievalProcess:self completedWithResult:resultWithUniqueMatchedDynamicLink];
  205. } else if (self.completed) {
  206. NSArray<FIRDLRetrievalProcessResult *> *foundResultsWithDynamicLinks =
  207. [self foundResultsWithDynamicLinks];
  208. NSArray<FIRDLRetrievalProcessResult *> *resultsThatEncounteredErrors = [self resultsWithErrors];
  209. if (foundResultsWithDynamicLinks.count) {
  210. // return any result if no unique-matched URL is available
  211. // TODO: Merge match message from all results
  212. [self.delegate retrievalProcess:self
  213. completedWithResult:foundResultsWithDynamicLinks.firstObject];
  214. } else if (resultsThatEncounteredErrors.count > 0) {
  215. // TODO: Merge match message and errors from all results
  216. [self.delegate retrievalProcess:self
  217. completedWithResult:resultsThatEncounteredErrors.firstObject];
  218. } else {
  219. // dynamic link not found
  220. // TODO: Merge match message from all results
  221. FIRDLRetrievalProcessResult *result = [[self results] firstObject];
  222. if (!result) {
  223. // if we did not get any results, construct one
  224. NSString *message = NSLocalizedString(@"Pending dynamic link not found",
  225. @"Message when dynamic link was not found");
  226. result = [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:nil
  227. error:nil
  228. message:message
  229. matchSource:nil];
  230. }
  231. [self.delegate retrievalProcess:self completedWithResult:result];
  232. }
  233. }
  234. }
  235. - (void)markCompleted {
  236. while (!self.completed) {
  237. [self.requestResults addObject:[NSNull null]];
  238. }
  239. }
  240. - (nullable NSURL *)uniqueMatchLinkToCheck {
  241. _clipboardContentAtMatchProcessStart = nil;
  242. NSString *pasteboardContents = [UIPasteboard generalPasteboard].string;
  243. NSInteger linkStringMinimumLength =
  244. expectedCopiedLinkStringSuffix.length + /* ? or & */ 1 + /* http:// */ 7;
  245. if ((pasteboardContents.length >= linkStringMinimumLength) &&
  246. [pasteboardContents hasSuffix:expectedCopiedLinkStringSuffix] &&
  247. [NSURL URLWithString:pasteboardContents]) {
  248. // remove custom suffix and preceding '&' or '?' character from string
  249. NSString *linkStringWithoutSuffix = [pasteboardContents
  250. substringToIndex:pasteboardContents.length - expectedCopiedLinkStringSuffix.length - 1];
  251. NSURL *URL = [NSURL URLWithString:linkStringWithoutSuffix];
  252. if (URL) {
  253. // check is link matches short link format
  254. if (FIRDLMatchesShortLinkFormat(URL)) {
  255. _clipboardContentAtMatchProcessStart = pasteboardContents;
  256. return URL;
  257. }
  258. // check is link matches long link format
  259. if (FIRDLCanParseUniversalLinkURL(URL)) {
  260. _clipboardContentAtMatchProcessStart = pasteboardContents;
  261. return URL;
  262. }
  263. }
  264. }
  265. return nil;
  266. }
  267. - (void)clearUsedUniqueMatchLinkToCheckFromClipboard {
  268. // See discussion in b/65304652
  269. // We will clear clipboard after we used the unique match link from the clipboard
  270. if (_clipboardContentAtMatchProcessStart.length > 0 &&
  271. [_clipboardContentAtMatchProcessStart isEqualToString:_clipboardContentAtMatchProcessStart]) {
  272. [UIPasteboard generalPasteboard].string = @"";
  273. }
  274. }
  275. - (void)fetchLocaleFromWebView {
  276. if (_jsExecutor) {
  277. return;
  278. }
  279. NSString *jsString = @"window.generateFingerprint=function(){try{var "
  280. @"languageCode=navigator.languages?navigator.languages[0]:navigator."
  281. @"language;return languageCode;}catch(b){return"
  282. "}};";
  283. _jsExecutor = [[FIRDLJavaScriptExecutor alloc] initWithDelegate:self script:jsString];
  284. }
  285. @end
  286. NS_ASSUME_NONNULL_END