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