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 <React/RCTUtils.h>
10
11NSString * const kEXEmbeddedBundleResourceName = @"shell-app";
12NSString * const kEXEmbeddedManifestResourceName = @"shell-app-manifest";
13
14@implementation EXEnvironment
15
16+ (nonnull instancetype)sharedEnvironment
17{
18  static EXEnvironment *theManager;
19  static dispatch_once_t once;
20  dispatch_once(&once, ^{
21    if (!theManager) {
22      theManager = [[EXEnvironment alloc] init];
23    }
24  });
25  return theManager;
26}
27
28- (id)init
29{
30  if (self = [super init]) {
31    [self _loadDefaultConfig];
32  }
33  return self;
34}
35
36- (BOOL)isStandaloneUrlScheme:(NSString *)scheme
37{
38  return (_urlScheme && [scheme isEqualToString:_urlScheme]);
39}
40
41- (BOOL)hasUrlScheme
42{
43  return (_urlScheme != nil);
44}
45
46#pragma mark - internal
47
48- (void)_reset
49{
50  _isDetached = NO;
51  _standaloneManifestUrl = nil;
52  _urlScheme = nil;
53  _areRemoteUpdatesEnabled = YES;
54  _allManifestUrls = @[];
55  _isDebugXCodeScheme = NO;
56  _releaseChannel = @"default";
57}
58
59- (void)_loadDefaultConfig
60{
61  // use bundled EXShell.plist
62  NSString *configPath = [[NSBundle mainBundle] pathForResource:@"EXShell" ofType:@"plist"];
63  NSDictionary *shellConfig = (configPath) ? [NSDictionary dictionaryWithContentsOfFile:configPath] : [NSDictionary dictionary];
64
65  // use ExpoKit dev url from EXBuildConstants
66  NSString *expoKitDevelopmentUrl = [EXBuildConstants sharedInstance].expoKitDevelopmentUrl;
67
68  // use bundled info.plist
69  NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
70
71  // use bundled shell app manifest
72  NSDictionary *embeddedManifest = @{};
73  NSString *path = [[NSBundle mainBundle] pathForResource:kEXEmbeddedManifestResourceName ofType:@"json"];
74  NSData *data = [NSData dataWithContentsOfFile:path];
75  if (data) {
76    NSError *jsonError;
77    id manifest = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
78    if (!jsonError && [manifest isKindOfClass:[NSDictionary class]]) {
79      embeddedManifest = (NSDictionary *)manifest;
80    }
81  }
82
83  BOOL isDetached = NO;
84#ifdef EX_DETACHED
85  isDetached = YES;
86#endif
87
88  BOOL isDebugXCodeScheme = NO;
89#if DEBUG
90  isDebugXCodeScheme = YES;
91#endif
92
93  BOOL isUserDetach = NO;
94  if (isDetached) {
95#ifndef EX_DETACHED_SERVICE
96  isUserDetach = YES;
97#endif
98  }
99
100  [self _loadShellConfig:shellConfig
101           withInfoPlist:infoPlist
102       withExpoKitDevUrl:expoKitDevelopmentUrl
103    withEmbeddedManifest:embeddedManifest
104              isDetached:isDetached
105      isDebugXCodeScheme:isDebugXCodeScheme
106            isUserDetach:isUserDetach];
107}
108
109- (void)_loadShellConfig:(NSDictionary *)shellConfig
110           withInfoPlist:(NSDictionary *)infoPlist
111       withExpoKitDevUrl:(NSString *)expoKitDevelopmentUrl
112    withEmbeddedManifest:(NSDictionary *)embeddedManifest
113              isDetached:(BOOL)isDetached
114      isDebugXCodeScheme:(BOOL)isDebugScheme
115            isUserDetach:(BOOL)isUserDetach
116{
117  [self _reset];
118  NSMutableArray *allManifestUrls = [NSMutableArray array];
119  _isDetached = isDetached;
120  _isDebugXCodeScheme = isDebugScheme;
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 andInfoPlist:infoPlist];
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 andInfoPlist:(NSDictionary *)infoPlist
200{
201  _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue];
202  _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil)
203    ? YES
204    : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue];
205  if (infoPlist[@"ExpoReleaseChannel"]) {
206    _releaseChannel = infoPlist[@"ExpoReleaseChannel"];
207  } else {
208    _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"];
209  }
210  // other shell config goes here
211}
212
213- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest
214{
215  id bundleUrl = manifest[@"bundleUrl"];
216  if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) {
217    _embeddedBundleUrl = (NSString *)bundleUrl;
218  }
219}
220
221- (void)_setAnalyticsPropertiesWithStandaloneManifestUrl:(NSString *)shellManifestUrl
222                                     isUserDetached:(BOOL)isUserDetached
223{
224  if (_testEnvironment == EXTestEnvironmentNone) {
225    [[EXAnalytics sharedInstance] setUserProperties:@{ @"INITIAL_URL": shellManifestUrl }];
226    if (isUserDetached) {
227      [[EXAnalytics sharedInstance] setUserProperties:@{ @"IS_DETACHED": @YES }];
228    }
229  }
230}
231
232/**
233 *  Is this a valid url scheme for a standalone app?
234 */
235- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment
236{
237  // don't allow shell apps to intercept exp links
238  if (urlScheme && urlScheme.length) {
239    if (isForDevelopment) {
240      return YES;
241    } else {
242      // prod shell apps must have some non-exp/exps url scheme
243      return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]);
244    }
245  }
246  return NO;
247}
248
249@end
250