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