From 8a20a963fce20a660909ad3a3877d0f7381a782a Mon Sep 17 00:00:00 2001 From: Eric Samelson Date: Thu, 8 Nov 2018 15:23:24 -0800 Subject: [PATCH] [android][ios][home] persist session info in one location on clients (#2521) [android][ios][home][jest-expo] persist session info in one location on clients --- .../host/exp/exponent/ExponentManifest.java | 19 ++- .../exceptions/ManifestException.java | 2 +- .../exp/exponent/kernel/ExponentUrls.java | 12 +- .../modules/ExponentKernelModule.java | 46 ++++-- .../storage/ExponentSharedPreferences.java | 24 +++ home/storage/LocalStorage.js | 31 ++-- ios/Exponent.xcodeproj/project.pbxproj | 6 + .../CachedResource/EXManifestResource.m | 2 +- .../Kernel/AppLoader/EXFileDownloader.m | 3 +- ios/Exponent/Kernel/DevSupport/EXHomeModule.m | 34 ++++- ios/Exponent/Kernel/DevSupport/EXSession.h | 18 +++ ios/Exponent/Kernel/DevSupport/EXSession.m | 140 ++++++++++++++++++ .../Kernel/Environment/EXEnvironment.h | 5 - packages/jest-expo/src/expoModules.js | 5 + 14 files changed, 300 insertions(+), 47 deletions(-) create mode 100644 ios/Exponent/Kernel/DevSupport/EXSession.h create mode 100644 ios/Exponent/Kernel/DevSupport/EXSession.m diff --git a/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.java b/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.java index 00ae02484a70c4..39b76e1ed49a8c 100644 --- a/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.java +++ b/android/expoview/src/main/java/host/exp/exponent/ExponentManifest.java @@ -26,6 +26,7 @@ import host.exp.exponent.network.ExponentNetwork; import host.exp.exponent.storage.ExponentSharedPreferences; import host.exp.exponent.utils.ColorParser; +import host.exp.expoview.Exponent; import host.exp.expoview.R; import expolib_v1.okhttp3.Request; @@ -209,7 +210,11 @@ public void fetchManifest(final String manifestUrl, final ManifestListener liste String httpManifestUrl = uriBuilder.build().toString(); // Fetch manifest - Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl(httpManifestUrl, manifestUrl.equals(Constants.INITIAL_URL)); + Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl( + httpManifestUrl, + manifestUrl.equals(Constants.INITIAL_URL), + mExponentSharedPreferences.getSessionSecret() + ); requestBuilder.header("Exponent-Accept-Signature", "true"); requestBuilder.header("Expo-JSON-Error", "true"); requestBuilder.cacheControl(CacheControl.FORCE_NETWORK); @@ -290,7 +295,11 @@ public boolean fetchCachedManifest(final String manifestUrl, final ManifestListe } // Fetch manifest - Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl(httpManifestUrl, manifestUrl.equals(Constants.INITIAL_URL)); + Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl( + httpManifestUrl, + manifestUrl.equals(Constants.INITIAL_URL), + mExponentSharedPreferences.getSessionSecret() + ); requestBuilder.header("Exponent-Accept-Signature", "true"); requestBuilder.header("Expo-JSON-Error", "true"); @@ -404,7 +413,11 @@ public void onCachedResponse(ExpoResponse response, boolean isEmbedded) { public void fetchEmbeddedManifest(final String manifestUrl, final ManifestListener listener) { String httpManifestUrl = httpManifestUrlBuilder(manifestUrl).build().toString(); - Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl(httpManifestUrl, manifestUrl.equals(Constants.INITIAL_URL)); + Request.Builder requestBuilder = ExponentUrls.addExponentHeadersToManifestUrl( + httpManifestUrl, + manifestUrl.equals(Constants.INITIAL_URL), + mExponentSharedPreferences.getSessionSecret() + ); requestBuilder.header("Exponent-Accept-Signature", "true"); requestBuilder.header("Expo-JSON-Error", "true"); String finalUri = requestBuilder.build().url().toString(); diff --git a/android/expoview/src/main/java/host/exp/exponent/exceptions/ManifestException.java b/android/expoview/src/main/java/host/exp/exponent/exceptions/ManifestException.java index b8fd87a92668ca..b222ab739e2783 100644 --- a/android/expoview/src/main/java/host/exp/exponent/exceptions/ManifestException.java +++ b/android/expoview/src/main/java/host/exp/exponent/exceptions/ManifestException.java @@ -63,7 +63,7 @@ public String toString() { formattedMessage = "This experience requires a newer version of the Expo client - please download the latest version from the Play Store."; break; case "EXPERIENCE_NOT_VIEWABLE": - formattedMessage = "The experience you requested is not viewable by you. You will need to log in or ask the owner to grant you access."; + formattedMessage = rawMessage; // From server: The experience you requested is not viewable by you. You will need to log in or ask the owner to grant you access. break; case "USER_SNACK_NOT_FOUND": case "SNACK_NOT_FOUND": diff --git a/android/expoview/src/main/java/host/exp/exponent/kernel/ExponentUrls.java b/android/expoview/src/main/java/host/exp/exponent/kernel/ExponentUrls.java index cfcde52c7f2c61..205bc3560c55cd 100644 --- a/android/expoview/src/main/java/host/exp/exponent/kernel/ExponentUrls.java +++ b/android/expoview/src/main/java/host/exp/exponent/kernel/ExponentUrls.java @@ -13,8 +13,6 @@ public class ExponentUrls { - private static String sSessionSecret; - private static final List HTTPS_HOSTS = new ArrayList<>(); static { HTTPS_HOSTS.add("exp.host"); @@ -31,10 +29,6 @@ private static boolean isHttpsHost(final String host) { return false; } - public static void setSessionSecret(String sessionSecret) { - sSessionSecret = sessionSecret; - } - public static String toHttp(final String rawUrl) { if (rawUrl.startsWith("http")) { return rawUrl; @@ -59,7 +53,7 @@ public static Request.Builder addExponentHeadersToUrl(String urlString) { return builder; } - public static Request.Builder addExponentHeadersToManifestUrl(String urlString, boolean isShellAppManifest) { + public static Request.Builder addExponentHeadersToManifestUrl(String urlString, boolean isShellAppManifest, String sessionSecret) { Request.Builder builder = addExponentHeadersToUrl(urlString) .header("Accept", "application/expo+json,application/json"); @@ -79,8 +73,8 @@ public static Request.Builder addExponentHeadersToManifestUrl(String urlString, builder.header("Expo-Api-Version", "1") .header("Expo-Client-Environment", clientEnvironment); - if (sSessionSecret != null) { - builder.header("Expo-Session", sSessionSecret); + if (sessionSecret != null) { + builder.header("Expo-Session", sessionSecret); } return builder; diff --git a/android/expoview/src/main/java/host/exp/exponent/modules/ExponentKernelModule.java b/android/expoview/src/main/java/host/exp/exponent/modules/ExponentKernelModule.java index 53b3df596f3a74..9b704b719a0ebb 100644 --- a/android/expoview/src/main/java/host/exp/exponent/modules/ExponentKernelModule.java +++ b/android/expoview/src/main/java/host/exp/exponent/modules/ExponentKernelModule.java @@ -5,6 +5,7 @@ import android.app.Activity; import android.support.annotation.Nullable; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; @@ -14,7 +15,8 @@ import com.facebook.react.common.MapBuilder; import com.facebook.react.modules.core.DeviceEventManagerModule; -import java.io.IOException; +import org.json.JSONObject; + import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -28,16 +30,10 @@ import host.exp.exponent.experience.ExperienceActivity; import host.exp.exponent.kernel.ExponentKernelModuleInterface; import host.exp.exponent.kernel.ExponentKernelModuleProvider; -import host.exp.exponent.kernel.ExponentUrls; import host.exp.exponent.kernel.Kernel; -import host.exp.exponent.network.ExpoHttpCallback; -import host.exp.exponent.network.ExpoResponse; import host.exp.exponent.network.ExponentNetwork; import host.exp.exponent.storage.ExponentSharedPreferences; -import expolib_v1.okhttp3.Call; -import expolib_v1.okhttp3.Callback; -import expolib_v1.okhttp3.Request; -import expolib_v1.okhttp3.Response; +import host.exp.exponent.utils.JSONBundleConverter; public class ExponentKernelModule extends ReactContextBaseJavaModule implements ExponentKernelModuleInterface { @@ -112,13 +108,39 @@ public void consumeEventQueue() { } @ReactMethod - public void setSessionSecret(String sessionSecret) { - ExponentUrls.setSessionSecret(sessionSecret); + public void getSessionAsync(Promise promise) { + String sessionString = mExponentSharedPreferences.getString(ExponentSharedPreferences.EXPO_AUTH_SESSION); + try { + JSONObject sessionJsonObject = new JSONObject(sessionString); + WritableMap session = Arguments.fromBundle(JSONBundleConverter.JSONToBundle(sessionJsonObject)); + promise.resolve(session); + } catch (Exception e) { + promise.resolve(null); + EXL.e(TAG, e); + } } @ReactMethod - public void removeSessionSecret() { - ExponentUrls.setSessionSecret(null); + public void setSessionAsync(ReadableMap session, Promise promise) { + try { + JSONObject sessionJsonObject = new JSONObject(session.toHashMap()); + mExponentSharedPreferences.updateSession(sessionJsonObject); + promise.resolve(null); + } catch (Exception e) { + promise.reject("ERR_SESSION_NOT_SAVED", "Could not save session secret", e); + EXL.e(TAG, e); + } + } + + @ReactMethod + public void removeSessionAsync(Promise promise) { + try { + mExponentSharedPreferences.removeSession(); + promise.resolve(null); + } catch (Exception e) { + promise.reject("ERR_SESSION_NOT_REMOVED", "Could not remove session secret", e); + EXL.e(TAG, e); + } } @ReactMethod diff --git a/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.java b/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.java index 5780a8ed275175..ff23f7812c2775 100644 --- a/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.java +++ b/android/expoview/src/main/java/host/exp/exponent/storage/ExponentSharedPreferences.java @@ -51,6 +51,8 @@ public static class ManifestAndBundleUrl { public static final String SHOULD_NOT_USE_KERNEL_CACHE = "should_not_use_kernel_cache"; public static final String KERNEL_REVISION_ID = "kernel_revision_id"; public static final String SAFE_MANIFEST_KEY = "safe_manifest"; + public static final String EXPO_AUTH_SESSION = "expo_auth_session"; + public static final String EXPO_AUTH_SESSION_SECRET_KEY = "sessionSecret"; // Metadata public static final String EXPERIENCE_METADATA_PREFIX = "experience_metadata_"; @@ -136,6 +138,28 @@ public String getOrCreateUUID() { return uuid; } + public void updateSession(JSONObject session) { + setString(EXPO_AUTH_SESSION, session.toString()); + } + + public void removeSession() { + setString(EXPO_AUTH_SESSION, null); + } + + public String getSessionSecret() { + String sessionString = getString(EXPO_AUTH_SESSION); + if (sessionString == null) { + return null; + } + try { + JSONObject session = new JSONObject(sessionString); + return session.getString(EXPO_AUTH_SESSION_SECRET_KEY); + } catch (Exception e) { + EXL.e(TAG, e); + return null; + } + } + public void updateManifest(String manifestUrl, JSONObject manifest, String bundleUrl) { try { JSONObject parentObject = new JSONObject(); diff --git a/home/storage/LocalStorage.js b/home/storage/LocalStorage.js index 73ee95188f8028..dea4d2e802b496 100644 --- a/home/storage/LocalStorage.js +++ b/home/storage/LocalStorage.js @@ -49,19 +49,29 @@ async function updateSettingsAsync(updatedSettings) { } async function getSessionAsync() { - let results = await AsyncStorage.getItem(Keys.Session); - - try { - let session = JSON.parse(results); - return session; - } catch (e) { - return null; + let results = await ExponentKernel.getSessionAsync(); + if (!results) { + // NOTE(2018-11-8): we are migrating to storing all session keys + // using the Kernel module instead of AsyncStorage, but we need to + // continue to check the old location for a little while + // until all clients in use have migrated over + results = await AsyncStorage.getItem(Keys.Session); + if (results) { + try { + results = JSON.parse(results); + await saveSessionAsync(results); + await AsyncStorage.removeItem(Keys.Session); + } catch (e) { + return null; + } + } } + + return results; } async function saveSessionAsync(session) { - ExponentKernel.setSessionSecret(session.sessionSecret); - return AsyncStorage.setItem(Keys.Session, JSON.stringify(session)); + return ExponentKernel.setSessionAsync(session); } async function getHistoryAsync() { @@ -89,8 +99,7 @@ async function removeAuthTokensAsync() { } async function removeSessionAsync() { - ExponentKernel.removeSessionSecret(); - return AsyncStorage.removeItem(Keys.Session); + return ExponentKernel.removeSessionAsync(); } async function clearAllAsync() { diff --git a/ios/Exponent.xcodeproj/project.pbxproj b/ios/Exponent.xcodeproj/project.pbxproj index 17570444279c82..3ec48d7b6c970a 100644 --- a/ios/Exponent.xcodeproj/project.pbxproj +++ b/ios/Exponent.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 0779EE2620977C9E00C2D71B /* AIRGoogleMapOverlay.m in Sources */ = {isa = PBXBuildFile; fileRef = 0779EE2120977C9D00C2D71B /* AIRGoogleMapOverlay.m */; }; 0779EE2720977C9E00C2D71B /* AIRGoogleMapOverlayManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 0779EE2320977C9D00C2D71B /* AIRGoogleMapOverlayManager.m */; }; 0779EE2820977C9E00C2D71B /* AIRDummyView.m in Sources */ = {isa = PBXBuildFile; fileRef = 0779EE2420977C9E00C2D71B /* AIRDummyView.m */; }; + 0799CFDA21839B520039E97C /* EXSession.m in Sources */ = {isa = PBXBuildFile; fileRef = 0799CFD921839B520039E97C /* EXSession.m */; }; 07E26F301FFFFB55004667A1 /* EXCalendarConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 07E26F2D1FFFFB55004667A1 /* EXCalendarConverter.m */; }; 07E26F311FFFFB55004667A1 /* EXCalendar.m in Sources */ = {isa = PBXBuildFile; fileRef = 07E26F2E1FFFFB55004667A1 /* EXCalendar.m */; }; 08FCA2B1B8F4B2420E552303 /* libPods-Tests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FDC01B7874B93745E639DFA6 /* libPods-Tests.a */; }; @@ -347,6 +348,8 @@ 0779EE2320977C9D00C2D71B /* AIRGoogleMapOverlayManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRGoogleMapOverlayManager.m; sourceTree = ""; }; 0779EE2420977C9E00C2D71B /* AIRDummyView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRDummyView.m; sourceTree = ""; }; 0779EE2520977C9E00C2D71B /* AIRGoogleMapOverlay.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRGoogleMapOverlay.h; sourceTree = ""; }; + 0799CFD821839B470039E97C /* EXSession.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EXSession.h; sourceTree = ""; }; + 0799CFD921839B520039E97C /* EXSession.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EXSession.m; sourceTree = ""; }; 07E26F2C1FFFFB55004667A1 /* EXCalendar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = EXCalendar.h; sourceTree = ""; }; 07E26F2D1FFFFB55004667A1 /* EXCalendarConverter.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXCalendarConverter.m; sourceTree = ""; }; 07E26F2E1FFFFB55004667A1 /* EXCalendar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = EXCalendar.m; sourceTree = ""; }; @@ -1511,6 +1514,8 @@ B5AC39961E95A90B00540AA7 /* EXKernelDevKeyCommands.m */, B5AC39991E95A90B00540AA7 /* EXKernelDevMotionHandler.h */, B5AC399A1E95A90B00540AA7 /* EXKernelDevMotionHandler.m */, + 0799CFD821839B470039E97C /* EXSession.h */, + 0799CFD921839B520039E97C /* EXSession.m */, ); path = DevSupport; sourceTree = ""; @@ -2611,6 +2616,7 @@ B5AC39AA1E95A90B00540AA7 /* EXKernelDevKeyCommands.m in Sources */, B5FB74311FF6DADB001C764B /* EXVideoManager.m in Sources */, BB50E88D20B9C0D3003C752D /* RNRootViewGestureRecognizer.m in Sources */, + 0799CFDA21839B520039E97C /* EXSession.m in Sources */, 3159BB3621806E24002D2A81 /* RNSVGLine.m in Sources */, BB5BD32920AEFCB4007E02FC /* REAOperatorNode.m in Sources */, B54D2913205307380019411A /* EXAppLoadingCancelView.m in Sources */, diff --git a/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m b/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m index 1a3400cf1519e2..865470f8da11d3 100644 --- a/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m +++ b/ios/Exponent/Kernel/AppLoader/CachedResource/EXManifestResource.m @@ -432,7 +432,7 @@ - (NSError *)_formatError:(NSError *)error } else if ([errorCode isEqualToString:@"NO_COMPATIBLE_EXPERIENCE_FOUND"]){ formattedMessage = rawMessage; // No compatible experience found at ${originalUrl}. Only ${currentSdkVersions} are supported. } else if ([errorCode isEqualToString:@"EXPERIENCE_NOT_VIEWABLE"]) { - formattedMessage = [NSString stringWithFormat:@"The experience you requested is not viewable by you. You will need to log in or ask the owner to grant you access."]; + formattedMessage = rawMessage; // From server: The experience you requested is not viewable by you. You will need to log in or ask the owner to grant you access. } else if ([errorCode isEqualToString:@"USER_SNACK_NOT_FOUND"] || [errorCode isEqualToString:@"SNACK_NOT_FOUND"]) { formattedMessage = [NSString stringWithFormat:@"No snack found at %@.", self.originalUrl]; } else if ([errorCode isEqualToString:@"SNACK_RUNTIME_NOT_RELEASE"]) { diff --git a/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m b/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m index ec0f0c95f3967e..4e1efec8540f99 100644 --- a/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m +++ b/ios/Exponent/Kernel/AppLoader/EXFileDownloader.m @@ -2,6 +2,7 @@ #import "EXEnvironment.h" #import "EXFileDownloader.h" +#import "EXSession.h" #import "EXVersions.h" #import "EXKernelUtil.h" @@ -104,7 +105,7 @@ - (void)setHTTPHeaderFields:(NSMutableURLRequest *)request [request setValue:@"1" forHTTPHeaderField:@"Expo-Api-Version"]; [request setValue:clientEnvironment forHTTPHeaderField:@"Expo-Client-Environment"]; - NSString *sessionSecret = [EXEnvironment sharedEnvironment].sessionSecret; + NSString *sessionSecret = [[EXSession sharedInstance] sessionSecret]; if (sessionSecret) { [request setValue:sessionSecret forHTTPHeaderField:@"Expo-Session"]; } diff --git a/ios/Exponent/Kernel/DevSupport/EXHomeModule.m b/ios/Exponent/Kernel/DevSupport/EXHomeModule.m index 323f37e54bc9c0..bffc8382615923 100644 --- a/ios/Exponent/Kernel/DevSupport/EXHomeModule.m +++ b/ios/Exponent/Kernel/DevSupport/EXHomeModule.m @@ -2,6 +2,7 @@ #import "EXEnvironment.h" #import "EXHomeModule.h" +#import "EXSession.h" #import "EXUnversioned.h" #import @@ -170,14 +171,39 @@ - (void)dispatchJSEvent:(NSString *)eventName body:(NSDictionary *)eventBody onS } } -RCT_EXPORT_METHOD(setSessionSecret:(NSString *)sessionSecret) +RCT_REMAP_METHOD(getSessionAsync, + getSessionAsync:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) +{ + NSDictionary *session = [[EXSession sharedInstance] session]; + resolve(session); +} + +RCT_REMAP_METHOD(setSessionAsync, + setSessionAsync:(NSDictionary *)session + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { - [EXEnvironment sharedEnvironment].sessionSecret = sessionSecret; + NSError *error; + BOOL success = [[EXSession sharedInstance] saveSessionToKeychain:session error:&error]; + if (success) { + resolve(nil); + } else { + reject(@"ERR_SESSION_NOT_SAVED", @"Could not save session", error); + } } -RCT_EXPORT_METHOD(removeSessionSecret) +RCT_REMAP_METHOD(removeSessionAsync, + removeSessionAsync:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject) { - [EXEnvironment sharedEnvironment].sessionSecret = nil; + NSError *error; + BOOL success = [[EXSession sharedInstance] deleteSessionFromKeychainWithError:&error]; + if (success) { + resolve(nil); + } else { + reject(@"ERR_SESSION_NOT_REMOVED", @"Could not remove session", error); + } } RCT_EXPORT_METHOD(addDevMenu) diff --git a/ios/Exponent/Kernel/DevSupport/EXSession.h b/ios/Exponent/Kernel/DevSupport/EXSession.h new file mode 100644 index 00000000000000..db76e55ba4df71 --- /dev/null +++ b/ios/Exponent/Kernel/DevSupport/EXSession.h @@ -0,0 +1,18 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface EXSession : NSObject + ++ (instancetype)sharedInstance; + +- (NSDictionary * _Nullable)session; +- (NSString * _Nullable)sessionSecret; +- (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error; +- (BOOL)deleteSessionFromKeychainWithError:(NSError **)error; + +@end + +NS_ASSUME_NONNULL_END diff --git a/ios/Exponent/Kernel/DevSupport/EXSession.m b/ios/Exponent/Kernel/DevSupport/EXSession.m new file mode 100644 index 00000000000000..f19f529eefb811 --- /dev/null +++ b/ios/Exponent/Kernel/DevSupport/EXSession.m @@ -0,0 +1,140 @@ +// Copyright 2015-present 650 Industries. All rights reserved. + +#import "EXSession.h" +#import "EXUnversioned.h" + +NSString * const kEXSessionKeychainKey = @"host.exp.exponent.session"; +NSString * const kEXSessionKeychainService = @"app"; + +@interface EXSession () + +@property (nonatomic, strong) NSDictionary *session; + +@end + +@implementation EXSession + ++ (nonnull instancetype)sharedInstance +{ + static EXSession *theSession; + static dispatch_once_t once; + dispatch_once(&once, ^{ + if (!theSession) { + theSession = [[EXSession alloc] init]; + } + }); + return theSession; +} + +- (NSDictionary * _Nullable)session +{ + if (_session) { + return _session; + } + NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ + (__bridge id)kSecMatchLimit:(__bridge id)kSecMatchLimitOne, + (__bridge id)kSecReturnData:(__bridge id)kCFBooleanTrue + }]; + [query addEntriesFromDictionary:[self _searchQuery]]; + + CFTypeRef foundDict = NULL; + OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &foundDict); + + if (status == noErr) { + NSData *result = (__bridge_transfer NSData *)foundDict; + NSError *jsonError; + id session = [NSJSONSerialization JSONObjectWithData:result + options:kNilOptions + error:&jsonError]; + if (!jsonError && [session isKindOfClass:[NSDictionary class]]) { + return (NSDictionary *)session; + } + } + return nil; +} + +- (NSString * _Nullable)sessionSecret +{ + NSDictionary *session = [self session]; + if (!session) { + return nil; + } + + id sessionSecret = session[@"sessionSecret"]; + if (sessionSecret && [sessionSecret isKindOfClass:[NSString class]]) { + return (NSString *)sessionSecret; + } + return nil; +} + +- (BOOL)saveSessionToKeychain:(NSDictionary *)session error:(NSError **)error +{ + NSError *jsonError; + NSData *encodedData = [NSJSONSerialization dataWithJSONObject:session + options:kNilOptions + error:&jsonError]; + if (jsonError) { + if (error) { + *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") + code:-1 + userInfo:@{ + NSLocalizedDescriptionKey: @"Could not serialize JSON to save session to keychain", + NSUnderlyingErrorKey: jsonError + }]; + } + return NO; + } + + NSDictionary *searchQuery = [self _searchQuery]; + NSDictionary *updateQuery = @{ (__bridge id)kSecValueData:encodedData }; + NSMutableDictionary *addQuery = [NSMutableDictionary dictionaryWithDictionary:searchQuery]; + [addQuery addEntriesFromDictionary:updateQuery]; + + OSStatus status = SecItemAdd((__bridge CFDictionaryRef)addQuery, NULL); + + if (status == errSecDuplicateItem) { + status = SecItemUpdate((__bridge CFDictionaryRef)searchQuery, (__bridge CFDictionaryRef)updateQuery); + } + + if (status == errSecSuccess) { + _session = session; + return YES; + } else { + if (error) { + *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") + code:-1 + userInfo:@{ NSLocalizedDescriptionKey: @"Could not save session to keychain" }]; + } + return NO; + } +} + +- (BOOL)deleteSessionFromKeychainWithError:(NSError **)error +{ + OSStatus status = SecItemDelete((__bridge CFDictionaryRef)[self _searchQuery]); + + if (status == errSecSuccess) { + _session = nil; + return YES; + } else { + if (error) { + *error = [NSError errorWithDomain:EX_UNVERSIONED(@"EXKernelErrorDomain") + code:-1 + userInfo:@{ NSLocalizedDescriptionKey: @"Could not delete session from keychain" }]; + } + return NO; + } +} + +- (NSDictionary *)_searchQuery +{ + NSData *encodedKey = [kEXSessionKeychainKey dataUsingEncoding:NSUTF8StringEncoding]; + return @{ + (__bridge id)kSecClass:(__bridge id)kSecClassGenericPassword, + (__bridge id)kSecAttrService:kEXSessionKeychainService, + (__bridge id)kSecAttrGeneric:encodedKey, + (__bridge id)kSecAttrAccount:encodedKey + }; +} + +@end diff --git a/ios/Exponent/Kernel/Environment/EXEnvironment.h b/ios/Exponent/Kernel/Environment/EXEnvironment.h index f2a9ac13aaf3e2..f060a4521805ec 100644 --- a/ios/Exponent/Kernel/Environment/EXEnvironment.h +++ b/ios/Exponent/Kernel/Environment/EXEnvironment.h @@ -58,11 +58,6 @@ FOUNDATION_EXPORT NSString * const kEXEmbeddedManifestResourceName; */ @property (nonatomic, readonly) BOOL areRemoteUpdatesEnabled; -/** - * The session secret for the signed in user in the Expo Client - */ -@property (nonatomic, strong, nullable) NSString *sessionSecret; - /** * Whether the app is running in a test environment (local Xcode test target, CI, or not at all). */ diff --git a/packages/jest-expo/src/expoModules.js b/packages/jest-expo/src/expoModules.js index 400b61aef559fd..8e06b9e88e6b95 100644 --- a/packages/jest-expo/src/expoModules.js +++ b/packages/jest-expo/src/expoModules.js @@ -465,6 +465,11 @@ module.exports = { activate: { type: 'function', functionType: 'async' }, deactivate: { type: 'function', functionType: 'async' }, }, + ExponentKernel: { + getSessionAsync: { type: 'function', functionType: 'async' }, + removeSessionAsync: { type: 'function', functionType: 'async' }, + setSessionAsync: { type: 'function', functionType: 'async' }, + }, ExponentLinearGradientManager: {}, ExponentMailComposer: { composeAsync: { type: 'function', functionType: 'promise' } }, ExponentNotifications: {