@authon/react-native 0.3.0 → 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.md CHANGED
@@ -2,35 +2,27 @@
2
2
 
3
3
  # @authon/react-native
4
4
 
5
- React Native SDK for [Authon](https://authon.dev) authentication. Provides React hooks, social login with in-app browser, and secure token storage.
5
+ React Native SDK for [Authon](https://authon.dev). It provides:
6
6
 
7
- ## Install
8
-
9
- ```bash
10
- npm install @authon/react-native @authon/js
11
- ```
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`)
12
12
 
13
- Peer dependencies:
13
+ ## Install
14
14
 
15
15
  ```bash
16
- npm install react-native react-native-svg
16
+ npm install @authon/react-native react-native-svg
17
+ npx expo install expo-secure-store expo-web-browser
17
18
  ```
18
19
 
19
- | Peer dependency | Version |
20
- |---|---|
21
- | `react` | `^18.0.0 \|\| ^19.0.0` |
22
- | `react-native` | `>=0.70.0` |
23
- | `react-native-svg` | `>=12.0.0` |
24
-
25
- ---
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.
26
22
 
27
23
  ## Setup
28
24
 
29
- Wrap your app with `AuthonProvider` and pass a token storage adapter. For secure storage, use `expo-secure-store` (Expo) or `react-native-keychain` (bare RN).
30
-
31
25
  ```tsx
32
- // App.tsx
33
- import React from 'react';
34
26
  import { AuthonProvider } from '@authon/react-native';
35
27
  import * as SecureStore from 'expo-secure-store';
36
28
 
@@ -49,62 +41,48 @@ export default function App() {
49
41
  }
50
42
  ```
51
43
 
52
- Without a storage adapter, tokens persist only in memory and are lost on app restart.
53
-
54
- ---
44
+ Without a storage adapter, tokens remain in memory only.
55
45
 
56
46
  ## Hooks
57
47
 
58
48
  ### `useAuthon()`
59
49
 
60
- Returns the full auth context.
61
-
62
- ```typescript
63
- import { useAuthon } from '@authon/react-native';
64
-
50
+ ```ts
65
51
  const {
66
- isLoaded, // boolean — true once the initial auth check is complete
67
- isSignedIn, // boolean
68
- userId, // string | null
69
- sessionId, // string | null
70
- accessToken, // string | null
71
- user, // AuthonUser | null
72
- signIn, // (params: SignInParams) => Promise<void>
73
- signUp, // (params: SignUpParams) => Promise<void>
74
- signOut, // () => Promise<void>
75
- getToken, // () => string | null — returns current access token
76
- providers, // OAuthProviderType[] — enabled providers from your project config
77
- branding, // BrandingConfig | null
78
- startOAuth, // (provider, redirectUri?) => Promise<{ url: string; state: string }>
79
- completeOAuth, // (state: string) => Promise<void>
80
- on, // (event, listener) => () => void — subscribe to auth events
81
- client, // AuthonMobileClient — underlying client instance
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,
82
67
  } = useAuthon();
83
68
  ```
84
69
 
85
70
  ### `useUser()`
86
71
 
87
- Convenience hook that returns only user state.
88
-
89
- ```typescript
90
- import { useUser } from '@authon/react-native';
91
-
72
+ ```ts
92
73
  const { isLoaded, isSignedIn, user } = useUser();
93
74
  ```
94
75
 
95
- ---
96
-
97
- ## Examples
98
-
99
- ### Login screen with email and password
76
+ ## Email / Password Example
100
77
 
101
78
  ```tsx
102
- import React, { useState } from 'react';
79
+ import { useState } from 'react';
103
80
  import { View, TextInput, Button, Text, ActivityIndicator } from 'react-native';
104
- import { useAuthon } from '@authon/react-native';
81
+ import { useAuthon, useUser } from '@authon/react-native';
105
82
 
106
83
  export function LoginScreen() {
107
- const { signIn, isLoaded } = useAuthon();
84
+ const { isLoaded } = useUser();
85
+ const { signIn, signOut, user, isSignedIn } = useAuthon();
108
86
  const [email, setEmail] = useState('');
109
87
  const [password, setPassword] = useState('');
110
88
  const [loading, setLoading] = useState(false);
@@ -116,7 +94,7 @@ export function LoginScreen() {
116
94
  try {
117
95
  await signIn({ strategy: 'email_password', email, password });
118
96
  } catch (err: any) {
119
- setError(err.message);
97
+ setError(err.message ?? 'Sign-in failed');
120
98
  } finally {
121
99
  setLoading(false);
122
100
  }
@@ -124,350 +102,150 @@ export function LoginScreen() {
124
102
 
125
103
  if (!isLoaded) return <ActivityIndicator />;
126
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
+ );
112
+ }
113
+
127
114
  return (
128
115
  <View style={{ padding: 24, gap: 12 }}>
129
- <TextInput
130
- placeholder="Email"
131
- value={email}
132
- onChangeText={setEmail}
133
- keyboardType="email-address"
134
- autoCapitalize="none"
135
- />
136
- <TextInput
137
- placeholder="Password"
138
- value={password}
139
- onChangeText={setPassword}
140
- secureTextEntry
141
- />
142
- {error && <Text style={{ color: 'red' }}>{error}</Text>}
143
- <Button title={loading ? 'Signing in...' : 'Sign In'} onPress={handleSignIn} disabled={loading} />
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} />
144
120
  </View>
145
121
  );
146
122
  }
147
123
  ```
148
124
 
149
- ### Social login buttons
150
-
151
- Use the built-in `SocialButtons` component. It fetches the enabled providers from your Authon project and renders buttons automatically.
125
+ ## Social Buttons
152
126
 
153
127
  ```tsx
154
- import React from 'react';
155
- import { View } from 'react-native';
156
128
  import { SocialButtons } from '@authon/react-native';
157
129
 
158
130
  export function SocialLoginSection() {
159
131
  return (
160
- <View style={{ padding: 24 }}>
161
- <SocialButtons
162
- onSuccess={() => console.log('Signed in')}
163
- onError={(err) => console.error(err)}
164
- />
165
- </View>
132
+ <SocialButtons
133
+ onSuccess={() => console.log('Signed in')}
134
+ onError={(error) => console.error(error)}
135
+ />
166
136
  );
167
137
  }
168
138
  ```
169
139
 
170
- Compact icon-only layout:
140
+ For fully custom buttons, use `SocialButton` or call `startOAuth()` / `completeOAuth()` yourself.
171
141
 
172
- ```tsx
173
- <SocialButtons
174
- compact
175
- onSuccess={() => console.log('Signed in')}
176
- onError={(err) => console.error(err)}
177
- />
178
- ```
179
-
180
- Individual `SocialButton`:
181
-
182
- ```tsx
183
- import { SocialButton } from '@authon/react-native';
184
-
185
- <SocialButton
186
- provider="google"
187
- onPress={(provider) => handleOAuth(provider)}
188
- loading={isLoading}
189
- label="Continue with Google" // optional — defaults to "Continue with Google"
190
- compact={false} // optional — icon-only square button
191
- height={48} // optional — button height in px
192
- borderRadius={10} // optional
193
- />
194
- ```
142
+ ## Manual OAuth Flow
195
143
 
196
- ### Manual OAuth flow with in-app browser
197
-
198
- `SocialButtons` handles OAuth automatically. For custom flows, use `startOAuth` and `completeOAuth` directly. OAuth uses the device browser (via `Linking.openURL`) rather than a popup.
144
+ The basic SDK flow looks like this:
199
145
 
200
146
  ```tsx
201
- import React, { useState } from 'react';
202
- import { Button, Linking } from 'react-native';
147
+ import { Linking } from 'react-native';
203
148
  import { useAuthon } from '@authon/react-native';
204
149
 
205
- export function GoogleSignInButton() {
150
+ export function GoogleButton() {
206
151
  const { startOAuth, completeOAuth } = useAuthon();
207
- const [loading, setLoading] = useState(false);
208
152
 
209
- const handleGoogleSignIn = async () => {
210
- setLoading(true);
211
- try {
212
- const { url, state } = await startOAuth('google');
213
- // Open the OAuth URL in the default browser
214
- await Linking.openURL(url);
215
- // Poll for OAuth completion (3 minute timeout)
216
- await completeOAuth(state);
217
- } catch (err: any) {
218
- if (err.message !== 'OAuth timeout') {
219
- console.error('OAuth failed:', err);
220
- }
221
- } finally {
222
- setLoading(false);
223
- }
153
+ const handlePress = async () => {
154
+ const { url, state } = await startOAuth('google');
155
+ await Linking.openURL(url);
156
+ await completeOAuth(state);
224
157
  };
225
158
 
226
- return (
227
- <Button
228
- title={loading ? 'Signing in...' : 'Sign in with Google'}
229
- onPress={handleGoogleSignIn}
230
- disabled={loading}
231
- />
232
- );
159
+ // ...
233
160
  }
234
161
  ```
235
162
 
236
- ### Session management
237
-
238
- ```tsx
239
- import React, { useEffect, useState } from 'react';
240
- import { View, Text, Button, FlatList } from 'react-native';
241
- import { useAuthon } from '@authon/react-native';
242
-
243
- export function SessionsScreen() {
244
- const { client, userId, signOut } = useAuthon();
245
- const [sessions, setSessions] = useState<any[]>([]);
163
+ This is the simplest option, but it is browser-driven and completion is polling-based.
246
164
 
247
- useEffect(() => {
248
- if (!userId) return;
249
- // Access the underlying AuthonMobileClient for advanced operations
250
- client.getUser().then(() => {
251
- // sessions are managed server-side; use @authon/node on the backend
252
- // to list and revoke sessions via authon.sessions.list(userId)
253
- });
254
- }, [userId]);
165
+ ## Recommended Expo OAuth Flow
255
166
 
256
- return (
257
- <View>
258
- <Button title="Sign Out (all devices)" onPress={signOut} />
259
- </View>
260
- );
261
- }
262
- ```
263
-
264
- ### MFA setup
265
-
266
- MFA (TOTP) is configured on the client side through the underlying `@authon/js` client. Access it via `client` from `useAuthon()`.
167
+ If you want a cleaner Android experience with `expo-web-browser`, request the OAuth URL manually with `flow=redirect` and `returnTo`.
267
168
 
268
169
  ```tsx
269
- import React, { useState } from 'react';
270
- import { View, TextInput, Button, Text, Image } from 'react-native';
170
+ import * as WebBrowser from 'expo-web-browser';
171
+ import { Button } from 'react-native';
271
172
  import { useAuthon } from '@authon/react-native';
272
173
 
273
- export function MfaSetupScreen() {
274
- const { client } = useAuthon();
275
- const [qrCodeUri, setQrCodeUri] = useState<string | null>(null);
276
- const [backupCodes, setBackupCodes] = useState<string[]>([]);
277
- const [code, setCode] = useState('');
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
+ );
278
190
 
279
- const setupMfa = async () => {
280
- // Use the underlying client's MFA methods (via @authon/js)
281
- const jsClient = (client as any)._jsClient;
282
- if (!jsClient) return;
191
+ if (!response.ok) {
192
+ throw new Error(await response.text());
193
+ }
283
194
 
284
- const setup = await jsClient.setupMfa();
285
- setQrCodeUri(setup.qrCodeUri);
286
- setBackupCodes(setup.backupCodes);
287
- };
288
-
289
- const verifyMfa = async () => {
290
- const jsClient = (client as any)._jsClient;
291
- await jsClient.verifyMfaSetup(code);
292
- };
293
-
294
- return (
295
- <View style={{ padding: 24, gap: 12 }}>
296
- {qrCodeUri ? (
297
- <>
298
- <Image source={{ uri: qrCodeUri }} style={{ width: 200, height: 200 }} />
299
- <Text>Backup codes: {backupCodes.join(', ')}</Text>
300
- <TextInput
301
- placeholder="Enter 6-digit code"
302
- value={code}
303
- onChangeText={setCode}
304
- keyboardType="numeric"
305
- />
306
- <Button title="Verify" onPress={verifyMfa} />
307
- </>
308
- ) : (
309
- <Button title="Enable MFA" onPress={setupMfa} />
310
- )}
311
- </View>
312
- );
195
+ return response.json() as Promise<{ url: string; state: string }>;
313
196
  }
314
- ```
315
197
 
316
- ### Web3 wallet connection
198
+ export function GoogleButton() {
199
+ const { completeOAuth, getToken } = useAuthon();
317
200
 
318
- ```tsx
319
- import React, { useState } from 'react';
320
- import { Button, View, Text } from 'react-native';
321
- import { useAuthon } from '@authon/react-native';
201
+ const handlePress = async () => {
202
+ const { url, state } = await requestOAuthUrl('google');
203
+ const pollPromise = completeOAuth(state);
322
204
 
323
- export function Web3ConnectScreen() {
324
- const { client } = useAuthon();
325
- const [walletAddress, setWalletAddress] = useState<string | null>(null);
326
-
327
- const connectWallet = async () => {
328
- // Request nonce from Authon API via the underlying client
329
- const apiUrl = client.getApiUrl();
330
- const token = client.getAccessToken();
331
-
332
- // 1. Get a sign message from Authon
333
- const nonceRes = await fetch(`${apiUrl}/v1/auth/web3/nonce`, {
334
- method: 'POST',
335
- headers: {
336
- 'Content-Type': 'application/json',
337
- ...(token ? { Authorization: `Bearer ${token}` } : {}),
338
- 'x-api-key': 'pk_live_...',
339
- },
340
- body: JSON.stringify({ address: walletAddress, chain: 'evm' }),
341
- });
342
- const { message } = await nonceRes.json();
343
-
344
- // 2. Sign with your wallet library (e.g. WalletConnect, ethers)
345
- // const signature = await wallet.signMessage(message);
346
-
347
- // 3. Verify and link wallet
348
- // await fetch(`${apiUrl}/v1/auth/web3/verify`, { ... })
349
- };
205
+ await WebBrowser.openAuthSessionAsync(url, APP_DEEP_LINK);
206
+ await pollPromise;
350
207
 
351
- return (
352
- <View style={{ padding: 24 }}>
353
- <Button title="Connect Wallet" onPress={connectWallet} />
354
- </View>
355
- );
356
- }
357
- ```
358
-
359
- ### Auth event subscription
360
-
361
- ```tsx
362
- import { useEffect } from 'react';
363
- import { useAuthon } from '@authon/react-native';
208
+ const authonAccessToken = getToken();
209
+ // If your app also has its own backend session, exchange authonAccessToken here.
210
+ };
364
211
 
365
- function App() {
366
- const { on } = useAuthon();
367
-
368
- useEffect(() => {
369
- const unsubSignedIn = on('signedIn', (user) => {
370
- console.log('User signed in:', user.id);
371
- });
372
- const unsubSignedOut = on('signedOut', () => {
373
- console.log('User signed out');
374
- });
375
- const unsubRefreshed = on('tokenRefreshed', () => {
376
- console.log('Token refreshed');
377
- });
378
- const unsubError = on('error', (err) => {
379
- console.error('Auth error:', err);
380
- });
381
-
382
- return () => {
383
- unsubSignedIn();
384
- unsubSignedOut();
385
- unsubRefreshed();
386
- unsubError();
387
- };
388
- }, [on]);
389
-
390
- return null;
212
+ return <Button title="Continue with Google" onPress={handlePress} />;
391
213
  }
392
214
  ```
393
215
 
394
- ---
395
-
396
- ## OAuth Note
216
+ Example HTTPS bridge page:
397
217
 
398
- Unlike web SDKs, `@authon/react-native` does not use popups. OAuth flows open the system browser via `Linking.openURL`. After the user authenticates, the SDK polls the Authon API to complete the session (up to 3 minutes). The `SocialButtons` component handles this polling automatically.
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');
399
226
 
400
- ---
227
+ const target = new URL('myapp://oauth-callback');
228
+ if (state) target.searchParams.set('state', state);
229
+ if (error) target.searchParams.set('error', error);
401
230
 
402
- ## API Reference
403
-
404
- ### `<AuthonProvider>`
405
-
406
- | Prop | Type | Description |
407
- |---|---|---|
408
- | `publishableKey` | `string` | Your Authon publishable key (`pk_live_...`) |
409
- | `apiUrl` | `string` | Optional — custom API base URL |
410
- | `storage` | `TokenStorage` | Optional — secure storage adapter for token persistence |
411
- | `children` | `React.ReactNode` | App content |
412
-
413
- ### `TokenStorage` interface
414
-
415
- ```typescript
416
- interface TokenStorage {
417
- getItem(key: string): Promise<string | null>;
418
- setItem(key: string, value: string): Promise<void>;
419
- removeItem(key: string): Promise<void>;
420
- }
231
+ window.location.replace(target.toString());
232
+ </script>
233
+ </body>
234
+ </html>
421
235
  ```
422
236
 
423
- ### `SignInParams`
237
+ ## Important Notes
424
238
 
425
- ```typescript
426
- interface SignInParams {
427
- strategy: 'email_password' | 'oauth';
428
- email?: string;
429
- password?: string;
430
- provider?: string;
431
- }
432
- ```
433
-
434
- ### `SignUpParams`
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()`.
435
244
 
436
- ```typescript
437
- interface SignUpParams {
438
- email: string;
439
- password: string;
440
- displayName?: string;
441
- }
442
- ```
245
+ ## Docs
443
246
 
444
- ### `AuthonMobileClient` methods
445
-
446
- | Method | Returns | Description |
447
- |---|---|---|
448
- | `signIn(params)` | `Promise<{ tokens, user }>` | Sign in with email/password or OAuth |
449
- | `signUp(params)` | `Promise<{ tokens, user }>` | Create account and sign in |
450
- | `signOut()` | `Promise<void>` | Sign out and clear local session |
451
- | `getUser()` | `Promise<AuthonUser \| null>` | Fetch current user from API |
452
- | `getCachedUser()` | `AuthonUser \| null` | Return locally cached user |
453
- | `getAccessToken()` | `string \| null` | Return current access token |
454
- | `isAuthenticated()` | `boolean` | Check if token is valid and not expired |
455
- | `refreshToken(token?)` | `Promise<TokenPair \| null>` | Refresh the access token |
456
- | `getProviders()` | `Promise<OAuthProviderType[]>` | Fetch enabled OAuth providers |
457
- | `getBranding()` | `Promise<BrandingConfig \| null>` | Fetch project branding config |
458
- | `getOAuthUrl(provider, redirectUri)` | `Promise<{ url, state }>` | Get OAuth authorization URL |
459
- | `completeOAuth(state)` | `Promise<{ tokens, user }>` | Poll for OAuth completion |
460
- | `getApiUrl()` | `string` | Return configured API base URL |
461
- | `on(event, listener)` | `() => void` | Subscribe to auth events, returns unsubscribe function |
462
- | `setStorage(storage)` | `void` | Set token storage adapter |
463
- | `initialize()` | `Promise<TokenPair \| null>` | Load tokens from storage on app start |
464
- | `destroy()` | `void` | Clear timers and event listeners |
465
-
466
- ---
467
-
468
- ## Documentation
469
-
470
- [authon.dev/docs](https://authon.dev/docs)
247
+ - [Authon docs](https://authon.dev/docs)
248
+ - [Authon OAuth flow](https://authon.dev/docs)
471
249
 
472
250
  ## License
473
251
 
package/dist/index.cjs CHANGED
@@ -187,8 +187,14 @@ var AuthonMobileClient = class {
187
187
  await this.ensureInitialized();
188
188
  return this._branding;
189
189
  }
190
- async getOAuthUrl(provider, redirectUri) {
191
- const params = new URLSearchParams({ redirectUri, flow: "redirect" });
190
+ async getOAuthUrl(provider, options) {
191
+ const normalized = typeof options === "string" ? { redirectUri: options } : options ?? {};
192
+ const redirectUri = normalized.redirectUri || `${this.apiUrl}/v1/auth/oauth/redirect`;
193
+ const flow = normalized.flow || "redirect";
194
+ const params = new URLSearchParams({ redirectUri, flow });
195
+ if (normalized.returnTo) {
196
+ params.set("returnTo", normalized.returnTo);
197
+ }
192
198
  return await this.request(
193
199
  "GET",
194
200
  `/v1/auth/oauth/${provider}/url?${params.toString()}`
@@ -500,9 +506,8 @@ function AuthonProvider({ children, storage, ...config }) {
500
506
  return client.getAccessToken();
501
507
  }, [client]);
502
508
  const startOAuth = (0, import_react.useCallback)(
503
- async (provider, redirectUri) => {
504
- const uri = redirectUri || `${client.getApiUrl()}/v1/auth/oauth/redirect`;
505
- return client.getOAuthUrl(provider, uri);
509
+ async (provider, options) => {
510
+ return client.getOAuthUrl(provider, options);
506
511
  },
507
512
  [client]
508
513
  );