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  _updatesCheckAutomatically = YES;
55  _updatesFallbackToCacheTimeout = @(0);
56  _allManifestUrls = @[];
57  _isDebugXCodeScheme = NO;
58  _releaseChannel = @"default";
59}
60
61- (void)_loadDefaultConfig
62{
63  // use bundled EXShell.plist
64  NSString *configPath = [[NSBundle mainBundle] pathForResource:@"EXShell" ofType:@"plist"];
65  NSDictionary *shellConfig = (configPath) ? [NSDictionary dictionaryWithContentsOfFile:configPath] : [NSDictionary dictionary];
66
67  // use ExpoKit dev url from EXBuildConstants
68  NSString *expoKitDevelopmentUrl = [EXBuildConstants sharedInstance].expoKitDevelopmentUrl;
69
70  // use bundled info.plist
71  NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary];
72
73  // use bundled shell app manifest
74  NSDictionary *embeddedManifest = @{};
75  NSString *path = [[NSBundle mainBundle] pathForResource:kEXEmbeddedManifestResourceName ofType:@"json"];
76  NSData *data = [NSData dataWithContentsOfFile:path];
77  if (data) {
78    NSError *jsonError;
79    id manifest = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
80    if (!jsonError && [manifest isKindOfClass:[NSDictionary class]]) {
81      embeddedManifest = (NSDictionary *)manifest;
82    }
83  }
84
85  BOOL isDetached = NO;
86#ifdef EX_DETACHED
87  isDetached = YES;
88#endif
89
90  BOOL isDebugXCodeScheme = NO;
91#if DEBUG
92  isDebugXCodeScheme = YES;
93#endif
94
95  BOOL isUserDetach = NO;
96  if (isDetached) {
97#ifndef EX_DETACHED_SERVICE
98  isUserDetach = YES;
99#endif
100  }
101
102  [self _loadShellConfig:shellConfig
103           withInfoPlist:infoPlist
104       withExpoKitDevUrl:expoKitDevelopmentUrl
105    withEmbeddedManifest:embeddedManifest
106              isDetached:isDetached
107      isDebugXCodeScheme:isDebugXCodeScheme
108            isUserDetach:isUserDetach];
109}
110
111- (void)_loadShellConfig:(NSDictionary *)shellConfig
112           withInfoPlist:(NSDictionary *)infoPlist
113       withExpoKitDevUrl:(NSString *)expoKitDevelopmentUrl
114    withEmbeddedManifest:(NSDictionary *)embeddedManifest
115              isDetached:(BOOL)isDetached
116      isDebugXCodeScheme:(BOOL)isDebugScheme
117            isUserDetach:(BOOL)isUserDetach
118{
119  [self _reset];
120  NSMutableArray *allManifestUrls = [NSMutableArray array];
121  _isDetached = isDetached;
122  _isDebugXCodeScheme = isDebugScheme;
123
124  if (shellConfig) {
125    _testEnvironment = [EXTest testEnvironmentFromString:shellConfig[@"testEnvironment"]];
126
127    if (_isDetached) {
128      // configure published shell url
129      [self _loadProductionUrlFromConfig:shellConfig];
130      if (_standaloneManifestUrl) {
131        [allManifestUrls addObject:_standaloneManifestUrl];
132      }
133      if (isDetached && isDebugScheme) {
134        // local detach development: point shell manifest url at local development url
135        [self _loadDetachedDevelopmentUrl:expoKitDevelopmentUrl];
136        if (_standaloneManifestUrl) {
137          [allManifestUrls addObject:_standaloneManifestUrl];
138        }
139      }
140      // load standalone url scheme
141      [self _loadUrlSchemeFromInfoPlist:infoPlist];
142      if (!_standaloneManifestUrl) {
143        @throw [NSException exceptionWithName:NSInternalInconsistencyException
144                                       reason:@"This app is configured to be a standalone app, but does not specify a standalone manifest url."
145                                     userInfo:nil];
146      }
147
148      // load bundleUrl from embedded manifest
149      [self _loadEmbeddedBundleUrlWithManifest:embeddedManifest];
150
151      // load everything else from EXShell
152      [self _loadMiscPropertiesWithConfig:shellConfig andInfoPlist:infoPlist];
153
154      [self _setAnalyticsPropertiesWithStandaloneManifestUrl:_standaloneManifestUrl isUserDetached:isUserDetach];
155    }
156  }
157  _allManifestUrls = allManifestUrls;
158}
159
160- (void)_loadProductionUrlFromConfig:(NSDictionary *)shellConfig
161{
162  _standaloneManifestUrl = shellConfig[@"manifestUrl"];
163  if ([ExpoKit sharedInstance].publishedManifestUrlOverride) {
164    _standaloneManifestUrl = [ExpoKit sharedInstance].publishedManifestUrlOverride;
165  }
166}
167
168- (void)_loadDetachedDevelopmentUrl:(NSString *)expoKitDevelopmentUrl
169{
170  if (expoKitDevelopmentUrl) {
171    _standaloneManifestUrl = expoKitDevelopmentUrl;
172  } else {
173    @throw [NSException exceptionWithName:NSInternalInconsistencyException
174                                   reason:@"You are running a detached app from Xcode, but it hasn't been configured for local development yet. "
175                                           "You must run a packager for this Expo project before running it from XCode."
176                                 userInfo:nil];
177  }
178}
179
180- (void)_loadUrlSchemeFromInfoPlist:(NSDictionary *)infoPlist
181{
182  if (infoPlist[@"CFBundleURLTypes"]) {
183    // if the shell app has a custom url scheme, read that.
184    // this was configured when the shell app was built.
185    NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"];
186    if (urlTypes && urlTypes.count) {
187      NSDictionary *urlType = urlTypes[0];
188      NSArray *urlSchemes = urlType[@"CFBundleURLSchemes"];
189      if (urlSchemes) {
190        for (NSString *urlScheme in urlSchemes) {
191          if ([self _isValidStandaloneUrlScheme:urlScheme forDevelopment:NO]) {
192            _urlScheme = urlScheme;
193            break;
194          }
195        }
196      }
197    }
198  }
199}
200
201- (void)_loadMiscPropertiesWithConfig:(NSDictionary *)shellConfig andInfoPlist:(NSDictionary *)infoPlist
202{
203  _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue];
204  _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil)
205    ? YES
206    : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue];
207  _updatesCheckAutomatically = (shellConfig[@"updatesCheckAutomatically"] == nil)
208    ? YES
209    : [shellConfig[@"updatesCheckAutomatically"] boolValue];
210  _updatesFallbackToCacheTimeout = (shellConfig[@"updatesFallbackToCacheTimeout"] &&
211                                    [shellConfig[@"updatesFallbackToCacheTimeout"] isKindOfClass:[NSNumber class]])
212    ? shellConfig[@"updatesFallbackToCacheTimeout"]
213    : @(0);
214  if (infoPlist[@"ExpoReleaseChannel"]) {
215    _releaseChannel = infoPlist[@"ExpoReleaseChannel"];
216  } else {
217    _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"];
218  }
219  // other shell config goes here
220}
221
222- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest
223{
224  id bundleUrl = manifest[@"bundleUrl"];
225  if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) {
226    _embeddedBundleUrl = (NSString *)bundleUrl;
227  }
228}
229
230- (void)_setAnalyticsPropertiesWithStandaloneManifestUrl:(NSString *)shellManifestUrl
231                                     isUserDetached:(BOOL)isUserDetached
232{
233  if (_testEnvironment == EXTestEnvironmentNone) {
234    [[EXAnalytics sharedInstance] setUserProperties:@{ @"INITIAL_URL": shellManifestUrl }];
235    if (isUserDetached) {
236      [[EXAnalytics sharedInstance] setUserProperties:@{ @"IS_DETACHED": @YES }];
237    }
238  }
239}
240
241/**
242 *  Is this a valid url scheme for a standalone app?
243 */
244- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment
245{
246  // don't allow shell apps to intercept exp links
247  if (urlScheme && urlScheme.length) {
248    if (isForDevelopment) {
249      return YES;
250    } else {
251      // prod shell apps must have some non-exp/exps url scheme
252      return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]);
253    }
254  }
255  return NO;
256}
257
258@end
259