Files
firebase-ios-sdk/FirebaseDynamicLinks/Sources/FIRDLDefaultRetrievalProcessV2.m
T
dmandar f5062d861c FDL source, unit tests and app now live in FirebaseDynamicLinks folder. (#4767)
* Move FDL sources to new location.

* Add FDL unit tests.

* Move test app.

* Fix source path.

* Move unit tests path.

* Rename source to sources.

* Change relative path.

* Update podspec.

* Fix header search path.

* Fix relative path for scripts.

* Fix tests.
2020-01-30 11:44:31 -08:00

322 lines
13 KiB
Objective-C

/*
* Copyright 2018 Google
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#import "FirebaseDynamicLinks/Sources/FIRDLDefaultRetrievalProcessV2.h"
#import <UIKit/UIKit.h>
#import "FirebaseDynamicLinks/Sources/FIRDLJavaScriptExecutor.h"
#import "FirebaseDynamicLinks/Sources/FIRDLRetrievalProcessResult+Private.h"
#import "FirebaseDynamicLinks/Sources/FIRDynamicLink+Private.h"
#import "FirebaseDynamicLinks/Sources/FIRDynamicLinkNetworking.h"
#import "FirebaseDynamicLinks/Sources/Utilities/FDLUtilities.h"
// The maximum number of successful fingerprint api calls.
const static NSUInteger kMaximumNumberOfSuccessfulFingerprintAPICalls = 2;
// Reason for this string to ensure that only FDL links, copied to clipboard by AppPreview Page
// JavaScript code, are recognized and used in copy-unique-match process. If user copied FDL to
// clipboard by himself, that link must not be used in copy-unique-match process.
// This constant must be kept in sync with constant in the server version at
// durabledeeplink/click/ios/click_page.js
static NSString *expectedCopiedLinkStringSuffix = @"_icp=1";
NS_ASSUME_NONNULL_BEGIN
@interface FIRDLDefaultRetrievalProcessV2 () <FIRDLJavaScriptExecutorDelegate>
@property(atomic, strong) NSMutableArray *requestResults;
@end
@implementation FIRDLDefaultRetrievalProcessV2 {
FIRDynamicLinkNetworking *_networkingService;
NSString *_clientID;
NSString *_URLScheme;
NSString *_APIKey;
NSString *_FDLSDKVersion;
NSString *_clipboardContentAtMatchProcessStart;
FIRDLJavaScriptExecutor *_jsExecutor;
NSString *_localeFromWebView;
}
@synthesize delegate = _delegate;
#pragma mark - Initialization
- (instancetype)initWithNetworkingService:(FIRDynamicLinkNetworking *)networkingService
clientID:(NSString *)clientID
URLScheme:(NSString *)URLScheme
APIKey:(NSString *)APIKey
FDLSDKVersion:(NSString *)FDLSDKVersion
delegate:(id<FIRDLRetrievalProcessDelegate>)delegate {
NSParameterAssert(networkingService);
NSParameterAssert(clientID);
NSParameterAssert(URLScheme);
NSParameterAssert(APIKey);
if (self = [super init]) {
_networkingService = networkingService;
_clientID = [clientID copy];
_URLScheme = [URLScheme copy];
_APIKey = [APIKey copy];
_FDLSDKVersion = [FDLSDKVersion copy];
self.requestResults =
[[NSMutableArray alloc] initWithCapacity:kMaximumNumberOfSuccessfulFingerprintAPICalls];
_delegate = delegate;
}
return self;
}
#pragma mark - FIRDLRetrievalProcessProtocol
- (void)retrievePendingDynamicLink {
if (_localeFromWebView) {
[self retrievePendingDynamicLinkInternal];
} else {
[self fetchLocaleFromWebView];
}
}
- (BOOL)isCompleted {
return self.requestResults.count >= kMaximumNumberOfSuccessfulFingerprintAPICalls;
}
#pragma mark - FIRDLJavaScriptExecutorDelegate
- (void)javaScriptExecutor:(FIRDLJavaScriptExecutor *)executor
completedExecutionWithResult:(NSString *)result {
_localeFromWebView = result ?: @"";
_jsExecutor = nil;
[self retrievePendingDynamicLinkInternal];
}
- (void)javaScriptExecutor:(FIRDLJavaScriptExecutor *)executor failedWithError:(NSError *)error {
_localeFromWebView = @"";
_jsExecutor = nil;
[self retrievePendingDynamicLinkInternal];
}
#pragma mark - Internal methods
- (void)retrievePendingDynamicLinkInternal {
CGRect mainScreenBounds = [UIScreen mainScreen].bounds;
NSInteger resolutionWidth = mainScreenBounds.size.width;
NSInteger resolutionHeight = mainScreenBounds.size.height;
if ([[[UIDevice currentDevice] model] isEqualToString:@"iPad"] &&
UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone) {
// iPhone App running in compatibility mode on iPad
// screen resolution reported by UIDevice/UIScreen will be wrong
resolutionWidth = 0;
resolutionHeight = 0;
}
NSURL *uniqueMatchLinkToCheck = [self uniqueMatchLinkToCheck];
__weak __typeof__(self) weakSelf = self;
FIRPostInstallAttributionCompletionHandler completionHandler =
^(NSDictionary *_Nullable dynamicLinkParameters, NSString *_Nullable matchMessage,
NSError *_Nullable error) {
__typeof__(self) strongSelf = weakSelf;
if (!strongSelf) {
return;
}
if (strongSelf.completed) {
// we may abort process and return previously found dynamic link before all requests
// completed
return;
}
FIRDynamicLink *dynamicLink;
if (dynamicLinkParameters.count) {
dynamicLink = [[FIRDynamicLink alloc] initWithParametersDictionary:dynamicLinkParameters];
}
FIRDLRetrievalProcessResult *result =
[[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:dynamicLink
error:error
message:matchMessage
matchSource:nil];
[strongSelf.requestResults addObject:result];
[strongSelf handleRequestResultsUpdated];
if (!error) {
[strongSelf clearUsedUniqueMatchLinkToCheckFromClipboard];
}
};
// Disable deprecated warning for internal methods.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
// If not unique match, we send request twice, since there are two server calls:
// one for IPv4, another for IPV6.
[_networkingService
retrievePendingDynamicLinkWithIOSVersion:[UIDevice currentDevice].systemVersion
resolutionHeight:resolutionHeight
resolutionWidth:resolutionWidth
locale:FIRDLDeviceLocale()
localeRaw:FIRDLDeviceLocaleRaw()
localeFromWebView:_localeFromWebView
timezone:FIRDLDeviceTimezone()
modelName:FIRDLDeviceModelName()
FDLSDKVersion:_FDLSDKVersion
appInstallationDate:FIRDLAppInstallationDate()
uniqueMatchVisualStyle:FIRDynamicLinkNetworkingUniqueMatchVisualStyleUnknown
retrievalProcessType:
FIRDynamicLinkNetworkingRetrievalProcessTypeImplicitDefault
uniqueMatchLinkToCheck:uniqueMatchLinkToCheck
handler:completionHandler];
#pragma clang pop
}
- (NSArray<FIRDLRetrievalProcessResult *> *)foundResultsWithDynamicLinks {
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
NSDictionary<NSString *, id> *_Nullable bindings) {
if ([evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]]) {
FIRDLRetrievalProcessResult *result = (FIRDLRetrievalProcessResult *)evaluatedObject;
return result.dynamicLink.url != nil;
}
return NO;
}];
return [self.requestResults filteredArrayUsingPredicate:predicate];
}
- (NSArray<FIRDLRetrievalProcessResult *> *)resultsWithErrors {
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
NSDictionary<NSString *, id> *_Nullable bindings) {
if ([evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]]) {
FIRDLRetrievalProcessResult *result = (FIRDLRetrievalProcessResult *)evaluatedObject;
return result.error != nil;
}
return NO;
}];
return [self.requestResults filteredArrayUsingPredicate:predicate];
}
- (NSArray<FIRDLRetrievalProcessResult *> *)results {
NSPredicate *predicate =
[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject,
NSDictionary<NSString *, id> *_Nullable bindings) {
return [evaluatedObject isKindOfClass:[FIRDLRetrievalProcessResult class]];
}];
return [self.requestResults filteredArrayUsingPredicate:predicate];
}
- (nullable FIRDLRetrievalProcessResult *)resultWithUniqueMatchedDynamicLink {
// return result with unique-matched dynamic link if found
NSArray<FIRDLRetrievalProcessResult *> *foundResultsWithDynamicLinks =
[self foundResultsWithDynamicLinks];
for (FIRDLRetrievalProcessResult *result in foundResultsWithDynamicLinks) {
if (result.dynamicLink.matchType == FIRDLMatchTypeUnique) {
return result;
}
}
return nil;
}
- (void)handleRequestResultsUpdated {
FIRDLRetrievalProcessResult *resultWithUniqueMatchedDynamicLink =
[self resultWithUniqueMatchedDynamicLink];
if (resultWithUniqueMatchedDynamicLink) {
[self markCompleted];
[self.delegate retrievalProcess:self completedWithResult:resultWithUniqueMatchedDynamicLink];
} else if (self.completed) {
NSArray<FIRDLRetrievalProcessResult *> *foundResultsWithDynamicLinks =
[self foundResultsWithDynamicLinks];
NSArray<FIRDLRetrievalProcessResult *> *resultsThatEncounteredErrors = [self resultsWithErrors];
if (foundResultsWithDynamicLinks.count) {
// return any result if no unique-matched URL is available
// TODO: Merge match message from all results
[self.delegate retrievalProcess:self
completedWithResult:foundResultsWithDynamicLinks.firstObject];
} else if (resultsThatEncounteredErrors.count > 0) {
// TODO: Merge match message and errors from all results
[self.delegate retrievalProcess:self
completedWithResult:resultsThatEncounteredErrors.firstObject];
} else {
// dynamic link not found
// TODO: Merge match message from all results
FIRDLRetrievalProcessResult *result = [[self results] firstObject];
if (!result) {
// if we did not get any results, construct one
NSString *message = NSLocalizedString(@"Pending dynamic link not found",
@"Message when dynamic link was not found");
result = [[FIRDLRetrievalProcessResult alloc] initWithDynamicLink:nil
error:nil
message:message
matchSource:nil];
}
[self.delegate retrievalProcess:self completedWithResult:result];
}
}
}
- (void)markCompleted {
while (!self.completed) {
[self.requestResults addObject:[NSNull null]];
}
}
- (nullable NSURL *)uniqueMatchLinkToCheck {
_clipboardContentAtMatchProcessStart = nil;
NSString *pasteboardContents = [UIPasteboard generalPasteboard].string;
NSInteger linkStringMinimumLength =
expectedCopiedLinkStringSuffix.length + /* ? or & */ 1 + /* http:// */ 7;
if ((pasteboardContents.length >= linkStringMinimumLength) &&
[pasteboardContents hasSuffix:expectedCopiedLinkStringSuffix] &&
[NSURL URLWithString:pasteboardContents]) {
// remove custom suffix and preceding '&' or '?' character from string
NSString *linkStringWithoutSuffix = [pasteboardContents
substringToIndex:pasteboardContents.length - expectedCopiedLinkStringSuffix.length - 1];
NSURL *URL = [NSURL URLWithString:linkStringWithoutSuffix];
if (URL) {
// check is link matches short link format
if (FIRDLMatchesShortLinkFormat(URL)) {
_clipboardContentAtMatchProcessStart = pasteboardContents;
return URL;
}
// check is link matches long link format
if (FIRDLCanParseUniversalLinkURL(URL)) {
_clipboardContentAtMatchProcessStart = pasteboardContents;
return URL;
}
}
}
return nil;
}
- (void)clearUsedUniqueMatchLinkToCheckFromClipboard {
// See discussion in b/65304652
// We will clear clipboard after we used the unique match link from the clipboard
if (_clipboardContentAtMatchProcessStart.length > 0 &&
[_clipboardContentAtMatchProcessStart isEqualToString:_clipboardContentAtMatchProcessStart]) {
[UIPasteboard generalPasteboard].string = @"";
}
}
- (void)fetchLocaleFromWebView {
if (_jsExecutor) {
return;
}
NSString *jsString = @"window.generateFingerprint=function(){try{var "
@"languageCode=navigator.languages?navigator.languages[0]:navigator."
@"language;return languageCode;}catch(b){return"
"}};";
_jsExecutor = [[FIRDLJavaScriptExecutor alloc] initWithDelegate:self script:jsString];
}
@end
NS_ASSUME_NONNULL_END