@authon/react-native 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ko.md ADDED
@@ -0,0 +1,252 @@
1
+ [English](./README.md) | **한국어**
2
+
3
+ # @authon/react-native
4
+
5
+ [Authon](https://authon.dev)용 React Native SDK입니다. 아래 기능을 제공합니다.
6
+
7
+ - 앱 전역 인증 상태를 위한 `AuthonProvider`
8
+ - `useAuthon()`, `useUser()` 훅
9
+ - storage adapter 기반 보안 토큰 저장
10
+ - `SocialButton`, `SocialButtons`
11
+ - 저수준 OAuth 헬퍼(`startOAuth`, `completeOAuth`, `client`)
12
+
13
+ ## 설치
14
+
15
+ ```bash
16
+ npm install @authon/react-native react-native-svg
17
+ npx expo install expo-secure-store expo-web-browser
18
+ ```
19
+
20
+ Expo 앱에서는 `expo-secure-store`를 권장합니다.
21
+ `expo-web-browser`는 필수는 아니지만, Android에서 더 안정적인 OAuth UX가 필요하면 함께 설치하는 편이 좋습니다.
22
+
23
+ ## 설정
24
+
25
+ ```tsx
26
+ import { AuthonProvider } from '@authon/react-native';
27
+ import * as SecureStore from 'expo-secure-store';
28
+
29
+ const storage = {
30
+ getItem: (key: string) => SecureStore.getItemAsync(key),
31
+ setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
32
+ removeItem: (key: string) => SecureStore.deleteItemAsync(key),
33
+ };
34
+
35
+ export default function App() {
36
+ return (
37
+ <AuthonProvider publishableKey="pk_live_..." storage={storage}>
38
+ <Navigation />
39
+ </AuthonProvider>
40
+ );
41
+ }
42
+ ```
43
+
44
+ storage adapter를 넣지 않으면 토큰은 메모리에만 남고 앱 재시작 시 사라집니다.
45
+
46
+ ## 훅
47
+
48
+ ### `useAuthon()`
49
+
50
+ ```ts
51
+ const {
52
+ isLoaded,
53
+ isSignedIn,
54
+ userId,
55
+ accessToken,
56
+ user,
57
+ signIn,
58
+ signUp,
59
+ signOut,
60
+ getToken,
61
+ providers,
62
+ branding,
63
+ startOAuth,
64
+ completeOAuth,
65
+ on,
66
+ client,
67
+ } = useAuthon();
68
+ ```
69
+
70
+ ### `useUser()`
71
+
72
+ ```ts
73
+ const { isLoaded, isSignedIn, user } = useUser();
74
+ ```
75
+
76
+ ## 이메일 / 비밀번호 예제
77
+
78
+ ```tsx
79
+ import { useState } from 'react';
80
+ import { View, TextInput, Button, Text, ActivityIndicator } from 'react-native';
81
+ import { useAuthon, useUser } from '@authon/react-native';
82
+
83
+ export function LoginScreen() {
84
+ const { isLoaded } = useUser();
85
+ const { signIn, signOut, user, isSignedIn } = useAuthon();
86
+ const [email, setEmail] = useState('');
87
+ const [password, setPassword] = useState('');
88
+ const [loading, setLoading] = useState(false);
89
+ const [error, setError] = useState<string | null>(null);
90
+
91
+ const handleSignIn = async () => {
92
+ setLoading(true);
93
+ setError(null);
94
+ try {
95
+ await signIn({ strategy: 'email_password', email, password });
96
+ } catch (err: any) {
97
+ setError(err.message ?? '로그인에 실패했습니다.');
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ };
102
+
103
+ if (!isLoaded) return <ActivityIndicator />;
104
+
105
+ if (isSignedIn) {
106
+ return (
107
+ <View style={{ padding: 24, gap: 12 }}>
108
+ <Text>Welcome, {user?.displayName ?? user?.email}</Text>
109
+ <Button title="로그아웃" onPress={signOut} />
110
+ </View>
111
+ );
112
+ }
113
+
114
+ return (
115
+ <View style={{ padding: 24, gap: 12 }}>
116
+ <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
117
+ <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
118
+ {error ? <Text style={{ color: 'red' }}>{error}</Text> : null}
119
+ <Button title={loading ? '로그인 중...' : '로그인'} onPress={handleSignIn} disabled={loading} />
120
+ </View>
121
+ );
122
+ }
123
+ ```
124
+
125
+ ## 소셜 버튼
126
+
127
+ ```tsx
128
+ import { SocialButtons } from '@authon/react-native';
129
+
130
+ export function SocialLoginSection() {
131
+ return (
132
+ <SocialButtons
133
+ onSuccess={() => console.log('Signed in')}
134
+ onError={(error) => console.error(error)}
135
+ />
136
+ );
137
+ }
138
+ ```
139
+
140
+ 완전히 커스텀한 버튼이 필요하면 `SocialButton`을 직접 쓰거나 `startOAuth()` / `completeOAuth()`를 수동으로 호출하면 됩니다.
141
+
142
+ ## 수동 OAuth 플로우
143
+
144
+ 가장 단순한 SDK 플로우는 아래와 같습니다.
145
+
146
+ ```tsx
147
+ import { Linking } from 'react-native';
148
+ import { useAuthon } from '@authon/react-native';
149
+
150
+ export function GoogleButton() {
151
+ const { startOAuth, completeOAuth } = useAuthon();
152
+
153
+ const handlePress = async () => {
154
+ const { url, state } = await startOAuth('google');
155
+ await Linking.openURL(url);
156
+ await completeOAuth(state);
157
+ };
158
+
159
+ // ...
160
+ }
161
+ ```
162
+
163
+ 이 방식은 가장 단순하지만, 브라우저 주도형이고 완료 감지는 polling 기반입니다.
164
+
165
+ ## 권장 Expo OAuth 플로우
166
+
167
+ Android에서 더 자연스러운 브라우저 종료 UX가 필요하면 `flow=redirect`와 `returnTo`를 포함해 OAuth URL을 직접 요청하고 `expo-web-browser`로 여는 방식을 권장합니다.
168
+
169
+ ```tsx
170
+ import * as WebBrowser from 'expo-web-browser';
171
+ import { Button } from 'react-native';
172
+ import { useAuthon } from '@authon/react-native';
173
+
174
+ const API_URL = 'https://api.authon.dev';
175
+ const PUBLISHABLE_KEY = 'pk_live_...';
176
+ const APP_DEEP_LINK = 'myapp://oauth-callback';
177
+ const RETURN_TO = 'https://auth.example.com/authon/mobile-callback';
178
+
179
+ async function requestOAuthUrl(provider: 'google') {
180
+ const params = new URLSearchParams({
181
+ redirectUri: `${API_URL}/v1/auth/oauth/redirect`,
182
+ flow: 'redirect',
183
+ returnTo: RETURN_TO,
184
+ });
185
+
186
+ const response = await fetch(
187
+ `${API_URL}/v1/auth/oauth/${provider}/url?${params.toString()}`,
188
+ { headers: { 'x-api-key': PUBLISHABLE_KEY } },
189
+ );
190
+
191
+ if (!response.ok) {
192
+ throw new Error(await response.text());
193
+ }
194
+
195
+ return response.json() as Promise<{ url: string; state: string }>;
196
+ }
197
+
198
+ export function GoogleButton() {
199
+ const { completeOAuth, getToken } = useAuthon();
200
+
201
+ const handlePress = async () => {
202
+ const { url, state } = await requestOAuthUrl('google');
203
+ const pollPromise = completeOAuth(state);
204
+
205
+ await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);
206
+ await pollPromise;
207
+
208
+ const authonAccessToken = getToken();
209
+ // 앱이 자체 백엔드 세션도 가진다면 여기서 authonAccessToken을 교환하세요.
210
+ };
211
+
212
+ return <Button title="Google로 계속하기" onPress={handlePress} />;
213
+ }
214
+ ```
215
+
216
+ HTTPS 브리지 페이지 예제:
217
+
218
+ ```html
219
+ <!doctype html>
220
+ <html>
221
+ <body>
222
+ <script>
223
+ const params = new URLSearchParams(window.location.search);
224
+ const state = params.get('authon_oauth_state');
225
+ const error = params.get('authon_oauth_error');
226
+
227
+ const target = new URL('myapp://oauth-callback');
228
+ if (state) target.searchParams.set('state', state);
229
+ if (error) target.searchParams.set('error', error);
230
+
231
+ window.location.replace(target.toString());
232
+ </script>
233
+ </body>
234
+ </html>
235
+ ```
236
+
237
+ ## 중요한 주의사항
238
+
239
+ - `myapp://...`를 OAuth 제공자 redirect URI로 직접 등록하지 마세요. 제공자 redirect URI는 항상 `{apiUrl}/v1/auth/oauth/redirect`여야 합니다.
240
+ - 앱 복귀용 브리지는 `returnTo`에 넣습니다. `returnTo`는 HTTPS URL이어야 하며, 해당 origin은 `ALLOWED_REDIRECT_ORIGINS`에 포함돼야 합니다.
241
+ - 커스텀 앱 스킴을 쓸 경우 HTTPS 브리지 페이지에서 `myapp://...`로 한 번 더 넘기세요.
242
+ - Android에서는 이미 열린 Custom Tab을 `dismissAuthSession()`으로 확실하게 닫을 수 없습니다. `openAuthSessionAsync()`와 브리지 체인을 기준으로 설계하세요.
243
+ - 앱이 자체 백엔드 세션도 운영한다면 `completeOAuth()` 직후 `getToken()`을 백엔드에 넘겨 자체 세션을 발급받으세요.
244
+
245
+ ## 문서
246
+
247
+ - [Authon docs](https://authon.dev/docs)
248
+ - [Authon OAuth flow](https://authon.dev/docs)
249
+
250
+ ## 라이선스
251
+
252
+ [MIT](../../LICENSE)
package/README.md CHANGED
@@ -1,111 +1,251 @@
1
+ **English** | [한국어](./README.ko.md)
2
+
1
3
  # @authon/react-native
2
4
 
3
- React Native SDK for [Authon](https://authon.dev) native OAuth, secure token storage, and React hooks.
5
+ React Native SDK for [Authon](https://authon.dev). It provides:
6
+
7
+ - `AuthonProvider` for app-wide auth state
8
+ - `useAuthon()` and `useUser()` hooks
9
+ - secure token persistence through a storage adapter
10
+ - `SocialButton` / `SocialButtons`
11
+ - low-level OAuth helpers (`startOAuth`, `completeOAuth`, `client`)
4
12
 
5
13
  ## Install
6
14
 
7
15
  ```bash
8
- npm install @authon/react-native
9
- # or
10
- pnpm add @authon/react-native
16
+ npm install @authon/react-native react-native-svg
17
+ npx expo install expo-secure-store expo-web-browser
11
18
  ```
12
19
 
13
- Requires `react-native >= 0.72`, `expo-auth-session`, and `expo-secure-store` (or bare RN equivalents).
14
-
15
- ## Quick Start
20
+ `expo-secure-store` is the recommended storage adapter for Expo apps.
21
+ `expo-web-browser` is optional but recommended when you want a more controlled OAuth flow in Expo.
16
22
 
17
- ### 1. Provider
23
+ ## Setup
18
24
 
19
25
  ```tsx
20
- // App.tsx
21
26
  import { AuthonProvider } from '@authon/react-native';
27
+ import * as SecureStore from 'expo-secure-store';
28
+
29
+ const storage = {
30
+ getItem: (key: string) => SecureStore.getItemAsync(key),
31
+ setItem: (key: string, value: string) => SecureStore.setItemAsync(key, value),
32
+ removeItem: (key: string) => SecureStore.deleteItemAsync(key),
33
+ };
22
34
 
23
35
  export default function App() {
24
36
  return (
25
- <AuthonProvider publishableKey="pk_live_...">
37
+ <AuthonProvider publishableKey="pk_live_..." storage={storage}>
26
38
  <Navigation />
27
39
  </AuthonProvider>
28
40
  );
29
41
  }
30
42
  ```
31
43
 
32
- ### 2. Use Hooks
44
+ Without a storage adapter, tokens remain in memory only.
45
+
46
+ ## Hooks
47
+
48
+ ### `useAuthon()`
49
+
50
+ ```ts
51
+ const {
52
+ isLoaded,
53
+ isSignedIn,
54
+ userId,
55
+ accessToken,
56
+ user,
57
+ signIn,
58
+ signUp,
59
+ signOut,
60
+ getToken,
61
+ providers,
62
+ branding,
63
+ startOAuth,
64
+ completeOAuth,
65
+ on,
66
+ client,
67
+ } = useAuthon();
68
+ ```
69
+
70
+ ### `useUser()`
71
+
72
+ ```ts
73
+ const { isLoaded, isSignedIn, user } = useUser();
74
+ ```
75
+
76
+ ## Email / Password Example
33
77
 
34
78
  ```tsx
79
+ import { useState } from 'react';
80
+ import { View, TextInput, Button, Text, ActivityIndicator } from 'react-native';
35
81
  import { useAuthon, useUser } from '@authon/react-native';
36
- import { View, Text, Button } from 'react-native';
37
-
38
- function ProfileScreen() {
39
- const { isSignedIn, signOut } = useAuthon();
40
- const { user } = useUser();
41
82
 
42
- if (!isSignedIn) {
43
- return <SignInScreen />;
83
+ export function LoginScreen() {
84
+ const { isLoaded } = useUser();
85
+ const { signIn, signOut, user, isSignedIn } = useAuthon();
86
+ const [email, setEmail] = useState('');
87
+ const [password, setPassword] = useState('');
88
+ const [loading, setLoading] = useState(false);
89
+ const [error, setError] = useState<string | null>(null);
90
+
91
+ const handleSignIn = async () => {
92
+ setLoading(true);
93
+ setError(null);
94
+ try {
95
+ await signIn({ strategy: 'email_password', email, password });
96
+ } catch (err: any) {
97
+ setError(err.message ?? 'Sign-in failed');
98
+ } finally {
99
+ setLoading(false);
100
+ }
101
+ };
102
+
103
+ if (!isLoaded) return <ActivityIndicator />;
104
+
105
+ if (isSignedIn) {
106
+ return (
107
+ <View style={{ padding: 24, gap: 12 }}>
108
+ <Text>Welcome, {user?.displayName ?? user?.email}</Text>
109
+ <Button title="Sign out" onPress={signOut} />
110
+ </View>
111
+ );
44
112
  }
45
113
 
46
114
  return (
47
- <View>
48
- <Text>Welcome, {user?.displayName}</Text>
49
- <Button title="Sign Out" onPress={signOut} />
115
+ <View style={{ padding: 24, gap: 12 }}>
116
+ <TextInput placeholder="Email" value={email} onChangeText={setEmail} autoCapitalize="none" />
117
+ <TextInput placeholder="Password" value={password} onChangeText={setPassword} secureTextEntry />
118
+ {error ? <Text style={{ color: 'red' }}>{error}</Text> : null}
119
+ <Button title={loading ? 'Signing in...' : 'Sign in'} onPress={handleSignIn} disabled={loading} />
50
120
  </View>
51
121
  );
52
122
  }
123
+ ```
124
+
125
+ ## Social Buttons
53
126
 
54
- function SignInScreen() {
55
- const { signInWithOAuth, signInWithEmail } = useAuthon();
127
+ ```tsx
128
+ import { SocialButtons } from '@authon/react-native';
56
129
 
130
+ export function SocialLoginSection() {
57
131
  return (
58
- <View>
59
- <Button title="Sign in with Google" onPress={() => signInWithOAuth('google')} />
60
- <Button title="Sign in with Apple" onPress={() => signInWithOAuth('apple')} />
61
- </View>
132
+ <SocialButtons
133
+ onSuccess={() => console.log('Signed in')}
134
+ onError={(error) => console.error(error)}
135
+ />
62
136
  );
63
137
  }
64
138
  ```
65
139
 
66
- ## API Reference
140
+ For fully custom buttons, use `SocialButton` or call `startOAuth()` / `completeOAuth()` yourself.
141
+
142
+ ## Manual OAuth Flow
67
143
 
68
- ### `<AuthonProvider>`
144
+ The basic SDK flow looks like this:
69
145
 
70
146
  ```tsx
71
- <AuthonProvider
72
- publishableKey="pk_live_..."
73
- config={{
74
- apiUrl: 'https://api.authon.dev',
75
- scheme: 'myapp', // Custom URL scheme for OAuth redirect
76
- }}
77
- >
78
- ```
147
+ import { Linking } from 'react-native';
148
+ import { useAuthon } from '@authon/react-native';
79
149
 
80
- ### Hooks
150
+ export function GoogleButton() {
151
+ const { startOAuth, completeOAuth } = useAuthon();
81
152
 
82
- #### `useAuthon()`
153
+ const handlePress = async () => {
154
+ const { url, state } = await startOAuth('google');
155
+ await Linking.openURL(url);
156
+ await completeOAuth(state);
157
+ };
83
158
 
84
- ```ts
85
- const {
86
- isSignedIn, // boolean
87
- isLoading, // boolean
88
- user, // AuthonUser | null
89
- signInWithOAuth, // (provider: string) => Promise<void>
90
- signInWithEmail, // (email: string, password: string) => Promise<AuthonUser>
91
- signOut, // () => Promise<void>
92
- getToken, // () => Promise<string | null>
93
- } = useAuthon();
159
+ // ...
160
+ }
94
161
  ```
95
162
 
96
- #### `useUser()`
163
+ This is the simplest option, but it is browser-driven and completion is polling-based.
97
164
 
98
- ```ts
99
- const { user, isLoading } = useUser();
165
+ ## Recommended Expo OAuth Flow
166
+
167
+ If you want a cleaner Android experience with `expo-web-browser`, request the OAuth URL manually with `flow=redirect` and `returnTo`.
168
+
169
+ ```tsx
170
+ import * as WebBrowser from 'expo-web-browser';
171
+ import { Button } from 'react-native';
172
+ import { useAuthon } from '@authon/react-native';
173
+
174
+ const API_URL = 'https://api.authon.dev';
175
+ const PUBLISHABLE_KEY = 'pk_live_...';
176
+ const APP_DEEP_LINK = 'myapp://oauth-callback';
177
+ const RETURN_TO = 'https://auth.example.com/authon/mobile-callback';
178
+
179
+ async function requestOAuthUrl(provider: 'google') {
180
+ const params = new URLSearchParams({
181
+ redirectUri: `${API_URL}/v1/auth/oauth/redirect`,
182
+ flow: 'redirect',
183
+ returnTo: RETURN_TO,
184
+ });
185
+
186
+ const response = await fetch(
187
+ `${API_URL}/v1/auth/oauth/${provider}/url?${params.toString()}`,
188
+ { headers: { 'x-api-key': PUBLISHABLE_KEY } },
189
+ );
190
+
191
+ if (!response.ok) {
192
+ throw new Error(await response.text());
193
+ }
194
+
195
+ return response.json() as Promise<{ url: string; state: string }>;
196
+ }
197
+
198
+ export function GoogleButton() {
199
+ const { completeOAuth, getToken } = useAuthon();
200
+
201
+ const handlePress = async () => {
202
+ const { url, state } = await requestOAuthUrl('google');
203
+ const pollPromise = completeOAuth(state);
204
+
205
+ await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);
206
+ await pollPromise;
207
+
208
+ const authonAccessToken = getToken();
209
+ // If your app also has its own backend session, exchange authonAccessToken here.
210
+ };
211
+
212
+ return <Button title="Continue with Google" onPress={handlePress} />;
213
+ }
214
+ ```
215
+
216
+ Example HTTPS bridge page:
217
+
218
+ ```html
219
+ <!doctype html>
220
+ <html>
221
+ <body>
222
+ <script>
223
+ const params = new URLSearchParams(window.location.search);
224
+ const state = params.get('authon_oauth_state');
225
+ const error = params.get('authon_oauth_error');
226
+
227
+ const target = new URL('myapp://oauth-callback');
228
+ if (state) target.searchParams.set('state', state);
229
+ if (error) target.searchParams.set('error', error);
230
+
231
+ window.location.replace(target.toString());
232
+ </script>
233
+ </body>
234
+ </html>
100
235
  ```
101
236
 
102
- ### Token Storage
237
+ ## Important Notes
103
238
 
104
- Tokens are stored using `expo-secure-store` (Expo) or the platform keychain (bare RN), keeping credentials encrypted at rest.
239
+ - Do not register `myapp://...` directly as the provider redirect URI. Keep provider redirect URIs on `{apiUrl}/v1/auth/oauth/redirect`.
240
+ - Use `returnTo` for your app callback bridge. `returnTo` should be an HTTPS URL you control, and its origin must be listed in `ALLOWED_REDIRECT_ORIGINS`.
241
+ - If you need a custom app scheme, let the HTTPS bridge page redirect into `myapp://...`.
242
+ - On Android, `dismissAuthSession()` cannot reliably close an already-open Custom Tab. Plan your flow around `openAuthSessionAsync()` and a proper bridge.
243
+ - If your mobile app also maintains its own backend session, exchange `getToken()` with your backend immediately after `completeOAuth()`.
105
244
 
106
- ## Documentation
245
+ ## Docs
107
246
 
108
- [authon.dev/docs](https://authon.dev/docs)
247
+ - [Authon docs](https://authon.dev/docs)
248
+ - [Authon OAuth flow](https://authon.dev/docs)
109
249
 
110
250
  ## License
111
251