1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXAnalytics.h"
4#import "EXBuildConstants.h"
5#import "EXKernelUtil.h"
6#import "ExpoKit.h"
7#import "EXEnvironment.h"
8
9#import <Crashlytics/Crashlytics.h>
10#import <React/RCTUtils.h>
11
12NSString * const kEXEmbeddedBundleResourceName = @"shell-app";
13NSString * const kEXEmbeddedManifestResourceName = @"shell-app-manifest";
14
15@implementation EXEnvironment
16
17+ (nonnull instancetype)sharedEnvironment
18{
19  static EXEnvironment *theManager;
20  static dispatch_once_t once;
21  dispatch_once(&once, ^{
22    if (!theManager) {
23      theManager = [[EXEnvironment alloc] init];
24    }
25  });
26  return theManager;
27}
28
29- (id)init
30{
31  if (self = [super init]) {
32    [self _loadDefaultConfig];
33  }
34  return self;
35}
36
37- (BOOL)isStandaloneUrlScheme:(NSString *)scheme
38{
39  return (_urlScheme && [scheme isEqualToString:_urlScheme]);
40}
41
42- (BOOL)hasUrlScheme
43{
44  return (_urlScheme != nil);
45}
46
47#pragma mark - internal
48
49- (void)_reset
50{
51  _isDetached = NO;
52  _standaloneManifestUrl = nil;
53  _urlScheme = nil;
54  _areRemoteUpdatesEnabled = YES;
55  _allManifestUrls = @[];
56  _isDebugXCodeScheme = NO;
57  _releaseChannel = @"default";
58}
59
60- (void)_loadDefaultConfig
61{
62  // use bundled EXShell.plist
63  NSString *configPath = [[NSBundle mainBundle] pathForResource:@"EXShell" ofType:@"plist"];
64  NSDictionary *shellConfig = (configPath) ? [NSDictionary dictionaryWithContentsOfFile:configPath] : [NSDictionary dictionary];
65
66  // use ExpoKit dev url from EXBuildConstants
67  NSString *expoKitDevelopmentUrl = [EXBuildConstants sharedInstance].expoKitDevelopmentUrl;
68
69  // use bundled info.plist
70  NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
71
72  // use bundled shell app manifest
73  NSDictionary *embeddedManifest = @{};
74  NSString *path = [[NSBundle mainBundle] pathForResource:kEXEmbeddedManifestResourceName ofType:@"json"];
75  NSData *data = [NSData dataWithContentsOfFile:path];
76  if (data) {
77    NSError *jsonError;
78    id manifest = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
79    if (!jsonError && [manifest isKindOfClass:[NSDictionary class]]) {
80      embeddedManifest = (NSDictionary *)manifest;
81    }
82  }
83
84  BOOL isDetached = NO;
85#ifdef EX_DETACHED
86  isDetached = YES;
87#endif
88
89  BOOL isDebugXCodeScheme = NO;
90#if DEBUG
91  isDebugXCodeScheme = YES;
92#endif
93
94  BOOL isUserDetach = NO;
95  if (isDetached) {
96#ifndef EX_DETACHED_SERVICE
97  isUserDetach = YES;
98#endif
99  }
100
101  [self _loadShellConfig:shellConfig
102           withInfoPlist:infoPlist
103       withExpoKitDevUrl:expoKitDevelopmentUrl
104    withEmbeddedManifest:embeddedManifest
105              isDetached:isDetached
106      isDebugXCodeScheme:isDebugXCodeScheme
107            isUserDetach:isUserDetach];
108}
109
110- (void)_loadShellConfig:(NSDictionary *)shellConfig
111           withInfoPlist:(NSDictionary *)infoPlist
112       withExpoKitDevUrl:(NSString *)expoKitDevelopmentUrl
113    withEmbeddedManifest:(NSDictionary *)embeddedManifest
114              isDetached:(BOOL)isDetached
115      isDebugXCodeScheme:(BOOL)isDebugScheme
116            isUserDetach:(BOOL)isUserDetach
117{
118  [self _reset];
119  NSMutableArray *allManifestUrls = [NSMutableArray array];
120  _isDetached = isDetached;
121
122  if (shellConfig) {
123    _testEnvironment = [EXTest testEnvironmentFromString:shellConfig[@"testEnvironment"]];
124
125    if (_isDetached) {
126      // configure published shell url
127      [self _loadProductionUrlFromConfig:shellConfig];
128      if (_standaloneManifestUrl) {
129        [allManifestUrls addObject:_standaloneManifestUrl];
130      }
131      if (isDetached && isDebugScheme) {
132        // local detach development: point shell manifest url at local development url
133        [self _loadDetachedDevelopmentUrl:expoKitDevelopmentUrl];
134        if (_standaloneManifestUrl) {
135          [allManifestUrls addObject:_standaloneManifestUrl];
136        }
137      }
138      // load standalone url scheme
139      [self _loadUrlSchemeFromInfoPlist:infoPlist];
140      if (!_standaloneManifestUrl) {
141        @throw [NSException exceptionWithName:NSInternalInconsistencyException
142                                       reason:@"This app is configured to be a standalone app, but does not specify a standalone manifest url."
143                                     userInfo:nil];
144      }
145
146      // load bundleUrl from embedded manifest
147      [self _loadEmbeddedBundleUrlWithManifest:embeddedManifest];
148
149      // load everything else from EXShell
150      [self _loadMiscPropertiesWithConfig:shellConfig];
151
152      [self _setAnalyticsPropertiesWithStandaloneManifestUrl:_standaloneManifestUrl isUserDetached:isUserDetach];
153    }
154  }
155  _allManifestUrls = allManifestUrls;
156}
157
158- (void)_loadProductionUrlFromConfig:(NSDictionary *)shellConfig
159{
160  _standaloneManifestUrl = shellConfig[@"manifestUrl"];
161  if ([ExpoKit sharedInstance].publishedManifestUrlOverride) {
162    _standaloneManifestUrl = [ExpoKit sharedInstance].publishedManifestUrlOverride;
163  }
164}
165
166- (void)_loadDetachedDevelopmentUrl:(NSString *)expoKitDevelopmentUrl
167{
168  if (expoKitDevelopmentUrl) {
169    _standaloneManifestUrl = expoKitDevelopmentUrl;
170  } else {
171    @throw [NSException exceptionWithName:NSInternalInconsistencyException
172                                   reason:@"You are running a detached app from Xcode, but it hasn't been configured for local development yet. "
173                                           "You must run a packager for this Expo project before running it from XCode."
174                                 userInfo:nil];
175  }
176}
177
178- (void)_loadUrlSchemeFromInfoPlist:(NSDictionary *)infoPlist
179{
180  if (infoPlist[@"CFBundleURLTypes"]) {
181    // if the shell app has a custom url scheme, read that.
182    // this was configured when the shell app was built.
183    NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"];
184    if (urlTypes && urlTypes.count) {
185      NSDictionary *urlType = urlTypes[0];
186      NSArray *urlSchemes = urlType[@"CFBundleURLSchemes"];
187      if (urlSchemes) {
188        for (NSString *urlScheme in urlSchemes) {
189          if ([self _isValidStandaloneUrlScheme:urlScheme forDevelopment:NO]) {
190            _urlScheme = urlScheme;
191            break;
192          }
193        }
194      }
195    }
196  }
197}
198
199- (void)_loadMiscPropertiesWithConfig:(NSDictionary *)shellConfig
200{
201  _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue];
202  _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil)
203    ? YES
204    : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue];
205  _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"];
206  // other shell config goes here
207}
208
209- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest
210{
211  id bundleUrl = manifest[@"bundleUrl"];
212  if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) {
213    _embeddedBundleUrl = (NSString *)bundleUrl;
214  }
215}
216
217- (void)_setAnalyticsPropertiesWithStandaloneManifestUrl:(NSString *)shellManifestUrl
218                                     isUserDetached:(BOOL)isUserDetached
219{
220  if (_testEnvironment == EXTestEnvironmentNone) {
221    [[EXAnalytics sharedInstance] setUserProperties:@{ @"INITIAL_URL": shellManifestUrl }];
222    [CrashlyticsKit setObjectValue:_standaloneManifestUrl forKey:@"initial_url"];
223    if (isUserDetached) {
224      [[EXAnalytics sharedInstance] setUserProperties:@{ @"IS_DETACHED": @YES }];
225    }
226  }
227}
228
229/**
230 *  Is this a valid url scheme for a standalone app?
231 */
232- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment
233{
234  // don't allow shell apps to intercept exp links
235  if (urlScheme && urlScheme.length) {
236    if (isForDevelopment) {
237      return YES;
238    } else {
239      // prod shell apps must have some non-exp/exps url scheme
240      return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]);
241    }
242  }
243  return NO;
244}
245
246@end
247