SDK

React Native SDK

@authon/react-nativeHooks, social buttons, secure token storage, and low-level OAuth helpers for React Native and Expo.

Install

Use `expo-secure-store` for token persistence. `expo-web-browser` is required for the recommended OAuth flow.

bash
npm install @authon/react-native react-native-svg
npx expo install expo-secure-store expo-web-browser

Provider Setup

App.tsx
import { AuthonProvider } from "@authon/react-native";
import * as SecureStore from "expo-secure-store";

const storage = {
  getItem: (key: string) => SecureStore.getItemAsync(key),
  setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
  removeItem: (key: string) => SecureStore.deleteItemAsync(key),
};

export default function App() {
  return (
    <AuthonProvider publishableKey="pk_live_..." storage={storage}>
      <Navigation />
    </AuthonProvider>
  );
}

Email / Password

screens/LoginScreen.tsx
import { useState } from "react";
import { View, TextInput, Button, Text, ActivityIndicator } from "react-native";
import { useAuthon, useUser } from "@authon/react-native";

export function LoginScreen() {
  const { isLoaded } = useUser();
  const { signIn, signOut, user, isSignedIn } = useAuthon();
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const handleSignIn = async () => {
    setLoading(true);
    setError(null);
    try {
      await signIn({ strategy: "email_password", email, password });
    } catch (err: any) {
      setError(err.message ?? "Sign-in failed");
    } finally {
      setLoading(false);
    }
  };

  if (!isLoaded) return <ActivityIndicator />;

  if (isSignedIn) {
    return (
      <View style={{ padding: 24, gap: 12 }}>
        <Text>Welcome, {user?.displayName ?? user?.email}</Text>
        <Button title="Sign out" onPress={signOut} />
      </View>
    );
  }

  return (
    <View style={{ padding: 24, gap: 12 }}>
      <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
      <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
      {error ? <Text style={{ color: "red" }}>{error}</Text> : null}
      <Button title={loading ? "Signing in..." : "Sign in"} onPress={handleSignIn} disabled={loading} />
    </View>
  );
}

Social Buttons

tsx
import { SocialButtons } from "@authon/react-native";

export function SocialLoginSection() {
  return (
    <SocialButtons
      onSuccess={() => console.log("Signed in")}
      onError={(error) => console.error(error)}
    />
  );
}

Recommended Expo OAuth Flow

The recommended flow opens an `expo-web-browser` session with `flow=redirect` and uses an HTTPS bridge page for the return. The entire chain should be HTTP 3xx redirects — avoid relying on JS-based redirects in the bridge page, as Android Custom Tabs may not execute them reliably.

GoogleButton.tsx
import * as WebBrowser from "expo-web-browser";
import { Alert, Button } from "react-native";
import { useAuthon } from "@authon/react-native";

const API_URL = "https:0
const PUBLISHABLE_KEY = "pk_live_...";
const APP_DEEP_LINK = "myapp://oauth-callback";

// HTTPS bridge page — Authon redirects here via HTTP 3xx,
// then this page redirects to your app deep link.
const RETURN_TO = "https:4

async function requestOAuthUrl(provider: string) {
  const params = new URLSearchParams({
    redirectUri: 11,
    flow: "redirect",
    returnTo: RETURN_TO,
  });

  const response = await fetch(
    12,
    { headers: { "x-api-key": PUBLISHABLE_KEY } },
  );

  if (!response.ok) {
    throw new Error(await response.text());
  }

  return response.json() as Promise<{ url: string; state: string }>;
}

export function GoogleButton() {
  const { completeOAuth, getToken, signOut } = useAuthon();

  const handlePress = async () => {
    try {
      const { url, state } = await requestOAuthUrl("google");

      5
      const pollPromise = completeOAuth(state);

      6
      await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);

      7
      await pollPromise;

      const authonAccessToken = getToken();
      8
    } catch (err: any) {
      9
      10
      Alert.alert("Login failed", err.message ?? "OAuth error");
    }
  };

  return <Button title="Continue with Google" onPress={handlePress} />;
}

HTTPS Bridge Page

The bridge page receives `authon_oauth_state` from the Authon redirect and forwards the user to your app's deep link. Prefer server-side HTTP 302 redirects over JS `window.location.replace` — this is the single most important factor for reliable browser auto-dismiss on both iOS and Android.

mobile-callback
<!-- Recommended: HTTP redirect (server-side) -->
<!-- Your server at /authon/mobile-callback should do: -->
<!-- 302 redirect to: myapp://oauth-callback?state=xxx -->
<!-- This is more reliable than JS-based redirect on Android Custom Tabs. -->

<!-- If you cannot do server-side redirect, use this HTML fallback: -->
<!doctype html>
<html>
  <body>
    <script>
      const params = new URLSearchParams(window.location.search);
      const state = params.get("authon_oauth_state");
      const error = params.get("authon_oauth_error");

      const target = new URL("myapp://oauth-callback");
      if (state) target.searchParams.set("state", state);
      if (error) target.searchParams.set("error", error);

      // Fallback: JS redirect. Less reliable on Android Custom Tabs.
      window.location.replace(target.toString());
    </script>
  </body>
</html>

Sign Out (Important)

When signing out, always call `signOut()` which clears both local tokens AND the Authon server session. This is critical when users switch between multiple OAuth providers (e.g. Google → Kakao). Failing to clear the Authon session causes stale session interference on the next login attempt.

LogoutButton.tsx
import { useAuthon } from "@authon/react-native";

function LogoutButton() {
  const { signOut } = useAuthon();

  const handleLogout = async () => {
    // signOut() calls DELETE /v1/auth/signout on Authon,
    // clearing the Authon session + local tokens.
    // This is critical when switching between providers
    // (e.g. Google → Kakao) to avoid stale session conflicts.
    await signOut();

    // If your app has its own backend session, clear it too:
    // await myBackend.post("/api/logout");
  };

  return <Button title="Sign out" onPress={handleLogout} />;
}

Account Deletion

Authon provides `DELETE /v1/auth/me` for users to delete their own account. The SDK exposes this as `deleteAccount()`. This is required for Apple App Store and Google Play compliance.

DeleteAccountButton.tsx
import { useAuthon } from "@authon/react-native";
import { Alert } from "react-native";

function DeleteAccountButton() {
  const { deleteAccount, signOut } = useAuthon();

  const handleDelete = () => {
    Alert.alert(
      "Delete Account",
      "This will permanently delete your account and all data. This cannot be undone.",
      [
        { text: "Cancel", style: "cancel" },
        {
          text: "Delete",
          style: "destructive",
          onPress: async () => {
            try {
              await deleteAccount();
              // User is now signed out and account is deleted.
            } catch (err: any) {
              Alert.alert("Error", err.message ?? "Failed to delete account");
            }
          },
        },
      ],
    );
  };

  return <Button title="Delete Account" onPress={handleDelete} color="red" />;
}
Important Notes
  • Provider redirect URIs must always be `{apiUrl}/v1/auth/oauth/redirect`. Never point providers directly at `myapp://...` custom schemes.
  • `returnTo` must be an HTTPS URL whose origin is in `ALLOWED_REDIRECT_ORIGINS`. This is your bridge page that forwards to the app deep link.
  • Use HTTP 3xx server redirect in your bridge page, not JS redirect. Android Custom Tabs may not reliably execute JS on intermediate pages, causing the browser to stay open.
  • The redirect chain should be: Provider → api.authon.dev → your HTTPS bridge (302) → myapp://oauth-callback. All transitions should be HTTP redirects, not JS navigation.
  • `completeOAuth()` rejects immediately when the poll returns `status=error`. Always wrap it in try/catch — do not let errors silently spin the loading state forever.
  • Always call `signOut()` on logout — clearing only your app's local state without revoking the Authon session causes problems when switching providers.
  • For apps with their own backend session, exchange `getToken()` with your backend immediately after `completeOAuth()` resolves.
  • The bridge/callback route should be locale-independent (e.g. `/authon/mobile-callback`, not `/en/authon/mobile-callback`) to avoid locale routing issues.
Authon — Universal Authentication Platform