React Native SDK
@authon/react-native — Hooks, 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.
npm install @authon/react-native react-native-svg
npx expo install expo-secure-store expo-web-browserProvider Setup
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
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
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.
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.
<!-- 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.
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.
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" />;
}- 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.