FIRDLDefaultRetrievalProcessV2.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353
  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 = [self retrievePasteboardContents];
  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. - (NSString *)retrievePasteboardContents {
  268. if (![self isPasteboardRetrievalEnabled]) {
  269. // Pasteboard check for dynamic link is disabled by user.
  270. return @"";
  271. }
  272. NSString *pasteboardContents = @"";
  273. if (@available(iOS 10.0, *)) {
  274. if ([[UIPasteboard generalPasteboard] hasURLs]) {
  275. pasteboardContents = [UIPasteboard generalPasteboard].string;
  276. }
  277. } else {
  278. pasteboardContents = [UIPasteboard generalPasteboard].string;
  279. }
  280. return pasteboardContents;
  281. }
  282. /**
  283. Property to enable or disable dynamic link retrieval from Pasteboard.
  284. This property is added because of iOS 14 feature where pop up is displayed while accessing
  285. Pasteboard. So if developers don't want their users to see the Pasteboard popup, they can set
  286. "FirebaseDeepLinkPasteboardRetrievalEnabled" to false in their plist.
  287. */
  288. - (BOOL)isPasteboardRetrievalEnabled {
  289. id retrievalEnabledValue =
  290. [[NSBundle mainBundle] infoDictionary][@"FirebaseDeepLinkPasteboardRetrievalEnabled"];
  291. if ([retrievalEnabledValue respondsToSelector:@selector(boolValue)]) {
  292. return [retrievalEnabledValue boolValue];
  293. }
  294. return YES;
  295. }
  296. - (void)clearUsedUniqueMatchLinkToCheckFromClipboard {
  297. // See discussion in b/65304652
  298. // We will clear clipboard after we used the unique match link from the clipboard
  299. if (_clipboardContentAtMatchProcessStart.length > 0 &&
  300. [_clipboardContentAtMatchProcessStart isEqualToString:_clipboardContentAtMatchProcessStart]) {
  301. [UIPasteboard generalPasteboard].string = @"";
  302. }
  303. }
  304. - (void)fetchLocaleFromWebView {
  305. if (_jsExecutor) {
  306. return;
  307. }
  308. NSString *jsString = @"window.generateFingerprint=function(){try{var "
  309. @"languageCode=navigator.languages?navigator.languages[0]:navigator."
  310. @"language;return languageCode;}catch(b){return"
  311. "}};";
  312. _jsExecutor = [[FIRDLJavaScriptExecutor alloc] initWithDelegate:self script:jsString];
  313. }
  314. @end
  315. NS_ASSUME_NONNULL_END