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  _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      [self _setAnalyticsPropertiesWithStandaloneManifestUrl:_standaloneManifestUrl isUserDetached:isUserDetach];
154    }
155  }
156  _allManifestUrls = allManifestUrls;
157}
158
159- (void)_loadProductionUrlFromConfig:(NSDictionary *)shellConfig
160{
161  _standaloneManifestUrl = shellConfig[@"manifestUrl"];
162  if ([ExpoKit sharedInstance].publishedManifestUrlOverride) {
163    _standaloneManifestUrl = [ExpoKit sharedInstance].publishedManifestUrlOverride;
164  }
165}
166
167- (void)_loadDetachedDevelopmentUrl:(NSString *)expoKitDevelopmentUrl
168{
169  if (expoKitDevelopmentUrl) {
170    _standaloneManifestUrl = expoKitDevelopmentUrl;
171  } else {
172    @throw [NSException exceptionWithName:NSInternalInconsistencyException
173                                   reason:@"You are running a detached app from Xcode, but it hasn't been configured for local development yet. "
174                                           "You must run a packager for this Expo project before running it from XCode."
175                                 userInfo:nil];
176  }
177}
178
179- (void)_loadUrlSchemeFromInfoPlist:(NSDictionary *)infoPlist
180{
181  if (infoPlist[@"CFBundleURLTypes"]) {
182    // if the shell app has a custom url scheme, read that.
183    // this was configured when the shell app was built.
184    NSArray *urlTypes = infoPlist[@"CFBundleURLTypes"];
185    if (urlTypes && urlTypes.count) {
186      NSDictionary *urlType = urlTypes[0];
187      NSArray *urlSchemes = urlType[@"CFBundleURLSchemes"];
188      if (urlSchemes) {
189        for (NSString *urlScheme in urlSchemes) {
190          if ([self _isValidStandaloneUrlScheme:urlScheme forDevelopment:NO]) {
191            _urlScheme = urlScheme;
192            break;
193          }
194        }
195      }
196    }
197  }
198}
199
200- (void)_loadMiscPropertiesWithConfig:(NSDictionary *)shellConfig andInfoPlist:(NSDictionary *)infoPlist
201{
202  _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue];
203  _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil)
204    ? YES
205    : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue];
206  if (infoPlist[@"ExpoReleaseChannel"]) {
207    _releaseChannel = infoPlist[@"ExpoReleaseChannel"];
208  } else {
209    _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"];
210  }
211  // other shell config goes here
212}
213
214- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest
215{
216  id bundleUrl = manifest[@"bundleUrl"];
217  if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) {
218    _embeddedBundleUrl = (NSString *)bundleUrl;
219  }
220}
221
222- (void)_setAnalyticsPropertiesWithStandaloneManifestUrl:(NSString *)shellManifestUrl
223                                     isUserDetached:(BOOL)isUserDetached
224{
225  if (_testEnvironment == EXTestEnvironmentNone) {
226    [[EXAnalytics sharedInstance] setUserProperties:@{ @"INITIAL_URL": shellManifestUrl }];
227    [CrashlyticsKit setObjectValue:_standaloneManifestUrl forKey:@"initial_url"];
228    if (isUserDetached) {
229      [[EXAnalytics sharedInstance] setUserProperties:@{ @"IS_DETACHED": @YES }];
230    }
231  }
232}
233
234/**
235 *  Is this a valid url scheme for a standalone app?
236 */
237- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment
238{
239  // don't allow shell apps to intercept exp links
240  if (urlScheme && urlScheme.length) {
241    if (isForDevelopment) {
242      return YES;
243    } else {
244      // prod shell apps must have some non-exp/exps url scheme
245      return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]);
246    }
247  }
248  return NO;
249}
250
251@end
252