Environment
- Platform: iOS physical device (simulator works fine)
- Package:
@privy-io/expo latest
- Package:
@privy-io/expo-native-extensions latest
- iOS Version: Latest
- Xcode Version: Latest
- React Native: Used as a single-page embedded view within a native iOS app (not a full RN app)
React Native Usage Context
Important: Our app is primarily a native iOS app that uses React Native only for the login page. The architecture is:
- Main app: Native iOS (Swift/UIKit)
- Login page only: React Native component (
LoginModal) embedded via RCTRootView
- The RN view is presented modally when login is needed and dismissed after successful login
- Each time the login page is shown, a new
RCTRootView is created with PrivyProvider
This means PrivyProvider is mounted/unmounted each time the user needs to login, rather than staying mounted for the app's lifetime.
Description
After a successful OAuth login (Google/Apple), logging out, and then attempting to log in again, the Privy SDK fails to initialize properly. The isReady state from usePrivy() hook remains false indefinitely, preventing any login operations.
Key observation: This issue only occurs on physical iOS devices, not on iOS Simulator. Killing and restarting the app resolves the issue temporarily.
Steps to Reproduce
- Fresh app launch on iOS physical device
- User clicks "Login with Google" → OAuth flow completes successfully ✅
- User logs out via
logout() from usePrivy()
- Login modal is dismissed (PrivyProvider unmounts, RCTRootView removed)
- User triggers login again → New login modal appears (new RCTRootView, new PrivyProvider mounts)
- User clicks "Login with Google" → Nothing happens ❌
Expected Behavior
After PrivyProvider remounts (in a new RCTRootView), isReady should become true, allowing the user to login again.
Actual Behavior
- First mount:
isReady transitions from false → true ✅
- Second mount (after logout):
isReady stays false forever ❌
Debug Logs
First login (works):
usePrivy() isReady=false
usePrivy() isReady=true ← SDK initialized successfully
loginOAuth() called → OAuth popup opens → login success
Second login attempt (fails):
usePrivy() isReady=false
usePrivy() isReady=false ← Never becomes true
usePrivy() isReady=false ← Stays false indefinitely
loginOAuth() called → Nothing happens (hangs or fails silently)
Code Context
Native iOS Side (Swift)
// RCTBridgeManager.swift - Manages the React Native bridge
class RCTBridgeManager {
static let shared = RCTBridgeManager()
private var bridge: RCTBridge?
func getRCTBridge() -> RCTBridge {
if bridge == nil {
bridge = RCTBridge(delegate: RNObjcHacker(), launchOptions: nil)!
}
return bridge!
}
}
// SceneDelegate.swift - Presents login page
func presentLoginPageIfNeeded() {
let loginVC = RNBaseViewController(
moduleName: "LoginModal",
initialProperties: ["fromRoot": "1", "fullScreen": "1"]
)
loginVC.modalPresentationStyle = .fullScreen
tabBarController.present(loginVC, animated: false)
}
func hideLoginViewController() {
loginVC.dismiss(animated: true) {
self.loginViewController = nil
}
}
React Native Side
LoginModal.tsx
export const LoginModal = () => {
return (
<SafeAreaProvider>
<PrivyProvider appId={APP_ID} clientId={CLIENT_ID}>
<LoginView />
</PrivyProvider>
</SafeAreaProvider>
);
};
const LoginView = () => {
const { isReady, logout } = usePrivy();
const { login: loginOAuth } = useLoginWithOAuth({
onSuccess(user) {
console.log('OAuth success');
// proceed with login flow
},
onError(error) {
console.log('OAuth error:', error);
},
});
const onGoogleLogin = async () => {
try {
await logout(); // Clean up before login
loginOAuth({ provider: 'google' }); // This hangs on second attempt
} catch (e) {
loginOAuth({ provider: 'google' });
} finally {
setShowLoading(false);
}
};
// ...
};
usePrivyWallet.ts (custom hook)
const usePrivyWallet = (opts: PrivyWalletHookOptions) => {
const [privyIsReady, setPrivyIsReady] = useState(false);
const { isReady, getAccessToken } = usePrivy();
const wallet = useEmbeddedWallet();
useEffect(() => {
if (privyIsReady) {
return;
}
if (isReady) {
setPrivyIsReady(true); // ❌ This never executes on second mount
}
}, [isReady, privyIsReady]);
// ...
return { creatWallet, privyIsReady };
};
Workarounds Attempted
| Approach |
Description |
Result |
| Keep PrivyProvider mounted |
Hide login view instead of unmounting |
❌ loginOAuth() doesn't trigger OAuth popup |
Call logout() before loginOAuth() |
Clean up state before new login |
❌ logout() hangs when isReady=false |
| Destroy RCTBridge |
Destroy and recreate React Native bridge |
❌ isReady still doesn't become true |
| Delay bridge destruction |
Destroy bridge when showing login page instead of when hiding |
❌ Same issue |
Call logout() after successful login |
Clean up Privy state before unmounting |
❌ Same issue |
Skip logout() call entirely |
Don't call logout at all |
❌ Same issue |
Remove logout() from component mount |
Avoid calling logout when isReady=false |
❌ Same issue |
| Reuse the same RCTBridge |
Don't destroy the bridge between login attempts |
❌ Same issue |
Analysis
The issue appears to be related to persistent state that Privy SDK stores, possibly in:
- iOS Keychain - Login credentials or session tokens
- Native module static variables - State that survives JavaScript context destruction
- Internal SDK state - Something that doesn't properly reset on re-initialization
The fact that:
- It works on simulator but not physical device
- Killing the app resolves it
isReady never becomes true on second mount
- Destroying and recreating the RCTBridge doesn't help
...suggests there's some native-level persistent state (likely Keychain or static variables in native modules) that isn't being properly cleaned up or is causing the SDK initialization to fail silently on physical devices.
Questions
- Is there a known issue with PrivyProvider re-initialization after logout on iOS physical devices?
- Is PrivyProvider designed to be mounted/unmounted multiple times, or should it stay mounted for the app's lifetime?
- For apps that use React Native as an embedded single-page view (not a full RN app), what's the recommended way to handle login/logout cycles?
- Does Privy SDK store any state in iOS Keychain that might interfere with re-initialization?
- Is there a way to force a complete reset of the SDK state before re-mounting PrivyProvider?
- Are there any specific cleanup steps needed when the RCTRootView/RCTBridge is destroyed and recreated?
Environment
@privy-io/expolatest@privy-io/expo-native-extensionslatestReact Native Usage Context
Important: Our app is primarily a native iOS app that uses React Native only for the login page. The architecture is:
LoginModal) embedded viaRCTRootViewRCTRootViewis created withPrivyProviderThis means
PrivyProvideris mounted/unmounted each time the user needs to login, rather than staying mounted for the app's lifetime.Description
After a successful OAuth login (Google/Apple), logging out, and then attempting to log in again, the Privy SDK fails to initialize properly. The
isReadystate fromusePrivy()hook remainsfalseindefinitely, preventing any login operations.Key observation: This issue only occurs on physical iOS devices, not on iOS Simulator. Killing and restarting the app resolves the issue temporarily.
Steps to Reproduce
logout()fromusePrivy()Expected Behavior
After PrivyProvider remounts (in a new RCTRootView),
isReadyshould becometrue, allowing the user to login again.Actual Behavior
isReadytransitions fromfalse→true✅isReadystaysfalseforever ❌Debug Logs
First login (works):
Second login attempt (fails):
Code Context
Native iOS Side (Swift)
React Native Side
LoginModal.tsx
usePrivyWallet.ts (custom hook)
Workarounds Attempted
loginOAuth()doesn't trigger OAuth popuplogout()beforeloginOAuth()logout()hangs whenisReady=falseisReadystill doesn't becometruelogout()after successful loginlogout()call entirelylogout()from component mountAnalysis
The issue appears to be related to persistent state that Privy SDK stores, possibly in:
The fact that:
isReadynever becomestrueon second mount...suggests there's some native-level persistent state (likely Keychain or static variables in native modules) that isn't being properly cleaned up or is causing the SDK initialization to fail silently on physical devices.
Questions