1// Copyright 2015-present 650 Industries. All rights reserved.
2
3#import "EXBuildConstants.h"
4#import "EXKernelUtil.h"
5#import "ExpoKit.h"
6#import "EXEnvironment.h"
7
8#import <React/RCTUtils.h>
9
10NSString * const kEXEmbeddedBundleResourceName = @"shell-app";
11NSString * const kEXEmbeddedManifestResourceName = @"shell-app-manifest";
12
13@implementation EXEnvironment
14
15+ (nonnull instancetype)sharedEnvironment
16{
17  static EXEnvironment *theManager;
18  static dispatch_once_t once;
19  dispatch_once(&once, ^{
20    if (!theManager) {
21      theManager = [[EXEnvironment alloc] init];
22    }
23  });
24  return theManager;
25}
26
27- (id)init
28{
29  if (self = [super init]) {
30    [self _loadDefaultConfig];
31  }
32  return self;
33}
34
35- (BOOL)isStandaloneUrlScheme:(NSString *)scheme
36{
37  return (_urlScheme && [scheme isEqualToString:_urlScheme]);
38}
39
40- (BOOL)hasUrlScheme
41{
42  return (_urlScheme != nil);
43}
44
45#pragma mark - internal
46
47- (void)_reset
48{
49  _isDetached = NO;
50  _standaloneManifestUrl = nil;
51  _urlScheme = nil;
52  _areRemoteUpdatesEnabled = YES;
53  _updatesCheckAutomatically = YES;
54  _updatesFallbackToCacheTimeout = @(0);
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  _isDebugXCodeScheme = isDebugScheme;
122
123  if (shellConfig) {
124    _testEnvironment = [EXTest testEnvironmentFromString:shellConfig[@"testEnvironment"]];
125
126    if (_isDetached) {
127      // configure published shell url
128      [self _loadProductionUrlFromConfig:shellConfig];
129      if (_standaloneManifestUrl) {
130        [allManifestUrls addObject:_standaloneManifestUrl];
131      }
132      if (isDetached && isDebugScheme) {
133        // local detach development: point shell manifest url at local development url
134        [self _loadDetachedDevelopmentUrl:expoKitDevelopmentUrl];
135        if (_standaloneManifestUrl) {
136          [allManifestUrls addObject:_standaloneManifestUrl];
137        }
138      }
139      // load standalone url scheme
140      [self _loadUrlSchemeFromInfoPlist:infoPlist];
141      if (!_standaloneManifestUrl) {
142        @throw [NSException exceptionWithName:NSInternalInconsistencyException
143                                       reason:@"This app is configured to be a standalone app, but does not specify a standalone manifest url."
144                                     userInfo:nil];
145      }
146
147      // load bundleUrl from embedded manifest
148      [self _loadEmbeddedBundleUrlWithManifest:embeddedManifest];
149
150      // load everything else from EXShell
151      [self _loadMiscPropertiesWithConfig:shellConfig andInfoPlist:infoPlist];
152    }
153  }
154  _allManifestUrls = allManifestUrls;
155}
156
157- (void)_loadProductionUrlFromConfig:(NSDictionary *)shellConfig
158{
159  _standaloneManifestUrl = shellConfig[@"manifestUrl"];
160}
161
162- (void)_loadDetachedDevelopmentUrl:(NSString *)expoKitDevelopmentUrl
163{
164  if (expoKitDevelopmentUrl) {
165    _standaloneManifestUrl = expoKitDevelopmentUrl;
166  } else {
167    @throw [NSException exceptionWithName:NSInternalInconsistencyException
168                                   reason:@"You are running a detached app from Xcode, but it hasn't been configured for local development yet. "
169                                           "You must run a packager for this Expo project before running it from XCode."
170                                 userInfo:nil];
171  }
172}
173
174- (void)_loadUrlSchemeFromInfoPlist:(NSDictionary *)infoPlist
175{
176  if (infoPlist[@"CFBundleURLTypes"]) {
177    // if the shell app has a custom url scheme, read that.
178    // this was configured when the shell app was built.
179    NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"];
180    if (urlTypes && urlTypes.count) {
181      NSDictionary *urlType = urlTypes[0];
182      NSArray *urlSchemes = urlType[@"CFBundleURLSchemes"];
183      if (urlSchemes) {
184        for (NSString *urlScheme in urlSchemes) {
185          if ([self _isValidStandaloneUrlScheme:urlScheme forDevelopment:NO]) {
186            _urlScheme = urlScheme;
187            break;
188          }
189        }
190      }
191    }
192  }
193}
194
195- (void)_loadMiscPropertiesWithConfig:(NSDictionary *)shellConfig andInfoPlist:(NSDictionary *)infoPlist
196{
197  _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue];
198  _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil)
199    ? YES
200    : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue];
201  _updatesCheckAutomatically = (shellConfig[@"updatesCheckAutomatically"] == nil)
202    ? YES
203    : [shellConfig[@"updatesCheckAutomatically"] boolValue];
204  _updatesFallbackToCacheTimeout = (shellConfig[@"updatesFallbackToCacheTimeout"] &&
205                                    [shellConfig[@"updatesFallbackToCacheTimeout"] isKindOfClass:[NSNumber class]])
206    ? shellConfig[@"updatesFallbackToCacheTimeout"]
207    : @(0);
208  if (infoPlist[@"ExpoReleaseChannel"]) {
209    _releaseChannel = infoPlist[@"ExpoReleaseChannel"];
210  } else {
211    _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"];
212  }
213  // other shell config goes here
214}
215
216- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest
217{
218  id bundleUrl = manifest[@"bundleUrl"];
219  if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) {
220    _embeddedBundleUrl = (NSString *)bundleUrl;
221  }
222}
223
224/**
225 *  Is this a valid url scheme for a standalone app?
226 */
227- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment
228{
229  // don't allow shell apps to intercept exp links
230  if (urlScheme && urlScheme.length) {
231    if (isForDevelopment) {
232      return YES;
233    } else {
234      // prod shell apps must have some non-exp/exps url scheme
235      return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]);
236    }
237  }
238  return NO;
239}
240
241@end
242