React Native SDK
@authon/react-native — React Native와 Expo에서 쓸 수 있는 훅, 소셜 버튼, 보안 토큰 저장소, 저수준 OAuth 헬퍼를 제공합니다.
설치
토큰 영속화에는 `expo-secure-store`를 사용하세요. 권장 OAuth 플로우에는 `expo-web-browser`가 필수입니다.
npm install @authon/react-native react-native-svg
npx expo install expo-secure-store expo-web-browserProvider 설정
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>
);
}이메일 / 비밀번호
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>
);
}소셜 버튼
import { SocialButtons } from "@authon/react-native";
export function SocialLoginSection() {
return (
<SocialButtons
onSuccess={() => console.log("Signed in")}
onError={(error) => console.error(error)}
/>
);
}권장 Expo OAuth 플로우
권장 방식은 `flow=redirect`로 `expo-web-browser` 세션을 열고 HTTPS 브리지 페이지를 거쳐 앱으로 복귀하는 것입니다. 전체 체인이 HTTP 3xx 리다이렉트로 구성되어야 합니다. 브리지 페이지에서 JS 기반 리다이렉트에 의존하면 Android Custom Tab에서 브라우저가 안 닫히는 문제가 발생합니다.
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 브리지 페이지
브리지 페이지는 Authon 리다이렉트에서 `authon_oauth_state`를 받아 앱의 딥 링크로 전달합니다. JS `window.location.replace` 대신 서버 사이드 HTTP 302 리다이렉트를 사용하세요. iOS/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>로그아웃 (중요)
로그아웃 시 반드시 `signOut()`을 호출하세요. 로컬 토큰과 Authon 서버 세션을 모두 정리합니다. 여러 OAuth 프로바이더를 전환하며 사용하는 경우(예: 구글 → 카카오) Authon 세션을 정리하지 않으면 다음 로그인에서 stale session 간섭이 발생합니다.
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} />;
}계정 삭제
Authon은 `DELETE /v1/auth/me`로 유저가 직접 계정을 삭제할 수 있습니다. SDK에서는 `deleteAccount()`로 사용할 수 있으며, Apple App Store와 Google Play 정책 준수에 필요합니다.
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" />;
}- OAuth 제공자에는 `myapp://...`를 직접 등록하지 마세요. 제공자 redirect URI는 항상 `{apiUrl}/v1/auth/oauth/redirect`여야 합니다.
- `returnTo`에 앱 콜백 브리지 URL을 넣습니다. 이 값은 HTTPS URL이어야 하고, 해당 origin은 `ALLOWED_REDIRECT_ORIGINS`에 포함돼야 합니다.
- 브리지 페이지에서는 JS 리다이렉트가 아닌 HTTP 3xx 서버 리다이렉트를 사용하세요. Android Custom Tab은 중간 페이지의 JS를 안정적으로 실행하지 못해 브라우저가 닫히지 않을 수 있습니다.
- 리다이렉트 체인: Provider → api.authon.dev → HTTPS 브리지(302) → myapp://oauth-callback. 모든 전환이 HTTP 리다이렉트여야 합니다.
- `completeOAuth()`는 poll 응답이 `status=error`이면 즉시 reject합니다. 반드시 try/catch로 감싸세요. 에러를 무시하면 로딩 상태가 무한 지속됩니다.
- 로그아웃 시 반드시 `signOut()`을 호출하세요. 앱 로컬 상태만 지우고 Authon 세션을 남겨두면 프로바이더 전환 시 문제가 생깁니다.
- 앱이 별도의 백엔드 세션을 가진 구조라면 `completeOAuth()` 직후 `getToken()`을 백엔드에 전달해 자체 세션을 발급받으세요.
- 브리지/콜백 경로는 locale 독립적이어야 합니다(예: `/authon/mobile-callback`, `/en/authon/mobile-callback` 아님). locale 라우팅 문제를 방지합니다.