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