From 31601c007d63ef2a90456dd6768968c178e635f6 Mon Sep 17 00:00:00 2001 From: Federico Bartoli Date: Fri, 9 Oct 2026 14:55:59 +0200 Subject: [PATCH 1/3] fix: do not forward redirects to a completed session on iOS (cherry picked from commit d22101188c0384128443a210ca2dc6408acba964) --- .changeset/fix-ios-completed-session.md | 5 +++ .../react-native-app-auth/ios/RNAppAuth.m | 32 ++++++++++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-ios-completed-session.md diff --git a/.changeset/fix-ios-completed-session.md b/.changeset/fix-ios-completed-session.md new file mode 100644 index 000000000..ebec200bd --- /dev/null +++ b/.changeset/fix-ios-completed-session.md @@ -0,0 +1,5 @@ +--- +"react-native-app-auth": patch +--- + +Fix an iOS crash ("An OAuth redirect was sent to a OIDExternalUserAgentSession after it already completed") by not keeping an authorization or logout session that completed synchronously, and by forwarding at most one redirect to each session. diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index c1181d6d5..e266a0dfc 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -17,7 +17,18 @@ @interface RNAppAuth() { @implementation RNAppAuth -(BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url { - return [_currentSession resumeExternalUserAgentFlowWithURL:url]; + id session = _currentSession; + if (session == nil) { + return NO; + } + BOOL handled = [session resumeExternalUserAgentFlowWithURL:url]; + // A session consumes at most one redirect. Once it has accepted one, stop forwarding URLs to + // it: the code exchange that may still be running does not need the session, and AppAuth + // < 2.1.0 raises an NSException when a completed session is resumed again. + if (handled && _currentSession == session) { + _currentSession = nil; + } + return handled; } - (dispatch_queue_t)methodQueue @@ -374,6 +385,9 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration } appDelegate.authorizationFlowManagerDelegate = self; __weak typeof(self) weakSelf = self; + // Set when the flow's callback has run. The callback can run synchronously, before the + // presenting call below returns (e.g. when the user-agent fails to open). + __block BOOL flowCompleted = NO; rnAppAuthTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; @@ -388,6 +402,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration OIDAuthorizationCallback callback = ^(OIDAuthorizationResponse *_Nullable authorizationResponse, NSError *_Nullable error) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -424,6 +439,7 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration NSError *_Nullable error ) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -456,6 +472,12 @@ - (void)authorizeWithConfiguration: (OIDServiceConfiguration *) configuration } } } + + // If the callback already ran, the session returned above has completed: do not keep it, + // otherwise a later redirect would be forwarded to a finished session. + if (flowCompleted) { + _currentSession = nil; + } } /* @@ -523,6 +545,9 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration } appDelegate.authorizationFlowManagerDelegate = self; __weak typeof(self) weakSelf = self; + // Set when the flow's callback has run. The callback can run synchronously, before the + // presenting call below returns (e.g. when the user-agent fails to open). + __block BOOL flowCompleted = NO; rnAppAuthTaskId = [UIApplication.sharedApplication beginBackgroundTaskWithExpirationHandler:^{ [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; @@ -540,6 +565,7 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration externalUserAgent: externalUserAgent callback: ^(OIDEndSessionResponse *_Nullable response, NSError *_Nullable error) { typeof(self) strongSelf = weakSelf; + flowCompleted = YES; strongSelf->_currentSession = nil; [UIApplication.sharedApplication endBackgroundTask:rnAppAuthTaskId]; rnAppAuthTaskId = UIBackgroundTaskInvalid; @@ -551,6 +577,10 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration error:error]; } }]; + + if (flowCompleted) { + _currentSession = nil; + } } - (void)configureUrlSession: (NSDictionary*) headers sessionTimeout: (double) sessionTimeout{ From b788da0f7a5b8f20708964db668ed4f0b7ffdc82 Mon Sep 17 00:00:00 2001 From: Federico Bartoli Date: Fri, 9 Oct 2026 17:15:32 +0200 Subject: [PATCH 2/3] chore: comment the completed-flow check in logout (cherry picked from commit aa18f255fcc9166d4c8adde80f1915b8a78b9776) --- packages/react-native-app-auth/ios/RNAppAuth.m | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-app-auth/ios/RNAppAuth.m b/packages/react-native-app-auth/ios/RNAppAuth.m index e266a0dfc..79cda3c59 100644 --- a/packages/react-native-app-auth/ios/RNAppAuth.m +++ b/packages/react-native-app-auth/ios/RNAppAuth.m @@ -578,6 +578,7 @@ - (void)endSessionWithConfiguration: (OIDServiceConfiguration *) configuration } }]; + // As in authorizeWithConfiguration:, do not keep a session whose callback already ran. if (flowCompleted) { _currentSession = nil; } From de80ab384ea5d7bc92ad640cc4341e2ec6aba353 Mon Sep 17 00:00:00 2001 From: Eli Zibin <1131641+zibs@users.noreply.github.com> Date: Mon, 19 Oct 2026 08:56:20 -0700 Subject: [PATCH 3/3] test(ios): cover completed authorization and logout sessions Compile the real module and AppAuth in a simulator regression runner. (cherry picked from commit a517587f6377937e1b9ed46aab01f9d9475b6ebb) --- .../native-tests/README.md | 21 ++ .../native-tests/ios-session.m | 236 ++++++++++++++++++ .../native-tests/ios-session.sh | 67 +++++ packages/react-native-app-auth/package.json | 1 + 4 files changed, 325 insertions(+) create mode 100644 packages/react-native-app-auth/native-tests/README.md create mode 100644 packages/react-native-app-auth/native-tests/ios-session.m create mode 100644 packages/react-native-app-auth/native-tests/ios-session.sh diff --git a/packages/react-native-app-auth/native-tests/README.md b/packages/react-native-app-auth/native-tests/README.md new file mode 100644 index 000000000..b0b90e50f --- /dev/null +++ b/packages/react-native-app-auth/native-tests/README.md @@ -0,0 +1,21 @@ +# iOS native checks + +With Xcode installed and an iOS simulator booted: + +```sh +yarn workspace react-native-app-auth test:ios-presenter SIMULATOR_UDID +yarn workspace react-native-app-auth test:ios-session SIMULATOR_UDID +``` + +The session checks also require AppAuth sources from the demo's installed CocoaPods. +Set `APP_AUTH_SOURCE_DIR` to use another checkout of AppAuth instead. AppAuth 2.0.0 +exercises the completed-session exception fixed by the module. + +The session runner compiles the actual `RNAppAuth.m`, scene presenter, and AppAuth +sources into a small simulator app. Only React bridge declarations are stubbed. +A controlled browser and URL protocol exercise failed presentation, code-only login, +wrong-state rejection, cancellation, token exchange with a duplicate redirect, and +logout. No credentials, real browser, or live identity provider are used. + +These checks verify native session lifecycle behavior; they do not replace testing +browser integration or a provider's login/logout flow in a React Native app. diff --git a/packages/react-native-app-auth/native-tests/ios-session.m b/packages/react-native-app-auth/native-tests/ios-session.m new file mode 100644 index 000000000..f464c91c5 --- /dev/null +++ b/packages/react-native-app-auth/native-tests/ios-session.m @@ -0,0 +1,236 @@ +#import +#import "AppAuth.h" +#import "RNAppAuth.h" +#import "RNAppAuthAuthorizationFlowManager.h" + +@interface RNAppAuth (SessionTests) +- (BOOL)resumeExternalUserAgentFlowWithURL:(NSURL *)url; +- (void)authorizeWithConfiguration:(OIDServiceConfiguration *)configuration + redirectUrl:(NSString *)redirectUrl clientId:(NSString *)clientId + clientSecret:(NSString *)clientSecret scopes:(NSArray *)scopes + useNonce:(BOOL)useNonce usePKCE:(BOOL)usePKCE + additionalParameters:(NSDictionary *)additionalParameters + skipCodeExchange:(BOOL)skipCodeExchange iosCustomBrowser:(NSString *)iosCustomBrowser + prefersEphemeralSession:(BOOL)prefersEphemeralSession + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +- (void)endSessionWithConfiguration:(OIDServiceConfiguration *)configuration + idTokenHint:(NSString *)idTokenHint postLogoutRedirectURL:(NSString *)postLogoutRedirectURL + additionalParameters:(NSDictionary *)additionalParameters iosCustomBrowser:(NSString *)iosCustomBrowser + prefersEphemeralSession:(BOOL)prefersEphemeralSession + resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject; +@end + +// Exercise actual AppAuth sessions without opening a browser. +@interface TestBrowser : NSObject +@property(nonatomic) BOOL opens; +@property(nonatomic, weak) id session; +@property(nonatomic, strong) id request; +@end +@implementation TestBrowser +- (BOOL)presentExternalUserAgentRequest:(id)request + session:(id)session { + self.session = session; + self.request = request; + return self.opens; +} +- (void)dismissExternalUserAgentAnimated:(BOOL)animated completion:(void (^)(void))completion { + if (completion) completion(); +} +@end + +@interface TestModule : RNAppAuth +@property(nonatomic, strong) TestBrowser *browser; +@end +@implementation TestModule +- (id)getCustomBrowser:(NSString *)browserType { return self.browser; } +@end + +// Hold the real token exchange open until the duplicate redirect has been delivered. +static NSURLProtocol *pendingTokenRequest; +@interface TokenProtocol : NSURLProtocol +@end +@implementation TokenProtocol ++ (BOOL)canInitWithRequest:(NSURLRequest *)request { + return [request.URL.host isEqualToString:@"session-tests.invalid"]; +} ++ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request { return request; } +- (void)startLoading { @synchronized(TokenProtocol.class) { pendingTokenRequest = self; } } +- (void)stopLoading {} +@end + +static NSUInteger checks; +static void Check(BOOL condition, NSString *description) { + if (!condition) { + fprintf(stderr, "FAIL: %s\n", description.UTF8String); + exit(1); + } + checks++; + fprintf(stderr, "PASS: %s\n", description.UTF8String); +} + +static void WaitUntil(BOOL (^condition)(void)) { + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:5]; + while (!condition() && deadline.timeIntervalSinceNow > 0) { + [NSRunLoop.currentRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; + } + Check(condition(), @"asynchronous operation completes within timeout"); +} + +static OIDServiceConfiguration *Configuration(void) { + return [[OIDServiceConfiguration alloc] + initWithAuthorizationEndpoint:[NSURL URLWithString:@"https://session-tests.invalid/authorize"] + tokenEndpoint:[NSURL URLWithString:@"https://session-tests.invalid/token"] + issuer:nil registrationEndpoint:nil + endSessionEndpoint:[NSURL URLWithString:@"https://session-tests.invalid/logout"]]; +} + +static TestModule *Module(BOOL opens) { + TestModule *module = [TestModule new]; + module.browser = [TestBrowser new]; + module.browser.opens = opens; + return module; +} + +static void Authorize(TestModule *module, BOOL skipExchange, + RCTPromiseResolveBlock resolve, RCTPromiseRejectBlock reject) { + [module authorizeWithConfiguration:Configuration() redirectUrl:@"com.rnaa.tests:/callback" + clientId:@"test-client" clientSecret:nil scopes:@[@"openid"] + useNonce:NO usePKCE:YES additionalParameters:@{@"state": @"expected-state"} + skipCodeExchange:skipExchange iosCustomBrowser:@"controlled" + prefersEphemeralSession:NO resolve:resolve reject:reject]; +} + +static void RunTests(void) { + NSURL *redirect = [NSURL URLWithString:@"com.rnaa.tests:/callback?code=test-code&state=expected-state"]; + RCTPromiseResolveBlock unexpectedResolve = ^(id result) { Check(NO, @"unexpected success"); }; + RCTPromiseRejectBlock unexpectedReject = ^(NSString *code, NSString *message, NSError *error) { + Check(NO, [NSString stringWithFormat:@"unexpected failure: %@ %@", code, message]); + }; + Check(![Module(YES) resumeExternalUserAgentFlowWithURL:redirect], @"no pending flow ignores redirect"); + + for (NSNumber *skip in @[@YES, @NO]) { + TestModule *module = Module(NO); + __block NSUInteger rejected = 0; + Authorize(module, skip.boolValue, unexpectedResolve, ^(NSString *code, NSString *message, NSError *error) { + Check(error.code == OIDErrorCodeSafariOpenError, @"authorization fails in the browser-open path"); + rejected++; + }); + Check(rejected == 1, @"browser-open failure rejects authorization once"); + Check([module valueForKey:@"currentSession"] == nil, @"synchronously completed authorization is not retained"); + Check(![module resumeExternalUserAgentFlowWithURL:redirect], @"redirect after failed authorization does not crash"); + } + + TestModule *logout = Module(NO); + __block NSUInteger logoutRejected = 0; + [logout endSessionWithConfiguration:Configuration() idTokenHint:@"test-id-token" + postLogoutRedirectURL:@"com.rnaa.tests:/logout" additionalParameters:@{} + iosCustomBrowser:@"controlled" prefersEphemeralSession:NO + resolve:unexpectedResolve reject:^(NSString *code, NSString *message, NSError *error) { + Check(error.code == OIDErrorCodeSafariOpenError, @"logout fails in the browser-open path"); + logoutRejected++; + }]; + Check(logoutRejected == 1, @"browser-open failure rejects logout once"); + Check([logout valueForKey:@"currentSession"] == nil, @"synchronously completed logout is not retained"); + Check(![logout resumeExternalUserAgentFlowWithURL:[NSURL URLWithString:@"com.rnaa.tests:/logout"]], + @"redirect after failed logout does not crash"); + + TestModule *skip = Module(YES); + __block NSDictionary *authorization; + Authorize(skip, YES, ^(id result) { authorization = result; }, unexpectedReject); + Check(![skip resumeExternalUserAgentFlowWithURL:[NSURL URLWithString:@"com.other:/unrelated"]], + @"unrelated URL is not consumed"); + Check([skip valueForKey:@"currentSession"] != nil, @"unrelated URL preserves pending flow"); + Check([skip resumeExternalUserAgentFlowWithURL:redirect], @"matching URL completes code-only authorization"); + Check([authorization[@"authorizationCode"] isEqual:@"test-code"] && [authorization[@"codeVerifier"] length] > 0, + @"code-only authorization retains code and PKCE verifier"); + Check(![skip resumeExternalUserAgentFlowWithURL:redirect], @"duplicate code-only redirect is ignored"); + + TestModule *wrongState = Module(YES); + __block NSUInteger stateRejected = 0; + Authorize(wrongState, YES, unexpectedResolve, ^(NSString *code, NSString *message, NSError *error) { + stateRejected++; + }); + Check([wrongState resumeExternalUserAgentFlowWithURL:[NSURL URLWithString:@"com.rnaa.tests:/callback?code=test&state=wrong"]], + @"matching URL with wrong state reaches AppAuth validation"); + Check(stateRejected == 1, @"AppAuth rejects wrong state"); + Check(![wrongState resumeExternalUserAgentFlowWithURL:redirect], @"rejected state leaves no resumable session"); + + TestModule *cancelled = Module(YES); + __block NSUInteger cancelRejected = 0; + Authorize(cancelled, NO, unexpectedResolve, ^(NSString *code, NSString *message, NSError *error) { + cancelRejected++; + }); + [cancelled.browser.session cancel]; + Check(cancelRejected == 1, @"cancellation rejects authorization once"); + Check(![cancelled resumeExternalUserAgentFlowWithURL:redirect], @"redirect after cancellation is ignored"); + + NSURLSessionConfiguration *sessionConfiguration = NSURLSessionConfiguration.ephemeralSessionConfiguration; + sessionConfiguration.protocolClasses = @[TokenProtocol.class]; + [OIDURLSessionProvider setSession:[NSURLSession sessionWithConfiguration:sessionConfiguration]]; + TestModule *token = Module(YES); + __block NSDictionary *tokens; + Authorize(token, NO, ^(id result) { tokens = result; }, unexpectedReject); + Check([token resumeExternalUserAgentFlowWithURL:redirect], @"first redirect starts real token exchange"); + WaitUntil(^BOOL { @synchronized(TokenProtocol.class) { return pendingTokenRequest != nil; } }); + Check(tokens == nil, @"token exchange remains pending"); + Check([token valueForKey:@"currentSession"] == nil, @"consumed browser session is cleared before token response"); + Check(![token resumeExternalUserAgentFlowWithURL:redirect], @"duplicate redirect during token exchange does not crash"); + NSURLProtocol *request = pendingTokenRequest; + NSData *body = [@"{\"access_token\":\"test-access-token\",\"token_type\":\"Bearer\",\"expires_in\":3600}" dataUsingEncoding:NSUTF8StringEncoding]; + NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:request.request.URL statusCode:200 + HTTPVersion:@"HTTP/1.1" headerFields:@{@"Content-Type": @"application/json"}]; + [request.client URLProtocol:request didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + [request.client URLProtocol:request didLoadData:body]; + [request.client URLProtocolDidFinishLoading:request]; + WaitUntil(^BOOL { return tokens != nil; }); + Check([tokens[@"accessToken"] isEqual:@"test-access-token"], @"clearing browser session preserves token completion"); + pendingTokenRequest = nil; + + TestModule *successLogout = Module(YES); + __block NSDictionary *logoutResponse; + [successLogout endSessionWithConfiguration:Configuration() idTokenHint:@"test-id-token" + postLogoutRedirectURL:@"com.rnaa.tests:/logout" additionalParameters:@{} + iosCustomBrowser:@"controlled" prefersEphemeralSession:NO + resolve:^(id result) { logoutResponse = result; } reject:unexpectedReject]; + OIDEndSessionRequest *logoutRequest = (OIDEndSessionRequest *)successLogout.browser.request; + NSURL *logoutRedirect = [NSURL URLWithString:[@"com.rnaa.tests:/logout?state=" stringByAppendingString:logoutRequest.state]]; + Check([successLogout resumeExternalUserAgentFlowWithURL:logoutRedirect], @"matching logout redirect is consumed"); + Check([logoutResponse[@"idTokenHint"] isEqual:@"test-id-token"], @"logout completes successfully"); + Check(![successLogout resumeExternalUserAgentFlowWithURL:logoutRedirect], @"duplicate logout redirect is ignored"); + + NSString *results = [NSString stringWithFormat:@"All %lu iOS session checks passed.\n", (unsigned long)checks]; + NSString *resultPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject + stringByAppendingPathComponent:@"session-results.txt"]; + [results writeToFile:resultPath atomically:YES encoding:NSUTF8StringEncoding error:nil]; + fprintf(stderr, "%s", results.UTF8String); +} + +@interface SessionSceneDelegate : UIResponder +@property(nonatomic, strong) UIWindow *window; +@end +@implementation SessionSceneDelegate +- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)options { + self.window = [[UIWindow alloc] initWithWindowScene:(UIWindowScene *)scene]; + self.window.rootViewController = [UIViewController new]; + [self.window makeKeyAndVisible]; + UIApplication.sharedApplication.delegate.window = self.window; +} +- (void)sceneDidBecomeActive:(UIScene *)scene { + RunTests(); + exit(0); +} +@end + +@interface SessionAppDelegate : UIResponder +@property(nonatomic, strong) UIWindow *window; +@property(nonatomic, weak) id authorizationFlowManagerDelegate; +@end +@implementation SessionAppDelegate +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)options { return YES; } +@end + +int main(int argc, char *argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass(SessionAppDelegate.class)); + } +} diff --git a/packages/react-native-app-auth/native-tests/ios-session.sh b/packages/react-native-app-auth/native-tests/ios-session.sh new file mode 100644 index 000000000..7aea7b10e --- /dev/null +++ b/packages/react-native-app-auth/native-tests/ios-session.sh @@ -0,0 +1,67 @@ +#!/bin/bash +set -euo pipefail + +simulator_id="${1:?Pass the UDID of a booted iOS simulator}" +package_dir="$(cd "$(dirname "$0")/.." && pwd)" +appauth_dir="${APP_AUTH_SOURCE_DIR:-$package_dir/../../examples/demo/ios/Pods/AppAuth}" +test_dir="$(mktemp -d "${TMPDIR:-/tmp}/rnaa-ios-session.XXXXXX")" +trap 'rm -rf "$test_dir"' EXIT +sdk_path="$(xcrun --sdk iphonesimulator --show-sdk-path)" +architecture="$(uname -m)" + +if [[ ! -f "$appauth_dir/Sources/AppAuthCore/OIDAuthState.m" ]]; then + echo 'Install the demo CocoaPods first, or set APP_AUTH_SOURCE_DIR to AppAuth sources.' >&2 + exit 1 +fi + +# Only React bridge declarations are stubbed. Compile the real RNAppAuth and AppAuth code. +mkdir -p "$test_dir/React" "$test_dir/SessionTests.app" +cat > "$test_dir/React/RCTBridgeModule.h" <<'HEADER' +#import +#import +typedef void (^RCTPromiseResolveBlock)(id result); +typedef void (^RCTPromiseRejectBlock)(NSString *code, NSString *message, NSError *error); +@protocol RCTBridgeModule +@end +#define RCT_EXPORT_MODULE(...) +#define RCT_REMAP_METHOD(js_name, ...) - (void)__VA_ARGS__ +HEADER +touch "$test_dir/React/RCTLog.h" "$test_dir/React/RCTConvert.h" + +cat > "$test_dir/SessionTests.app/Info.plist" <<'PLIST' + + + +CFBundleIdentifiercom.rnappauth.session-tests +CFBundleExecutableSessionTests +CFBundleNameSessionTests +CFBundlePackageTypeAPPL +CFBundleVersion1 +CFBundleShortVersionString1.0 +MinimumOSVersion13.0 +UIApplicationSceneManifest +UIApplicationSupportsMultipleScenes +UISceneConfigurations +UIWindowSceneSessionRoleApplication +UISceneConfigurationNameDefault +UISceneDelegateClassNameSessionSceneDelegate + + +PLIST + +appauth_sources=("$appauth_dir"/Sources/AppAuthCore/*.m "$appauth_dir"/Sources/AppAuth/iOS/*.m) +xcrun clang -fobjc-arc -fblocks -Wno-deprecated-declarations \ + -target "$architecture-apple-ios13.0-simulator" -isysroot "$sdk_path" \ + -framework UIKit -framework Foundation -framework SafariServices \ + -framework AuthenticationServices -framework Security \ + -I "$test_dir" -I "$package_dir/ios" -I "$appauth_dir/Sources" \ + -I "$appauth_dir/Sources/AppAuthCore" -I "$appauth_dir/Sources/AppAuth/iOS" \ + "${appauth_sources[@]}" "$package_dir/ios/RNAppAuth.m" \ + "$package_dir/ios/RNAppAuthPresentation.m" "$package_dir/native-tests/ios-session.m" \ + -o "$test_dir/SessionTests.app/SessionTests" + +xcrun simctl uninstall "$simulator_id" com.rnappauth.session-tests >/dev/null 2>&1 || true +xcrun simctl install "$simulator_id" "$test_dir/SessionTests.app" +xcrun simctl launch --console "$simulator_id" com.rnappauth.session-tests +data_dir="$(xcrun simctl get_app_container "$simulator_id" com.rnappauth.session-tests data)" +cat "$data_dir/Documents/session-results.txt" diff --git a/packages/react-native-app-auth/package.json b/packages/react-native-app-auth/package.json index dfcc499ee..72b443487 100644 --- a/packages/react-native-app-auth/package.json +++ b/packages/react-native-app-auth/package.json @@ -7,6 +7,7 @@ "scripts": { "test": "jest", "test:ios-presenter": "bash native-tests/ios-presentation.sh", + "test:ios-session": "bash native-tests/ios-session.sh", "lint": "eslint .", "build": "expo-module build plugin", "clean": "expo-module clean plugin",