@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 +252 -0
- package/README.md +196 -56
- package/dist/index.cjs +272 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +93 -4
- package/dist/index.d.ts +93 -4
- package/dist/index.js +267 -7
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
<
|
|
49
|
-
<
|
|
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
|
-
|
|
55
|
-
|
|
127
|
+
```tsx
|
|
128
|
+
import { SocialButtons } from '@authon/react-native';
|
|
56
129
|
|
|
130
|
+
export function SocialLoginSection() {
|
|
57
131
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
132
|
+
<SocialButtons
|
|
133
|
+
onSuccess={() => console.log('Signed in')}
|
|
134
|
+
onError={(error) => console.error(error)}
|
|
135
|
+
/>
|
|
62
136
|
);
|
|
63
137
|
}
|
|
64
138
|
```
|
|
65
139
|
|
|
66
|
-
|
|
140
|
+
For fully custom buttons, use `SocialButton` or call `startOAuth()` / `completeOAuth()` yourself.
|
|
141
|
+
|
|
142
|
+
## Manual OAuth Flow
|
|
67
143
|
|
|
68
|
-
|
|
144
|
+
The basic SDK flow looks like this:
|
|
69
145
|
|
|
70
146
|
```tsx
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
150
|
+
export function GoogleButton() {
|
|
151
|
+
const { startOAuth, completeOAuth } = useAuthon();
|
|
81
152
|
|
|
82
|
-
|
|
153
|
+
const handlePress = async () => {
|
|
154
|
+
const { url, state } = await startOAuth('google');
|
|
155
|
+
await Linking.openURL(url);
|
|
156
|
+
await completeOAuth(state);
|
|
157
|
+
};
|
|
83
158
|
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
163
|
+
This is the simplest option, but it is browser-driven and completion is polling-based.
|
|
97
164
|
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
237
|
+
## Important Notes
|
|
103
238
|
|
|
104
|
-
|
|
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
|
-
##
|
|
245
|
+
## Docs
|
|
107
246
|
|
|
108
|
-
[
|
|
247
|
+
- [Authon docs](https://authon.dev/docs)
|
|
248
|
+
- [Authon OAuth flow](https://authon.dev/docs)
|
|
109
249
|
|
|
110
250
|
## License
|
|
111
251
|
|