@authon/react-native 0.3.0 → 0.3.2

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 CHANGED
@@ -2,152 +2,250 @@
2
2
 
3
3
  # @authon/react-native
4
4
 
5
- [Authon](https://authon.dev)용 React Native SDK 네이티브 OAuth, 보안 토큰 저장소, React 훅을 제공합니다.
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`)
6
12
 
7
13
  ## 설치
8
14
 
9
15
  ```bash
10
- npm install @authon/react-native
11
- # or
12
- pnpm add @authon/react-native
16
+ npm install @authon/react-native react-native-svg
17
+ npx expo install expo-secure-store expo-web-browser
13
18
  ```
14
19
 
15
- `react-native >= 0.72`, `expo-auth-session`, `expo-secure-store`(또는 bare RN 동등 패키지)가 필요합니다.
16
-
17
- ## 빠른 시작
20
+ Expo 앱에서는 `expo-secure-store`를 권장합니다.
21
+ `expo-web-browser`는 필수는 아니지만, Android에서 더 안정적인 OAuth UX가 필요하면 함께 설치하는 편이 좋습니다.
18
22
 
19
- ### 1. Provider
23
+ ## 설정
20
24
 
21
25
  ```tsx
22
- // App.tsx
23
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
+ };
24
34
 
25
35
  export default function App() {
26
36
  return (
27
- <AuthonProvider publishableKey="pk_live_...">
37
+ <AuthonProvider publishableKey="pk_live_..." storage={storage}>
28
38
  <Navigation />
29
39
  </AuthonProvider>
30
40
  );
31
41
  }
32
42
  ```
33
43
 
34
- ### 2. 사용
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
+ ## 이메일 / 비밀번호 예제
35
77
 
36
78
  ```tsx
79
+ import { useState } from 'react';
80
+ import { View, TextInput, Button, Text, ActivityIndicator } from 'react-native';
37
81
  import { useAuthon, useUser } from '@authon/react-native';
38
- import { View, Text, Button } from 'react-native';
39
82
 
40
- function ProfileScreen() {
41
- const { isSignedIn, signOut } = useAuthon();
42
- const { user } = useUser();
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 />;
43
104
 
44
- if (!isSignedIn) {
45
- return <SignInScreen />;
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
+ );
46
112
  }
47
113
 
48
114
  return (
49
- <View>
50
- <Text>Welcome, {user?.displayName}</Text>
51
- <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 ? '로그인 중...' : '로그인'} onPress={handleSignIn} disabled={loading} />
52
120
  </View>
53
121
  );
54
122
  }
123
+ ```
124
+
125
+ ## 소셜 버튼
55
126
 
56
- function SignInScreen() {
57
- const { signInWithOAuth, signInWithEmail } = useAuthon();
127
+ ```tsx
128
+ import { SocialButtons } from '@authon/react-native';
58
129
 
130
+ export function SocialLoginSection() {
59
131
  return (
60
- <View>
61
- <Button title="Sign in with Google" onPress={() => signInWithOAuth('google')} />
62
- <Button title="Sign in with Apple" onPress={() => signInWithOAuth('apple')} />
63
- </View>
132
+ <SocialButtons
133
+ onSuccess={() => console.log('Signed in')}
134
+ onError={(error) => console.error(error)}
135
+ />
64
136
  );
65
137
  }
66
138
  ```
67
139
 
68
- ## API 레퍼런스
140
+ 완전히 커스텀한 버튼이 필요하면 `SocialButton`을 직접 쓰거나 `startOAuth()` / `completeOAuth()`를 수동으로 호출하면 됩니다.
141
+
142
+ ## 수동 OAuth 플로우
69
143
 
70
- ### `<AuthonProvider>`
144
+ 가장 단순한 SDK 플로우는 아래와 같습니다.
71
145
 
72
146
  ```tsx
73
- <AuthonProvider
74
- publishableKey="pk_live_..."
75
- config={{
76
- apiUrl: 'https://api.authon.dev',
77
- scheme: 'myapp', // OAuth 리다이렉트용 커스텀 URL 스킴
78
- }}
79
- >
80
- ```
147
+ import { Linking } from 'react-native';
148
+ import { useAuthon } from '@authon/react-native';
81
149
 
82
- ###
150
+ export function GoogleButton() {
151
+ const { startOAuth, completeOAuth } = useAuthon();
83
152
 
84
- #### `useAuthon()`
153
+ const handlePress = async () => {
154
+ const { url, state } = await startOAuth('google');
155
+ await Linking.openURL(url);
156
+ await completeOAuth(state);
157
+ };
85
158
 
86
- ```ts
87
- const {
88
- isSignedIn, // boolean
89
- isLoading, // boolean
90
- user, // AuthonUser | null
91
- signInWithOAuth, // (provider: string) => Promise<void>
92
- signInWithEmail, // (email: string, password: string) => Promise<AuthonUser>
93
- signOut, // () => Promise<void>
94
- getToken, // () => Promise<string | null>
95
- } = useAuthon();
159
+ // ...
160
+ }
96
161
  ```
97
162
 
98
- #### `useUser()`
163
+ 방식은 가장 단순하지만, 브라우저 주도형이고 완료 감지는 polling 기반입니다.
99
164
 
100
- ```ts
101
- const { user, isLoading } = useUser();
102
- ```
165
+ ## 권장 Expo OAuth 플로우
103
166
 
104
- ### 토큰 저장소
167
+ Android에서 자연스러운 브라우저 종료 UX가 필요하면 `flow=redirect`와 `returnTo`를 포함해 OAuth URL을 직접 요청하고 `expo-web-browser`로 여는 방식을 권장합니다.
105
168
 
106
- 토큰은 `expo-secure-store`(Expo) 또는 플랫폼 키체인(bare RN)을 사용하여 저장되며, 자격증명은 저장 시 암호화됩니다.
169
+ ```tsx
170
+ import * as WebBrowser from 'expo-web-browser';
171
+ import { Button } from 'react-native';
172
+ import { useAuthon } from '@authon/react-native';
107
173
 
108
- ## 다단계 인증 (MFA)
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
+ );
109
190
 
110
- `useAuthon()`에서 반환되는 `client`를 통해 MFA에 접근합니다.
191
+ if (!response.ok) {
192
+ throw new Error(await response.text());
193
+ }
111
194
 
112
- ```tsx
113
- import { useAuthon } from '@authon/react-native';
114
- import { AuthonMfaRequiredError } from '@authon/js';
195
+ return response.json() as Promise<{ url: string; state: string }>;
196
+ }
115
197
 
116
- function MfaSetupScreen() {
117
- const { client } = useAuthon();
118
- const [qrSvg, setQrSvg] = useState('');
198
+ export function GoogleButton() {
199
+ const { completeOAuth, getToken } = useAuthon();
119
200
 
120
- const enableMfa = async () => {
121
- const setup = await client!.setupMfa();
122
- setQrSvg(setup.qrCodeSvg); // QR을 SVG로 렌더링
123
- // setup.backupCodes — 사용자에게 안전하게 보관하도록 표시
124
- };
201
+ const handlePress = async () => {
202
+ const { url, state } = await requestOAuthUrl('google');
203
+ const pollPromise = completeOAuth(state);
125
204
 
126
- const verifySetup = async (code: string) => {
127
- await client!.verifyMfaSetup(code);
128
- };
205
+ await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);
206
+ await pollPromise;
129
207
 
130
- // MFA 로그인 플로우
131
- const signIn = async (email: string, password: string) => {
132
- try {
133
- await client!.signInWithEmail(email, password);
134
- } catch (err) {
135
- if (err instanceof AuthonMfaRequiredError) {
136
- // TOTP 입력 화면으로 이동
137
- // 이후: await client!.verifyMfa(err.mfaToken, code);
138
- }
139
- }
208
+ const authonAccessToken = getToken();
209
+ // 앱이 자체 백엔드 세션도 가진다면 여기서 authonAccessToken을 교환하세요.
140
210
  };
141
211
 
142
- // ...
212
+ return <Button title="Google로 계속하기" onPress={handlePress} />;
143
213
  }
144
214
  ```
145
215
 
146
- 전체 API 레퍼런스는 [`@authon/js` MFA 문서](../js/README.md#multi-factor-authentication-mfa)를 참고하세요.
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()`을 백엔드에 넘겨 자체 세션을 발급받으세요.
147
244
 
148
245
  ## 문서
149
246
 
150
- [authon.dev/docs](https://authon.dev/docs)
247
+ - [Authon docs](https://authon.dev/docs)
248
+ - [Authon OAuth flow](https://authon.dev/docs)
151
249
 
152
250
  ## 라이선스
153
251