Skip to content

[Bug] isReady stays false after logout and re-mount of PrivyProvider on iOS physical devices #133

@czMyshell

Description

@czMyshell

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

  1. Fresh app launch on iOS physical device
  2. User clicks "Login with Google" → OAuth flow completes successfully ✅
  3. User logs out via logout() from usePrivy()
  4. Login modal is dismissed (PrivyProvider unmounts, RCTRootView removed)
  5. User triggers login again → New login modal appears (new RCTRootView, new PrivyProvider mounts)
  6. 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 falsetrue
  • 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:

  1. iOS Keychain - Login credentials or session tokens
  2. Native module static variables - State that survives JavaScript context destruction
  3. 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

  1. Is there a known issue with PrivyProvider re-initialization after logout on iOS physical devices?
  2. Is PrivyProvider designed to be mounted/unmounted multiple times, or should it stay mounted for the app's lifetime?
  3. 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?
  4. Does Privy SDK store any state in iOS Keychain that might interfere with re-initialization?
  5. Is there a way to force a complete reset of the SDK state before re-mounting PrivyProvider?
  6. Are there any specific cleanup steps needed when the RCTRootView/RCTBridge is destroyed and recreated?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions