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 _allManifestUrls = @[]; 55 _isDebugXCodeScheme = NO; 56 _releaseChannel = @"default"; 57} 58 59- (void)_loadDefaultConfig 60{ 61 // use bundled EXShell.plist 62 NSString *configPath = [[NSBundle mainBundle] pathForResource:@"EXShell" ofType:@"plist"]; 63 NSDictionary *shellConfig = (configPath) ? [NSDictionary dictionaryWithContentsOfFile:configPath] : [NSDictionary dictionary]; 64 65 // use ExpoKit dev url from EXBuildConstants 66 NSString *expoKitDevelopmentUrl = [EXBuildConstants sharedInstance].expoKitDevelopmentUrl; 67 68 // use bundled info.plist 69 NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; 70 71 // use bundled shell app manifest 72 NSDictionary *embeddedManifest = @{}; 73 NSString *path = [[NSBundle mainBundle] pathForResource:kEXEmbeddedManifestResourceName ofType:@"json"]; 74 NSData *data = [NSData dataWithContentsOfFile:path]; 75 if (data) { 76 NSError *jsonError; 77 id manifest = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError]; 78 if (!jsonError && [manifest isKindOfClass:[NSDictionary class]]) { 79 embeddedManifest = (NSDictionary *)manifest; 80 } 81 } 82 83 BOOL isDetached = NO; 84#ifdef EX_DETACHED 85 isDetached = YES; 86#endif 87 88 BOOL isDebugXCodeScheme = NO; 89#if DEBUG 90 isDebugXCodeScheme = YES; 91#endif 92 93 BOOL isUserDetach = NO; 94 if (isDetached) { 95#ifndef EX_DETACHED_SERVICE 96 isUserDetach = YES; 97#endif 98 } 99 100 [self _loadShellConfig:shellConfig 101 withInfoPlist:infoPlist 102 withExpoKitDevUrl:expoKitDevelopmentUrl 103 withEmbeddedManifest:embeddedManifest 104 isDetached:isDetached 105 isDebugXCodeScheme:isDebugXCodeScheme 106 isUserDetach:isUserDetach]; 107} 108 109- (void)_loadShellConfig:(NSDictionary *)shellConfig 110 withInfoPlist:(NSDictionary *)infoPlist 111 withExpoKitDevUrl:(NSString *)expoKitDevelopmentUrl 112 withEmbeddedManifest:(NSDictionary *)embeddedManifest 113 isDetached:(BOOL)isDetached 114 isDebugXCodeScheme:(BOOL)isDebugScheme 115 isUserDetach:(BOOL)isUserDetach 116{ 117 [self _reset]; 118 NSMutableArray *allManifestUrls = [NSMutableArray array]; 119 _isDetached = isDetached; 120 _isDebugXCodeScheme = isDebugScheme; 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 andInfoPlist:infoPlist]; 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 andInfoPlist:(NSDictionary *)infoPlist 200{ 201 _isManifestVerificationBypassed = [shellConfig[@"isManifestVerificationBypassed"] boolValue]; 202 _areRemoteUpdatesEnabled = (shellConfig[@"areRemoteUpdatesEnabled"] == nil) 203 ? YES 204 : [shellConfig[@"areRemoteUpdatesEnabled"] boolValue]; 205 if (infoPlist[@"ExpoReleaseChannel"]) { 206 _releaseChannel = infoPlist[@"ExpoReleaseChannel"]; 207 } else { 208 _releaseChannel = (shellConfig[@"releaseChannel"] == nil) ? @"default" : shellConfig[@"releaseChannel"]; 209 } 210 // other shell config goes here 211} 212 213- (void)_loadEmbeddedBundleUrlWithManifest:(NSDictionary *)manifest 214{ 215 id bundleUrl = manifest[@"bundleUrl"]; 216 if (bundleUrl && [bundleUrl isKindOfClass:[NSString class]]) { 217 _embeddedBundleUrl = (NSString *)bundleUrl; 218 } 219} 220 221- (void)_setAnalyticsPropertiesWithStandaloneManifestUrl:(NSString *)shellManifestUrl 222 isUserDetached:(BOOL)isUserDetached 223{ 224 if (_testEnvironment == EXTestEnvironmentNone) { 225 [[EXAnalytics sharedInstance] setUserProperties:@{ @"INITIAL_URL": shellManifestUrl }]; 226 if (isUserDetached) { 227 [[EXAnalytics sharedInstance] setUserProperties:@{ @"IS_DETACHED": @YES }]; 228 } 229 } 230} 231 232/** 233 * Is this a valid url scheme for a standalone app? 234 */ 235- (BOOL)_isValidStandaloneUrlScheme:(NSString *)urlScheme forDevelopment:(BOOL)isForDevelopment 236{ 237 // don't allow shell apps to intercept exp links 238 if (urlScheme && urlScheme.length) { 239 if (isForDevelopment) { 240 return YES; 241 } else { 242 // prod shell apps must have some non-exp/exps url scheme 243 return (![urlScheme isEqualToString:@"exp"] && ![urlScheme isEqualToString:@"exps"]); 244 } 245 } 246 return NO; 247} 248 249@end 250