Skip to content

[Critical Error] Users being randomly logged out in production #14534

@ChristopherGabba

Description

@ChristopherGabba

Before opening, please confirm:

JavaScript Framework

React Native

Amplify APIs

Authentication

Amplify Version

v6

Amplify Categories

auth

Backend

Amplify Gen 2

Environment information

 System:
    OS: macOS 15.6.1
    CPU: (10) arm64 Apple M2 Pro
    Memory: 500.70 MB / 16.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 23.4.0 - ~/.nvm/versions/node/v23.4.0/bin/node
    Yarn: 1.22.22 - /opt/homebrew/bin/yarn
    npm: 11.5.2 - ~/.nvm/versions/node/v23.4.0/bin/npm
    bun: 1.2.19 - /opt/homebrew/bin/bun
    Watchman: 2025.04.14.00 - /opt/homebrew/bin/watchman
  Browsers:
    Chrome: 139.0.7258.155
    Safari: 18.6
  npmPackages:
    %name%:  0.1.0 
    @aws-amplify/backend: ^1.11.0 => 1.16.1 
    @aws-amplify/backend-cli: ^1.8.0 => 1.8.0 
    @aws-amplify/react-native: ^1.1.10 => 1.1.10 
    @aws-amplify/rtn-web-browser: ^1.1.4 => 1.1.4 
    @aws-appsync/utils: 2.0.3 => 2.0.3 
    @aws-sdk/client-cognito-identity-provider: 3.799.0 => 3.799.0 
    @aws-sdk/client-dynamodb: 3.799.0 => 3.799.0 
    @aws-sdk/client-sso-oidc: 3.799.0 => 3.799.0 (3.622.0, 3.637.0, 3.624.0, 3.621.0)
    @aws-sdk/client-sts: 3.799.0 => 3.799.0 (3.622.0, 3.879.0, 3.624.0, 3.621.0)
    @aws-sdk/types: 3.775.0 => 3.775.0 (3.609.0, 3.387.0, 3.862.0, 3.398.0, 3.821.0, 3.734.0)
    @aws-sdk/util-dynamodb: 3.799.0 => 3.799.0 
    @babel/core: ^7.20.0 => 7.28.3 
    @babel/plugin-proposal-export-namespace-from: ^7.18.9 => 7.18.9 
    @babel/plugin-proposal-optional-chaining: ^7.0.0 => 7.21.0 
    @babel/plugin-transform-arrow-functions: ^7.0.0 => 7.27.1 
    @babel/plugin-transform-nullish-coalescing-operator: ^7.0.0 => 7.27.1 
    @babel/plugin-transform-shorthand-properties: ^7.0.0 => 7.27.1 
    @babel/plugin-transform-template-literals: ^7.0.0 => 7.27.1 
    @babel/preset-env: ^7.20.0 => 7.28.3 
    @babel/runtime: ^7.20.0 => 7.28.3 
    @config-plugins/ffmpeg-kit-react-native: 9.0.0 => 9.0.0 
    @expo-google-fonts/m-plus-1p: 0.2.3 => 0.2.3 
    @expo-google-fonts/montserrat: 0.2.3 => 0.2.3 
    @expo/config-plugins: ~11.0.5 => 11.0.5 (10.1.2)
    @expo/metro-runtime: ~6.1.0 => 6.1.1 
    @gorhom/bottom-sheet: 5.1.6 => 5.1.6 
    @legendapp/list: ^2.0.0-beta.4 => 2.0.0-next.25 
    @react-native-async-storage/async-storage: 2.2.0 => 2.2.0 
    @react-native-community/netinfo: 11.4.1 => 11.4.1 
    @react-native-menu/menu: ^1.2.4 => 1.2.4 
    @react-navigation/bottom-tabs: 7.4.6 => 7.4.6 
    @react-navigation/native: 7.1.17 => 7.1.17 
    @react-navigation/native-stack: 7.3.25 => 7.3.25 
    @sentry/react-native: ~6.20.0 => 6.20.0 
    @tanstack/query-codemods:  undefined ()
    @tanstack/react-query: ^5.85.5 => 5.85.5 
    @tanstack/react-query-persist-client: ^5.85.5 => 5.85.5 
    @types/i18n-js: 3.8.2 => 3.8.2 
    @types/jest: ^29.2.1 => 29.5.14 
    @types/lodash.filter: ^4.6.9 => 4.6.9 
    @types/node: 22.10.5 => 22.10.5 (24.3.0)
    @types/react: ~19.1.10 => 19.1.12 (18.3.24)
    @types/react-test-renderer: ^18.0.0 => 18.3.1 
    @typescript-eslint/eslint-plugin: ^8.33.1 => 8.41.0 
    @typescript-eslint/parser: ^8.33.1 => 8.41.0 
    @typescript-eslint/utils: ^8.31.1 => 8.41.0 
    ContextAPIMixpanel:  0.0.1 
    MixpanelDemo:  0.0.1 
    MixpanelExample:  0.0.1 
    SimpleMixpanel:  0.0.1 
    aws-amplify: ^6.15.5 => 6.15.5 
    aws-amplify/adapter-core:  undefined ()
    aws-amplify/adapter-core/internals:  undefined ()
    aws-amplify/analytics:  undefined ()
    aws-amplify/analytics/kinesis:  undefined ()
    aws-amplify/analytics/kinesis-firehose:  undefined ()
    aws-amplify/analytics/personalize:  undefined ()
    aws-amplify/analytics/pinpoint:  undefined ()
    aws-amplify/api:  undefined ()
    aws-amplify/api/internals:  undefined ()
    aws-amplify/api/server:  undefined ()
    aws-amplify/auth:  undefined ()
    aws-amplify/auth/cognito:  undefined ()
    aws-amplify/auth/cognito/server:  undefined ()
    aws-amplify/auth/enable-oauth-listener:  undefined ()
    aws-amplify/auth/server:  undefined ()
    aws-amplify/data:  undefined ()
    aws-amplify/data/server:  undefined ()
    aws-amplify/datastore:  undefined ()
    aws-amplify/in-app-messaging:  undefined ()
    aws-amplify/in-app-messaging/pinpoint:  undefined ()
    aws-amplify/push-notifications:  undefined ()
    aws-amplify/push-notifications/pinpoint:  undefined ()
    aws-amplify/storage:  undefined ()
    aws-amplify/storage/s3:  undefined ()
    aws-amplify/storage/s3/server:  undefined ()
    aws-amplify/storage/server:  undefined ()
    aws-amplify/utils:  undefined ()
    aws-cdk: 2.1013.0 => 2.1013.0 
    aws-cdk-lib: 2.194.0 => 2.194.0 
    babel-jest: ^29.2.1 => 29.7.0 
    buffer: 6.0.3 => 6.0.3 (4.9.2, 5.6.0, 5.7.1)
    constructs: ^10.3.0 => 10.4.2 
    date-fns: 4.1.0 => 4.1.0 
    esbuild: ^0.21.1 => 0.21.5 (0.25.9)
    eslint: ^9.28.0 => 9.34.0 
    eslint-config-prettier: 9.1.0 => 9.1.0 
    eslint-config-standard: 17.0.0 => 17.0.0 
    eslint-plugin-import: 2.26.0 => 2.26.0 
    eslint-plugin-n: ^15.0.0 => 15.7.0 
    eslint-plugin-promise: 6.6.0 => 6.6.0 
    eslint-plugin-react: ^7.37.5 => 7.37.5 
    eslint-plugin-react-native: 4.0.0 => 4.0.0 
    expo: 54.0.0-preview.11 => 54.0.0-preview.11 
    expo-application: ~7.0.4 => 7.0.4 
    expo-blur: ~15.0.4 => 15.0.4 
    expo-build-properties: ~1.0.5 => 1.0.5 
    expo-clipboard: ~8.0.4 => 8.0.4 
    expo-constants: ~18.0.5 => 18.0.5 
    expo-contacts: ~15.0.5 => 15.0.5 
    expo-dev-client: ~6.0.6 => 6.0.6 
    expo-device: ~8.0.4 => 8.0.4 
    expo-file-system: ~19.0.5 => 19.0.6 
    expo-font: ~14.0.5 => 14.0.5 
    expo-haptics: ~15.0.4 => 15.0.4 
    expo-image: ~3.0.3 => 3.0.4 
    expo-image-picker: ~17.0.5 => 17.0.5 
    expo-keep-awake: ~15.0.4 => 15.0.4 
    expo-linear-gradient: ~15.0.4 => 15.0.4 
    expo-linking: ~8.0.5 => 8.0.5 
    expo-localization: ~17.0.4 => 17.0.4 
    expo-secure-store: ~15.0.4 => 15.0.4 
    expo-share-intent: 4.1.1 => 4.1.1 
    expo-sharing: ~14.0.4 => 14.0.4 
    expo-sms: ~14.0.4 => 14.0.4 
    expo-splash-screen: ~31.0.6 => 31.0.6 
    expo-store-review: ~9.0.4 => 9.0.4 
    expo-video: ~3.0.7 => 3.0.7 
    expo-video-metadata: 1.5.0 => 1.5.0 
    expo-video-thumbnails: ~10.0.4 => 10.0.4 
    ffmpeg-kit-react-native: 6.0.2 => 6.0.2 
    follow-redirects: 1.15.9 => 1.15.9 
    i18next: 25.2.1 => 25.2.1 
    intl-pluralrules: 2.0.1 => 2.0.1 
    jest: ^29.2.1 => 29.7.0 
    jest-expo: ~54.0.5 => 54.0.5 
    libphonenumber-js: 1.11.19 => 1.11.19 (1.9.47)
    libphonenumber-js-core:  undefined (1.0.0)
    libphonenumber-js-max:  undefined (1.0.0)
    libphonenumber-js-min:  undefined (1.0.0)
    libphonenumber-js-mobile:  undefined (1.0.0)
    libphonenumber-js/build:  undefined ()
    libphonenumber-js/core:  undefined ()
    libphonenumber-js/max:  undefined ()
    libphonenumber-js/max/metadata:  undefined ()
    libphonenumber-js/min:  undefined ()
    libphonenumber-js/min/metadata:  undefined ()
    libphonenumber-js/mobile:  undefined ()
    libphonenumber-js/mobile/examples:  undefined ()
    libphonenumber-js/mobile/metadata:  undefined ()
    lodash: 4.17.21 => 4.17.21 
    lottie-react-native: ~7.3.1 => 7.3.3 
    mixpanel-react-native: 3.1.2 => 3.1.2 
    mixpanelexpo:  1.0.0 
    onesignal-expo-plugin: 2.0.3 => 2.0.3 
    patch-package: 6.4.7 => 6.4.7 
    postinstall-prepare: 1.0.1 => 1.0.1 
    prettier: 2.8.8 => 2.8.8 (2.3.2, 3.6.2, 1.19.1)
    react: 19.1.0 => 19.1.0 (19.2.0-canary-5252281c-20250408)
    react-dom: 19.1.0 => 19.1.0 (19.2.0-canary-5252281c-20250408)
    react-i18next: 15.4.1 => 15.4.1 
    react-native: 0.81.1 => 0.81.1 
    react-native-blurhash: ^2.1.1 => 2.1.2 
    react-native-compressor: ^1.12.0 => 1.12.0 
    react-native-edge-to-edge: ^1.6.2 => 1.7.0 
    react-native-gesture-handler: ~2.28.0 => 2.28.0 
    react-native-get-random-values: 1.11.0 => 1.11.0 
    react-native-ios-context-menu: 3.1.3 => 3.1.3 
    react-native-ios-utilities: 5.1.8 => 5.1.8 
    react-native-keyboard-controller: 1.18.5 => 1.18.5 
    react-native-mime-types: 2.5.0 => 2.5.0 
    react-native-mmkv: 2.12.2 => 2.12.2 
    react-native-nitro-audio-manager: 0.1.3 => 0.1.3 
    react-native-nitro-modules: 0.28.1 => 0.28.1 
    react-native-nitro-screen-recorder: ^0.3.5 => 0.3.5 
    react-native-onesignal: 5.2.13 => 5.2.13 
    react-native-reanimated: ~3.19.1 => 3.19.1 
    react-native-safe-area-context: ~5.6.0 => 5.6.1 
    react-native-screens: ~4.15.0 => 4.15.4 
    react-native-static-safe-area-insets: 2.2.0 => 2.2.0 
    react-native-url-polyfill: 2.0.0 => 2.0.0 
    react-native-vision-camera: 4.7.0 => 4.7.0 
    react-native-vision-camera-face-detector: 1.8.3 => 1.8.3 
    react-native-webview: 13.15.0 => 13.15.0 
    react-native-worklets-core: 1.5.0 => 1.5.0 
    react-native-youtube-iframe: 2.3.0 => 2.3.0 
    react-native-z-view: 0.2.4 => 0.2.4 
    react-test-renderer: 18.2.0 => 18.2.0 (19.1.0)
    ts-jest: ^29.1.1 => 29.4.1 
    ts-node: ^10.9.2 => 10.9.2 
    tsx: ^4.9.4 => 4.20.5 (4.19.4)
    typescript: ~5.9.2 => 5.9.2 (4.4.4, 4.9.5)
    uuid: 11.0.5 => 11.0.5 (9.0.1, 11.1.0, 3.3.2, 7.0.3)
    zeego: 3.0.6 => 3.0.6 
    zustand: 5.0.5 => 5.0.5 
  npmGlobalPackages:
    corepack: 0.30.0
    eas-cli: 16.17.4
    license-checker: 25.0.1
    npm: 11.5.2

Describe the bug

Okay so its hard to reproduce, but we keep having users that get randomly logged out of our application after varying different times. They just open the app on iOS (currently using react-native & expo) and they are just logged out. I've spent a ton of time trying to troubleshoot this bug and its absolutely driving us crazy. I hope this is a problem with my client-side code, but I have add all kinds of extra error logging and Sentry logging and it seems to just... skip it? They are just logged out.

Related Issues I have found:

I haven't been able to reproduce this in development builds, most likely because its on a fast-refresh / recompile time and the tokens are always valid, but its happening in production often. I have had dozens of complaints from users open up the app to just be on the walkthrough screen.

If this ends up being something going on in the Amplify caching system or Cognito system, I hope that this takes high priority and gets fixed.

Expected behavior

The user only is logged out:

  • When they delete the app and re-install it
  • When they hit logout

Reproduction steps

I'm using Zustand to manage a Global Authentication State:

export interface AuthStore {
  isVerified: boolean
  isMissingRequiredAttributes: boolean
  isAuthenticated: boolean
  currentUser: User | null
  setCurrentUser: (user: User | null) => void
  clearAuthStore: () => void
}

Simplified Protected Routes with React Navigation:

function App(props: AppProps) {

  const { hasFinishedProcessingAuth } = useAuth()

  const isReadyToShowApp =
    hasFinishedProcessingAuth // && other permissives like cache rehydration

  useEffect(() => {
    if (isReadyToShowApp) {
      setTimeout(SplashScreen.hideAsync, 600)
    }
  }, [isReadyToShowApp])

  if (!isReadyToShowApp) {
    return null
  }

  const linking: LinkingOptions<NonNullable<unknown>> = {
     // ...
  }

  const $rootView: ViewStyle = {
    flex: 1,
  }

  return (
    <SafeAreaProvider
      initialMetrics={initialWindowMetrics}
      style={{ backgroundColor: colors.background }}
    >
        <AppNavigator linking={linking} />
    </SafeAreaProvider>
  )
}

export const AppNavigator = (props: NavigationProps) => {

  const { isAuthenticated, isVerified, isMissingRequiredAttributes } = useAuthenticationStore()

  return (
    <NavigationContainer
      {...props}
    >
      <AppStack
        isAuthenticated={isAuthenticated}
        isVerified={isVerified}
        isMissingRequiredAttributes={isMissingRequiredAttributes}
      />
    </NavigationContainer>
  )
}

// Simplified Auth Stack Layer

const AppStack = (props: AppStackProps) => {
  const { isAuthenticated, isVerified, isMissingRequiredAttributes } = props

  const initialRouteName = useMemo(() => {
    if (isAuthenticated && isVerified) {
      if (isMissingRequiredAttributes) {
        return "RequiredAttributes"
      } else {
        return "Tab"
      }
    } else {
      return "LoginSignUp"
    }
  }, [isAuthenticated, isVerified, isMissingRequiredAttributes])

  return (
    <Stack.Navigator
      screenOptions={{
        headerShown: false,
        navigationBarColor: colors.background,
      }}
      initialRouteName={initialRouteName}
    >
      {isAuthenticated && isVerified ? (
        <>
          {isMissingRequiredAttributes ? (
            <>
              <Stack.Screen
                name="RequiredAttributes"
                component={Screens.RequiredAttributesScreen}
              />
            </>
          ) : (
              <Stack.Screen name="Tab" component={TabNavigator} />
          )}
        </>
      ) : (
          <Stack.Screen
            name="LoginSignUp"
            component={Screens.LoginSignUpScreen}
            options={{ animation: "slide_from_right" }}
          />
      )}
    </Stack.Navigator>
  )
}

Code Snippet

Backend Configuration:

// Backend.ts
import { defineBackend } from "@aws-amplify/backend"
import { auth } from "./auth/resource"
import { data } from "./data/resource"
import { storage } from "./storage/resource"
import { blockDuplicateEmails } from "./functions/blockDuplicateEmails/resource"
import { weeklyPushNotifications } from "../amplify/functions/weeklyPushNotifications/resource"
/**
 * @see https://docs.amplify.aws/react/build-a-backend/ to add storage, functions, and more
 */
const backend = defineBackend({
  auth,
  data,
  storage,
  blockDuplicateEmails,
  weeklyPushNotifications,
})
// Define Phone Table variable for batch query
backend.data.resources.cfnResources.cfnGraphqlApi.environmentVariables = {
  PHONENUMBER_TABLE: "PhoneNumber-btsurcazonc3vcefwbvlflcwbm-NONE",
}

const { cfnUserPoolClient } = backend.auth.resources.cfnResources

cfnUserPoolClient.refreshTokenValidity = 120
//auth.resource.ts
import { defineAuth, secret } from "@aws-amplify/backend"
// import { blockDuplicateEmails } from "../functions/blockDuplicateEmails/resource"
import { linkAccounts } from "../functions/linkAccounts/resource"
/**
 * Define and configure your auth resource
 * @see https://docs.amplify.aws/gen2/build-a-backend/auth
 */
export const auth = defineAuth({
  loginWith: {
    email: {
      verificationEmailSubject: "Verify Your ReactApp Account",
      verificationEmailBody: (createCode: any) => `Your ReactApp verification code is: ${createCode()}`,
    },
    phone: {
      verificationMessage: (createCode) =>
        `Use this code to confirm your ReactApp account: ${createCode()}`,
    },
    externalProviders: {
      google: {
        clientId: secret("GOOGLE_CLIENT_ID"),
        clientSecret: secret("GOOGLE_CLIENT_SECRET"),
        attributeMapping: {
          email: "email",
          emailVerified: "email_verified",
          familyName: "family_name",
          givenName: "given_name",
          phoneNumber: "phone_number",
        },
        scopes: ["email", "openid", "profile", "phone"],
      },
      signInWithApple: {
        clientId: secret("SIWA_CLIENT_ID"),
        keyId: secret("SIWA_KEY_ID"),
        privateKey: secret("SIWA_PRIVATE_KEY"),
        teamId: secret("SIWA_TEAM_ID"),
        attributeMapping: {
          email: "email",
          givenName: "firstName",
          familyName: "lastName",
          emailVerified: "email_verified"
        },
        scopes: ["email", "name"],
      },
      callbackUrls: ["reactapp://"],
      logoutUrls: ["reactapp://"],
    },
  },
  accountRecovery: "EMAIL_ONLY",
  userAttributes: {
    birthdate: {
      mutable: true,
      required: false,
    },
    phoneNumber: {
      mutable: true,
      required: false,
    },
    givenName: {
      mutable: true,
      required: false,
    },
    familyName: {
      mutable: true,
      required: false,
    },
    preferredUsername: {
      mutable: true,
      required: false,
    },
    profilePicture: {
      mutable: true,
      required: false,
    },
  },
  triggers: {
    preSignUp: linkAccounts
  }
})

I have a useAuth hook:

/**
 * This hook checks for changes in the authentication state, as well as checks the user session upon login.
 * @returns isAuthenticated - The user's current authentication state
 * @returns isVerified - The user's current verification state
 */
export function useAuth(): {
  isAuthenticated: boolean
  isVerified: boolean
  isMissingRequiredAttributes: boolean
  hasFinishedProcessingAuth: boolean
} {
  const [authStateChanged, setAuthStateChanged] = useState(false)
  const [hasFinishedProcessingAuth, setHasFinishedProcessingAuth] = useState(false)
  const resetRootStore = useStore((state) => state.resetRootStore)

  const { bustCache } = useCacheBuster()

  const { isAuthenticated, isVerified, isMissingRequiredAttributes, setAuthProp, setCurrentUser } =
    useAuthenticationStore()

  const handleSignInAndApplyAttributes = async () => {
    try {
      const client = generateClient<Schema>()
      const currentUser = await getCurrentUser()

      const { errors, data: userSigningIn } = await client.models.User.get(
        { id: currentUser.userId },
        { selectionSet: userSelectionSet, authMode: "userPool" },
      )

      if (!errors && userSigningIn) {
        // Full user exists in database
        setAuthProp({
          currentUser: userSigningIn,
          isAuthenticated: true,
          isVerified: true,
          isMissingRequiredAttributes: false,
        })
      } else if (!errors && userSigningIn === null) {
        // User exists in Cognito but not in database
        const { data: userAttributes } = await tryCatch(fetchUserAttributes())
        setAuthProp({
          isVerified: userAttributes?.email_verified === "true",
          isAuthenticated: true,
          isMissingRequiredAttributes: true,
        })
      } else {
        // Error occurred or fallback scenario
        const { data: userAttributes, error } = await tryCatch(fetchUserAttributes())

        if (!userAttributes || !userAttributes.sub) {
          reportCrash({ error, method: "UserAttributesBackupCheck" })
          setAuthProp({
            isAuthenticated: false,
            isVerified: false,
            isMissingRequiredAttributes: true,
          })
          return // Early return to avoid setting tracking IDs
        }

        // This scenario is unclear - consider if user should be authenticated
        setAuthProp({
          currentUser: {
            id: userAttributes.sub,
            firstName: capitalizeFirstLetter(userAttributes.given_name) || "",
            lastName: capitalizeFirstLetter(userAttributes.family_name) || "",
            username: userAttributes.preferred_username || "",
            phoneNumber: { phoneNumber: userAttributes.phone_number || "" },
            birthdate: userAttributes.birthdate,
            email: userAttributes.email || "",
            profileImageS3Path: userAttributes.picture,
          },
          isAuthenticated: true, // Changed from false - if we have user data, they're authenticated
          isVerified: userAttributes?.email_verified === "true",
        })
      }

    } catch (error) {
      reportCrash({ error, method: "handleSignInAndApplyAttributes" })
      setAuthProp({
        isAuthenticated: false,
        isVerified: false,
        isMissingRequiredAttributes: true,
      })
    } finally {
      setHasFinishedProcessingAuth(true)
    }
  }

  useEffect(() => {
    void (async function manageAuth() {
      const { data: session, error: authSessionError } = await tryCatch(fetchAuthSession())

      if (authSessionError) {
        reportCrash({
          error: authSessionError,
          method: "useAuth-fetchAuthSession",
          component: "app.tsx",
        })

        switch (authSessionError.name) {
          case "UserUnAuthenticatedException":
          case "UserNotFoundException":
          case "NotAuthorizedException": {
            // Something is wrong and the user does not appear to be authenticated
            // So force a log out and reset
            resetRootStore()
            setAuthProp({
              isAuthenticated: false,
              isVerified: false,
              isMissingRequiredAttributes: true,
            })
            setHasFinishedProcessingAuth(true)
            await bustCache()
            break
          }
          case "UserAlreadyAuthenticatedException": {
            // The user is authenticated already
            // So hard set a login
            await handleSignInAndApplyAttributes()
            break
          }

          case "NetworkError":
            // CANNOT DETERMINE AUTH STATUS SO DO NOTHING
            const result = await NetInfo.fetch()
            const alertString = result.isConnected
              ? translate("errors:maybeConnection")
              : translate("errors:noConnection")

            Alert.alert("Network Error", alertString, [
              {
                text: translate("common:cancel"),
                style: "cancel",
              },
              {
                text: translate("common:retry"),
                onPress: async () => {
                  const result = await NetInfo.refresh()
                  if (result.isConnected || result.isInternetReachable) {
                    await tryCatch(fetchAuthSession({ forceRefresh: true }))
                    setAuthStateChanged((prev) => !prev)
                  }
                },
              },
            ])

            setHasFinishedProcessingAuth(true)
            break
          default: {
            reportCrash({ error: authSessionError, method: "useAuth", component: "app.tsx" })
          }
        }
        return
      }

      save(storageKey.accessTokenExpiration, session.tokens?.accessToken.payload.exp ?? "")

      const userIsNotAuthenticated = !session.tokens

      if (userIsNotAuthenticated) {
        setCurrentUser(null)
        setAuthProp({
          isAuthenticated: false,
          isVerified: false,
          isMissingRequiredAttributes: true,
        })
        setHasFinishedProcessingAuth(true)
        return
      }

      await handleSignInAndApplyAttributes()
    })()
  }, [authStateChanged])

  useEffect(() => {
    const unsubscribe = Hub.listen("auth", async (data) => {
      if (data.payload.event === "tokenRefresh") {
        return
      }
      if (data.payload.event === "tokenRefresh_failure") {
        await handleTokenRefreshFailure()
        return
      }

      setAuthStateChanged((prev) => !prev)
    })

    return () => unsubscribe()
  }, [])

  const handleTokenRefreshFailure = async () => {
    try {
      // Attempt one more refresh
      const { data: session, error } = await tryCatch(fetchAuthSession({ forceRefresh: true }))

      if (error || !session?.tokens) {
        reportCrash({
          error: error ?? new Error("Token refresh failed and session is empty"),
          method: "handleTokenRefreshFailure",
          component: "useAuth",
        })
        // If it still fails, show user-friendly retry option
        Alert.alert(
          translate("errors:somethingWentWrong"), // "Your session has expired"
          translate("errors:poorConnectivity"), // "Please try again to continue using the app"
          [
            {
              text: translate("common:cancel"),
              style: "cancel",
              onPress: async () => {
                // User chose to cancel - log them out gracefully
                await bustCache()
                resetRootStore()
                setAuthProp({
                  isAuthenticated: false,
                  isVerified: false,
                  isMissingRequiredAttributes: true,
                })
              },
            },
            {
              text: translate("common:retry"),
              onPress: async () => {
                // Force a complete re-authentication check
                setAuthStateChanged((prev) => !prev)
              },
            },
          ],
        )
      } else {
        // Silent success - update stored expiration
        if (session.tokens?.accessToken.payload.exp) {
          setAuthStateChanged((prev) => !prev)
        }
      }
    } catch (error) {
      // Complete failure - this is rare but handle gracefully
      reportCrash({ error, method: "handleTokenRefreshFailure", component: "useAuth" })
      await bustCache()
      resetRootStore()
    }
  }

  return { isAuthenticated, isVerified, isMissingRequiredAttributes, hasFinishedProcessingAuth }
}

My reportCrash function should log in Sentry when a user experiences a sign in error, but they just don't appear at all? It's like somehow just silently signing out the user?

I also have a function like so, that I call every time the app is opened that just makes sure that any API calls made are done with a fully refreshed user

export async function refreshAuthSessionOnlyIfExpired(): Promise<void> {
  const tokenExpiration = mmkvStorage.getNumber(storageKey.accessTokenExpiration) || 0
  const now = Math.floor(Date.now() / 1000)
  const FIVE_MINUTES = 60 * 5

  const tokenIsExpiredOrNearingExpiry = tokenExpiration < now + FIVE_MINUTES

  if (tokenIsExpiredOrNearingExpiry) {
    const { data: session, error } = await tryCatch(fetchAuthSession({ forceRefresh: true }))

    if (error as AuthError) {
      throw new Error(`AUTH_REFRESH_FAILURE: ${error?.message}`)
    }

    if (session?.tokens?.accessToken.payload.exp) {
      save(storageKey.accessTokenExpiration, session.tokens.accessToken.payload.exp)
    } else {
      throw new Error(`AUTH_REFRESH_FAILURE: No token expiration found in session.`)
    }
  } else {
    const secondsRemaining = tokenExpiration - now
    const minutesRemaining = Math.floor(secondsRemaining / 60)
    console.log(
      `Token is still valid, no need to refresh — ${minutesRemaining} minute(s) remaining.`,
    )
  }
}

Log output

None

aws-exports.js

See below.

Manual configuration

{
  "auth": {
    "user_pool_id": "*****",
    "aws_region": "us-east-1",
    "user_pool_client_id": "*****",
    "identity_pool_id": "*****",
    "mfa_methods": [],
    "standard_required_attributes": [
      "email"
    ],
    "username_attributes": [
      "email",
      "phone_number"
    ],
    "user_verification_types": [
      "email",
      "phone_number"
    ],
    "groups": [],
    "mfa_configuration": "NONE",
    "password_policy": {
      "min_length": 8,
      "require_lowercase": true,
      "require_numbers": true,
      "require_symbols": true,
      "require_uppercase": true
    },
    "oauth": {
      "identity_providers": [
        "GOOGLE",
        "SIGN_IN_WITH_APPLE"
      ],
      "redirect_sign_in_uri": [
        "reactapp://"
      ],
      "redirect_sign_out_uri": [
        "reactapp://"
      ],
      "response_type": "code",
      "scopes": [
        "phone",
        "email",
        "openid",
        "profile",
        "aws.cognito.signin.user.admin"
      ],
      "domain": "d39edb9c00f802d4f76e.auth.us-east-1.amazoncognito.com"
    },
    "unauthenticated_identities_enabled": true
  },
}

Additional configuration

No response

Mobile Device

iPhones 12 - 16

Mobile Operating System

iOS 18

Mobile Browser

N/A

Mobile Browser Version

N/A

Additional information and screenshots

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    AuthRelated to Auth components/categoryquestionGeneral question

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions