@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 +181 -83
- package/README.md +124 -346
- package/dist/index.cjs +10 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -3
- package/dist/index.d.ts +9 -3
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
## 빠른 시작
|
|
20
|
+
Expo 앱에서는 `expo-secure-store`를 권장합니다.
|
|
21
|
+
`expo-web-browser`는 필수는 아니지만, Android에서 더 안정적인 OAuth UX가 필요하면 함께 설치하는 편이 좋습니다.
|
|
18
22
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
const {
|
|
42
|
-
const { user } =
|
|
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 (
|
|
45
|
-
return
|
|
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
|
-
<
|
|
51
|
-
<
|
|
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
|
-
|
|
57
|
-
|
|
127
|
+
```tsx
|
|
128
|
+
import { SocialButtons } from '@authon/react-native';
|
|
58
129
|
|
|
130
|
+
export function SocialLoginSection() {
|
|
59
131
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
132
|
+
<SocialButtons
|
|
133
|
+
onSuccess={() => console.log('Signed in')}
|
|
134
|
+
onError={(error) => console.error(error)}
|
|
135
|
+
/>
|
|
64
136
|
);
|
|
65
137
|
}
|
|
66
138
|
```
|
|
67
139
|
|
|
68
|
-
|
|
140
|
+
완전히 커스텀한 버튼이 필요하면 `SocialButton`을 직접 쓰거나 `startOAuth()` / `completeOAuth()`를 수동으로 호출하면 됩니다.
|
|
141
|
+
|
|
142
|
+
## 수동 OAuth 플로우
|
|
69
143
|
|
|
70
|
-
|
|
144
|
+
가장 단순한 SDK 플로우는 아래와 같습니다.
|
|
71
145
|
|
|
72
146
|
```tsx
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
153
|
+
const handlePress = async () => {
|
|
154
|
+
const { url, state } = await startOAuth('google');
|
|
155
|
+
await Linking.openURL(url);
|
|
156
|
+
await completeOAuth(state);
|
|
157
|
+
};
|
|
85
158
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
163
|
+
이 방식은 가장 단순하지만, 브라우저 주도형이고 완료 감지는 polling 기반입니다.
|
|
99
164
|
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
throw new Error(await response.text());
|
|
193
|
+
}
|
|
111
194
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
import { AuthonMfaRequiredError } from '@authon/js';
|
|
195
|
+
return response.json() as Promise<{ url: string; state: string }>;
|
|
196
|
+
}
|
|
115
197
|
|
|
116
|
-
function
|
|
117
|
-
const {
|
|
118
|
-
const [qrSvg, setQrSvg] = useState('');
|
|
198
|
+
export function GoogleButton() {
|
|
199
|
+
const { completeOAuth, getToken } = useAuthon();
|
|
119
200
|
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
// setup.backupCodes — 사용자에게 안전하게 보관하도록 표시
|
|
124
|
-
};
|
|
201
|
+
const handlePress = async () => {
|
|
202
|
+
const { url, state } = await requestOAuthUrl('google');
|
|
203
|
+
const pollPromise = completeOAuth(state);
|
|
125
204
|
|
|
126
|
-
|
|
127
|
-
await
|
|
128
|
-
};
|
|
205
|
+
await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);
|
|
206
|
+
await pollPromise;
|
|
129
207
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
247
|
+
- [Authon docs](https://authon.dev/docs)
|
|
248
|
+
- [Authon OAuth flow](https://authon.dev/docs)
|
|
151
249
|
|
|
152
250
|
## 라이선스
|
|
153
251
|
|