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