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