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