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