Bolts/iOS/BFAppLinkNavigation.m (229 lines of code) (raw):

/* * Copyright (c) 2014, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * */ #import "BFAppLinkNavigation.h" #import <Bolts/Bolts.h> #import "BFMeasurementEvent_Internal.h" #import "BFAppLink_Internal.h" FOUNDATION_EXPORT NSString *const BFAppLinkDataParameterName; FOUNDATION_EXPORT NSString *const BFAppLinkTargetKeyName; FOUNDATION_EXPORT NSString *const BFAppLinkUserAgentKeyName; FOUNDATION_EXPORT NSString *const BFAppLinkExtrasKeyName; FOUNDATION_EXPORT NSString *const BFAppLinkVersionKeyName; FOUNDATION_EXPORT NSString *const BFAppLinkRefererAppLink; FOUNDATION_EXPORT NSString *const BFAppLinkRefererAppName; FOUNDATION_EXPORT NSString *const BFAppLinkRefererUrl; static id<BFAppLinkResolving> defaultResolver; @interface BFAppLinkNavigation () @property (nonatomic, copy, readwrite) NSDictionary *extras; @property (nonatomic, copy, readwrite) NSDictionary *appLinkData; @property (nonatomic, strong, readwrite) BFAppLink *appLink; @end @implementation BFAppLinkNavigation + (instancetype)navigationWithAppLink:(BFAppLink *)appLink extras:(NSDictionary *)extras appLinkData:(NSDictionary *)appLinkData { BFAppLinkNavigation *navigation = [[self alloc] init]; navigation.appLink = appLink; navigation.extras = extras; navigation.appLinkData = appLinkData; return navigation; } + (NSDictionary *)callbackAppLinkDataForAppWithName:(NSString *)appName url:(NSString *)url { return @{BFAppLinkRefererAppLink: @{BFAppLinkRefererAppName: appName, BFAppLinkRefererUrl: url}}; } - (NSString *)stringByEscapingQueryString:(NSString *)string { #if __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_7_0 || __MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_9 return [string stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; #else return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)string, NULL, (CFStringRef) @":/?#[]@!$&'()*+,;=", kCFStringEncodingUTF8)); #endif } - (NSURL *)appLinkURLWithTargetURL:(NSURL *)targetUrl error:(NSError **)error { NSMutableDictionary *appLinkData = [NSMutableDictionary dictionaryWithDictionary:self.appLinkData ?: @{}]; // Add applink protocol data if (!appLinkData[BFAppLinkUserAgentKeyName]) { appLinkData[BFAppLinkUserAgentKeyName] = [NSString stringWithFormat:@"Bolts iOS %@", BoltsFrameworkVersionString]; } if (!appLinkData[BFAppLinkVersionKeyName]) { appLinkData[BFAppLinkVersionKeyName] = BFAppLinkVersion; } appLinkData[BFAppLinkTargetKeyName] = [self.appLink.sourceURL absoluteString]; appLinkData[BFAppLinkExtrasKeyName] = self.extras ?: @{}; // JSON-ify the applink data NSError *jsonError = nil; NSData *jsonBlob = [NSJSONSerialization dataWithJSONObject:appLinkData options:0 error:&jsonError]; if (!jsonError) { NSString *jsonString = [[NSString alloc] initWithData:jsonBlob encoding:NSUTF8StringEncoding]; NSString *encoded = [self stringByEscapingQueryString:jsonString]; NSString *endUrlString = [NSString stringWithFormat:@"%@%@%@=%@", [targetUrl absoluteString], targetUrl.query ? @"&" : @"?", BFAppLinkDataParameterName, encoded]; return [NSURL URLWithString:endUrlString]; } else { if (error) { *error = jsonError; } // If there was an error encoding the app link data, fail hard. return nil; } } - (BFAppLinkNavigationType)navigate:(NSError **)error { NSURL *openedURL = nil; NSError *encodingError = nil; BFAppLinkNavigationType retType = BFAppLinkNavigationTypeFailure; // Find the first eligible/launchable target in the BFAppLink. for (BFAppLinkTarget *target in self.appLink.targets) { NSURL *appLinkAppURL = [self appLinkURLWithTargetURL:target.URL error:&encodingError]; if (encodingError || !appLinkAppURL) { if (error) { *error = encodingError; } } else if ([[UIApplication sharedApplication] openURL:appLinkAppURL]) { retType = BFAppLinkNavigationTypeApp; openedURL = appLinkAppURL; break; } } if (!openedURL && self.appLink.webURL) { // Fall back to opening the url in the browser if available. NSURL *appLinkBrowserURL = [self appLinkURLWithTargetURL:self.appLink.webURL error:&encodingError]; if (encodingError || !appLinkBrowserURL) { // If there was an error encoding the app link data, fail hard. if (error) { *error = encodingError; } } else if ([[UIApplication sharedApplication] openURL:appLinkBrowserURL]) { // This was a browser navigation. retType = BFAppLinkNavigationTypeBrowser; openedURL = appLinkBrowserURL; } } [self postAppLinkNavigateEventNotificationWithTargetURL:openedURL error:error ? *error : nil type:retType]; return retType; } - (void)postAppLinkNavigateEventNotificationWithTargetURL:(NSURL *)outputURL error:(NSError *)error type:(BFAppLinkNavigationType)type { NSString *const EVENT_YES_VAL = @"1"; NSString *const EVENT_NO_VAL = @"0"; NSMutableDictionary *logData = [[NSMutableDictionary alloc] init]; NSString *outputURLScheme = [outputURL scheme]; NSString *outputURLString = [outputURL absoluteString]; if (outputURLScheme) { logData[@"outputURLScheme"] = outputURLScheme; } if (outputURLString) { logData[@"outputURL"] = outputURLString; } NSString *sourceURLString = [self.appLink.sourceURL absoluteString]; NSString *sourceURLHost = [self.appLink.sourceURL host]; NSString *sourceURLScheme = [self.appLink.sourceURL scheme]; if (sourceURLString) { logData[@"sourceURL"] = sourceURLString; } if (sourceURLHost) { logData[@"sourceHost"] = sourceURLHost; } if (sourceURLScheme) { logData[@"sourceScheme"] = sourceURLScheme; } if ([error localizedDescription]) { logData[@"error"] = [error localizedDescription]; } NSString *success = nil; //no NSString *linkType = nil; // unknown; switch (type) { case BFAppLinkNavigationTypeFailure: success = EVENT_NO_VAL; linkType = @"fail"; break; case BFAppLinkNavigationTypeBrowser: success = EVENT_YES_VAL; linkType = @"web"; break; case BFAppLinkNavigationTypeApp: success = EVENT_YES_VAL; linkType = @"app"; break; default: break; } if (success) { logData[@"success"] = success; } if (linkType) { logData[@"type"] = linkType; } if ([self.appLink isBackToReferrer]) { [BFMeasurementEvent postNotificationForEventName:BFAppLinkNavigateBackToReferrerEventName args:logData]; } else { [BFMeasurementEvent postNotificationForEventName:BFAppLinkNavigateOutEventName args:logData]; } } + (BFTask *)resolveAppLinkInBackground:(NSURL *)destination resolver:(id<BFAppLinkResolving>)resolver { return [resolver appLinkFromURLInBackground:destination]; } + (BFTask *)resolveAppLinkInBackground:(NSURL *)destination { return [self resolveAppLinkInBackground:destination resolver:[self defaultResolver]]; } + (BFTask *)navigateToURLInBackground:(NSURL *)destination { return [self navigateToURLInBackground:destination resolver:[self defaultResolver]]; } + (BFTask *)navigateToURLInBackground:(NSURL *)destination resolver:(id<BFAppLinkResolving>)resolver { BFTask *resolutionTask = [self resolveAppLinkInBackground:destination resolver:resolver]; return [resolutionTask continueWithExecutor:[BFExecutor mainThreadExecutor] withSuccessBlock:^id(BFTask *task) { NSError *error = nil; BFAppLinkNavigationType result = [self navigateToAppLink:task.result error:&error]; if (error) { return [BFTask taskWithError:error]; } else { return @(result); } }]; } + (BFAppLinkNavigationType)navigateToAppLink:(BFAppLink *)link error:(NSError **)error { return [[BFAppLinkNavigation navigationWithAppLink:link extras:nil appLinkData:nil] navigate:error]; } + (BFAppLinkNavigationType)navigationTypeForLink:(BFAppLink *)link { return [[self navigationWithAppLink:link extras:nil appLinkData:nil] navigationType]; } - (BFAppLinkNavigationType)navigationType { BFAppLinkTarget *eligibleTarget = nil; for (BFAppLinkTarget *target in self.appLink.targets) { if ([[UIApplication sharedApplication] canOpenURL:target.URL]) { eligibleTarget = target; break; } } if (eligibleTarget != nil) { NSURL *appLinkURL = [self appLinkURLWithTargetURL:eligibleTarget.URL error:nil]; if (appLinkURL != nil) { return BFAppLinkNavigationTypeApp; } else { return BFAppLinkNavigationTypeFailure; } } if (self.appLink.webURL != nil) { NSURL *appLinkURL = [self appLinkURLWithTargetURL:eligibleTarget.URL error:nil]; if (appLinkURL != nil) { return BFAppLinkNavigationTypeBrowser; } else { return BFAppLinkNavigationTypeFailure; } } return BFAppLinkNavigationTypeFailure; } + (id<BFAppLinkResolving>)defaultResolver { if (defaultResolver) { return defaultResolver; } return [BFWebViewAppLinkResolver sharedInstance]; } + (void)setDefaultResolver:(id<BFAppLinkResolving>)resolver { defaultResolver = resolver; } @end