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