-
Notifications
You must be signed in to change notification settings - Fork 16
Description
Environment
- Platform: iOS physical device (simulator works fine)
- Package:
@privy-io/expolatest - Package:
@privy-io/expo-native-extensionslatest - 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 viaRCTRootView - The RN view is presented modally when login is needed and dismissed after successful login
- Each time the login page is shown, a new
RCTRootViewis created withPrivyProvider
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()fromusePrivy() - 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:
isReadytransitions fromfalse→true✅ - Second mount (after logout):
isReadystaysfalseforever ❌
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
isReadynever becomestrueon 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?