FIRDLDefaultRetrievalProcessV2.m 14 KB

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