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