1// Copyright 2015-present 650 Industries. All rights reserved. 2 3#import "EXKernel.h" 4#import "EXKernelAppLoader.h" 5#import "EXKernelLinkingManager.h" 6#import "EXReactAppManager.h" 7#import "EXShellManager.h" 8#import "EXVersions.h" 9 10#import <CocoaLumberjack/CocoaLumberjack.h> 11#import <React/RCTBridge+Private.h> 12 13@interface EXKernelLinkingManager () 14 15@property (nonatomic, weak) EXReactAppManager *appManagerToRefresh; 16 17@end 18 19@implementation EXKernelLinkingManager 20 21- (void)openUrl:(NSString *)urlString isUniversalLink:(BOOL)isUniversalLink 22{ 23 NSURL *url = [NSURL URLWithString:urlString]; 24 if (!url) { 25 DDLogInfo(@"Tried to route invalid url: %@", urlString); 26 return; 27 } 28 EXKernelAppRegistry *appRegistry = [EXKernel sharedInstance].appRegistry; 29 EXKernelAppRecord *destinationApp = nil; 30 NSURL *urlToRoute = nil; 31 32 if (isUniversalLink && [EXShellManager sharedInstance].isShell) { 33 destinationApp = [EXKernel sharedInstance].appRegistry.standaloneAppRecord; 34 } else { 35 urlToRoute = [[self class] uriTransformedForLinking:url isUniversalLink:isUniversalLink]; 36 37 for (NSString *recordId in [appRegistry appEnumerator]) { 38 EXKernelAppRecord *appRecord = [appRegistry recordForId:recordId]; 39 if (!appRecord || appRecord.status != kEXKernelAppRecordStatusRunning) { 40 continue; 41 } 42 if (appRecord.appLoader.manifestUrl && [urlToRoute.absoluteString hasPrefix:[[self class] linkingUriForExperienceUri:appRecord.appLoader.manifestUrl]]) { 43 // this is a link into a bridge we already have running. 44 // use this bridge as the link's destination instead of the kernel. 45 destinationApp = appRecord; 46 break; 47 } 48 } 49 } 50 51 if (destinationApp) { 52 [[EXKernel sharedInstance] sendUrl:urlToRoute.absoluteString toAppRecord:destinationApp]; 53 } else { 54 if (![EXShellManager sharedInstance].isShell 55 && [EXKernel sharedInstance].appRegistry.homeAppRecord 56 && [EXKernel sharedInstance].appRegistry.homeAppRecord.appManager.status == kEXReactAppManagerStatusRunning) { 57 // if Home is present and running, open a new app with this url. 58 // if home isn't running yet, we'll handle the LaunchOptions url after home finishes launching. 59 [[EXKernel sharedInstance] createNewAppWithUrl:urlToRoute initialProps:nil]; 60 } 61 } 62} 63 64#pragma mark - scoped module delegate 65 66- (void)linkingModule:(__unused id)linkingModule didOpenUrl:(NSString *)url 67{ 68 [self openUrl:url isUniversalLink:NO]; 69} 70 71- (BOOL)linkingModule:(__unused id)linkingModule shouldOpenExpoUrl:(NSURL *)url 72{ 73 // do not attempt to route internal exponent links at all if we're in a detached exponent app. 74 NSDictionary *versionsConfig = [EXVersions sharedInstance].versions; 75 if (versionsConfig && versionsConfig[@"detachedNativeVersions"]) { 76 return NO; 77 } 78 79 // we don't need to explicitly include a shell app custom URL scheme here 80 // because the default iOS linking behavior will still hand those links back to Exponent. 81 NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; 82 if (components) { 83 return ([components.scheme isEqualToString:@"exp"] || 84 [components.scheme isEqualToString:@"exps"] || 85 [components.host isEqualToString:@"exp.host"] || 86 [components.host hasSuffix:@".exp.host"] 87 ); 88 } 89 return NO; 90} 91 92- (void)utilModuleDidSelectReload:(id)scopedUtilModule 93{ 94 NSString *experienceId = ((EXScopedBridgeModule *)scopedUtilModule).experienceId; 95 [[EXKernel sharedInstance] reloadAppWithExperienceId:experienceId]; 96} 97 98#pragma mark - internal 99 100#pragma mark - static link transforming logic 101 102+ (NSString *)linkingUriForExperienceUri:(NSURL *)uri 103{ 104 uri = [self uriTransformedForLinking:uri isUniversalLink:NO]; 105 if (!uri) { 106 return nil; 107 } 108 NSURLComponents *components = [NSURLComponents componentsWithURL:uri resolvingAgainstBaseURL:YES]; 109 110 // if the provided uri is the shell app manifest uri, 111 // this should have been transformed into customscheme://+deep-link 112 // and then all we do here is strip off the deep-link part, leaving +. 113 if ([EXShellManager sharedInstance].isShell && [[EXShellManager sharedInstance] isShellUrlScheme:components.scheme]) { 114 return [NSString stringWithFormat:@"%@://+", components.scheme]; 115 } 116 117 NSMutableString* path = [NSMutableString stringWithString:components.path]; 118 119 // if the uri already contains a deep link, strip everything specific to that 120 path = [[self stringByRemovingDeepLink:path] mutableCopy]; 121 122 if (path.length == 0 || [path characterAtIndex:path.length - 1] != '/') { 123 [path appendString:@"/"]; 124 } 125 [path appendString:@"+"]; 126 components.path = path; 127 128 components.query = nil; 129 130 return [components string]; 131} 132 133+ (NSString *)stringByRemovingDeepLink:(NSString *)path 134{ 135 NSRange deepLinkRange = [path rangeOfString:@"+"]; 136 if (deepLinkRange.length > 0) { 137 path = [path substringToIndex:deepLinkRange.location]; 138 } 139 return path; 140} 141 142+ (NSURL *)uriTransformedForLinking:(NSURL *)uri isUniversalLink:(BOOL)isUniversalLink 143{ 144 if (!uri) { 145 return nil; 146 } 147 148 // If the initial uri is a universal link in a shell app don't touch it. 149 if ([EXShellManager sharedInstance].isShell && isUniversalLink) { 150 return uri; 151 } 152 153 NSURL *normalizedUri = [self _uriNormalizedForLinking:uri]; 154 155 if ([EXShellManager sharedInstance].isShell && [EXShellManager sharedInstance].hasUrlScheme) { 156 // if the provided uri is the shell app manifest uri, 157 // transform this into customscheme://+deep-link 158 if ([self _isShellManifestUrl:normalizedUri]) { 159 NSString *uriString = normalizedUri.absoluteString; 160 NSRange deepLinkRange = [uriString rangeOfString:@"+"]; 161 NSString *deepLink = @""; 162 if (deepLinkRange.length > 0) { 163 deepLink = [uriString substringFromIndex:deepLinkRange.location]; 164 } 165 NSString *result = [NSString stringWithFormat:@"%@://%@", [EXShellManager sharedInstance].urlScheme, deepLink]; 166 return [NSURL URLWithString:result]; 167 } 168 } 169 return normalizedUri; 170} 171 172+ (NSURL *)_uriNormalizedForLinking: (NSURL *)uri 173{ 174 NSURLComponents *components = [NSURLComponents componentsWithURL:uri resolvingAgainstBaseURL:YES]; 175 176 if ([EXShellManager sharedInstance].isShell && [[EXShellManager sharedInstance] isShellUrlScheme:components.scheme]) { 177 // if we're a shell and this uri had the shell scheme, leave it alone. 178 } else { 179 if ([components.scheme isEqualToString:@"https"]) { 180 components.scheme = @"exps"; 181 } else { 182 components.scheme = @"exp"; 183 } 184 } 185 186 if ([components.scheme isEqualToString:@"exp"] && [components.port integerValue] == 80) { 187 components.port = nil; 188 } else if ([components.scheme isEqualToString:@"exps"] && [components.port integerValue] == 443) { 189 components.port = nil; 190 } 191 192 return [components URL]; 193} 194 195+ (BOOL)_isShellManifestUrl: (NSURL *)normalizedUri 196{ 197 NSString *uriString = normalizedUri.absoluteString; 198 for (NSString *shellManifestUrl in [EXShellManager sharedInstance].allManifestUrls) { 199 NSURL *normalizedShellManifestURL = [self _uriNormalizedForLinking:[NSURL URLWithString:shellManifestUrl]]; 200 if ([normalizedShellManifestURL.absoluteString isEqualToString:uriString]) { 201 return YES; 202 } 203 } 204 return NO; 205} 206 207#pragma mark - UIApplication hooks 208 209+ (BOOL)application:(UIApplication *)application 210 openURL:(NSURL *)URL 211 sourceApplication:(NSString *)sourceApplication 212 annotation:(id)annotation 213{ 214 [[EXKernel sharedInstance].serviceRegistry.linkingManager openUrl:URL.absoluteString isUniversalLink:NO]; 215 return YES; 216} 217 218+ (BOOL)application:(UIApplication *)application 219continueUserActivity:(NSUserActivity *)userActivity 220 restorationHandler:(void (^)(NSArray *))restorationHandler 221{ 222 if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { 223 [[EXKernel sharedInstance].serviceRegistry.linkingManager openUrl:userActivity.webpageURL.absoluteString isUniversalLink:YES]; 224 225 } 226 return YES; 227} 228 229+ (NSURL *)initialUrlFromLaunchOptions:(NSDictionary *)launchOptions 230{ 231 NSURL *initialUrl; 232 233 if (launchOptions) { 234 if (launchOptions[UIApplicationLaunchOptionsURLKey]) { 235 initialUrl = launchOptions[UIApplicationLaunchOptionsURLKey]; 236 } else if (launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]) { 237 NSDictionary *userActivityDictionary = launchOptions[UIApplicationLaunchOptionsUserActivityDictionaryKey]; 238 239 if ([userActivityDictionary[UIApplicationLaunchOptionsUserActivityTypeKey] isEqual:NSUserActivityTypeBrowsingWeb]) { 240 initialUrl = ((NSUserActivity *)userActivityDictionary[@"UIApplicationLaunchOptionsUserActivityKey"]).webpageURL; 241 } 242 } 243 } 244 245 return initialUrl; 246} 247 248@end 249