@proveanything/smartlinks-auth-ui 0.5.21 → 0.6.0
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/ACCOUNT_CACHING.md +349 -0
- package/ANDROID_NATIVE_BRIDGE.md +775 -0
- package/AUTH_STATE_MANAGEMENT.md +262 -0
- package/CAPACITOR_INTEGRATION.md +244 -0
- package/CUSTOMIZATION_GUIDE.md +411 -0
- package/README.md +73 -13
- package/SDK_DEBUGGING_GUIDE.md +217 -0
- package/SMARTLINKS_FRAME.md +302 -0
- package/SMARTLINKS_INTEGRATION.md +330 -0
- package/WHATSAPP_OTP_PLAN.md +106 -0
- package/dist/api.d.ts +15 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/components/ProviderButtons.d.ts +1 -0
- package/dist/components/ProviderButtons.d.ts.map +1 -1
- package/dist/components/SmartlinksAuthUI.d.ts.map +1 -1
- package/dist/context/AuthContext.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.esm.js +386 -34
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +386 -33
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +98 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/persistentStorage.d.ts +14 -0
- package/dist/utils/persistentStorage.d.ts.map +1 -1
- package/dist/utils/tokenStorage.d.ts +7 -0
- package/dist/utils/tokenStorage.d.ts.map +1 -1
- package/package.json +15 -6
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Authentication State Management
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Smartlinks Auth UI module provides comprehensive authentication state management with real-time synchronization across browser tabs and multiple ways for your application to respond to authentication changes.
|
|
6
|
+
|
|
7
|
+
## Accessing Authentication State
|
|
8
|
+
|
|
9
|
+
### 1. Using the `useAuth` Hook
|
|
10
|
+
|
|
11
|
+
The primary way to access authentication state in React components:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { useAuth } from '@smartlinks/auth-ui';
|
|
15
|
+
|
|
16
|
+
function MyComponent() {
|
|
17
|
+
const { user, token, accountData, isAuthenticated, isLoading } = useAuth();
|
|
18
|
+
|
|
19
|
+
if (isLoading) return <div>Loading...</div>;
|
|
20
|
+
if (!isAuthenticated) return <div>Please log in</div>;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div>
|
|
24
|
+
<h1>Welcome, {user.displayName}!</h1>
|
|
25
|
+
<p>Email: {user.email}</p>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Properties:**
|
|
32
|
+
- `user: AuthUser | null` - Current authenticated user object
|
|
33
|
+
- `token: string | null` - Current authentication token
|
|
34
|
+
- `accountData: Record<string, any> | null` - Additional account metadata
|
|
35
|
+
- `isAuthenticated: boolean` - Whether a user is currently logged in
|
|
36
|
+
- `isLoading: boolean` - Whether initial auth state is still loading
|
|
37
|
+
|
|
38
|
+
### 2. Using `onAuthStateChange` Callback
|
|
39
|
+
|
|
40
|
+
Subscribe to all authentication state changes including login, logout, cross-tab synchronization, and token refresh:
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { useAuth } from '@smartlinks/auth-ui';
|
|
44
|
+
import { useEffect } from 'react';
|
|
45
|
+
import { useNavigate } from 'react-router-dom';
|
|
46
|
+
|
|
47
|
+
function App() {
|
|
48
|
+
const { onAuthStateChange } = useAuth();
|
|
49
|
+
const navigate = useNavigate();
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
// Subscribe to auth state changes
|
|
53
|
+
const unsubscribe = onAuthStateChange((event) => {
|
|
54
|
+
console.log('Auth state changed:', event.type);
|
|
55
|
+
|
|
56
|
+
if (event.type === 'LOGIN' || event.type === 'CROSS_TAB_SYNC') {
|
|
57
|
+
if (event.user) {
|
|
58
|
+
// User logged in (in this tab or another)
|
|
59
|
+
navigate('/dashboard');
|
|
60
|
+
}
|
|
61
|
+
} else if (event.type === 'LOGOUT') {
|
|
62
|
+
// User logged out
|
|
63
|
+
navigate('/login');
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Cleanup subscription on unmount
|
|
68
|
+
return unsubscribe;
|
|
69
|
+
}, [onAuthStateChange, navigate]);
|
|
70
|
+
|
|
71
|
+
return <YourAppComponents />;
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Event Types:**
|
|
76
|
+
- `LOGIN` - User logged in via the auth UI
|
|
77
|
+
- `LOGOUT` - User logged out
|
|
78
|
+
- `CROSS_TAB_SYNC` - Authentication state synchronized from another browser tab
|
|
79
|
+
- `TOKEN_REFRESH` - Authentication token was refreshed (future feature)
|
|
80
|
+
- `ACCOUNT_REFRESH` - Account information was fetched/refreshed
|
|
81
|
+
|
|
82
|
+
**Event Object:**
|
|
83
|
+
```typescript
|
|
84
|
+
{
|
|
85
|
+
type: 'LOGIN' | 'LOGOUT' | 'TOKEN_REFRESH' | 'CROSS_TAB_SYNC' | 'ACCOUNT_REFRESH';
|
|
86
|
+
user: AuthUser | null;
|
|
87
|
+
token: string | null;
|
|
88
|
+
accountData: Record<string, any> | null;
|
|
89
|
+
accountInfo?: Record<string, any> | null;
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### 3. Using Auth Operation Callbacks
|
|
94
|
+
|
|
95
|
+
Handle specific authentication operations with callbacks:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<SmartlinksAuthUI
|
|
99
|
+
clientId="your-client-id"
|
|
100
|
+
apiEndpoint="https://smartlinks.app/api/v1"
|
|
101
|
+
onAuthSuccess={(response) => {
|
|
102
|
+
console.log('User authenticated:', response.user);
|
|
103
|
+
// Redirect or update UI
|
|
104
|
+
window.location.href = '/dashboard';
|
|
105
|
+
}}
|
|
106
|
+
onAuthError={(error) => {
|
|
107
|
+
console.error('Authentication failed:', error);
|
|
108
|
+
// Show error message to user
|
|
109
|
+
}}
|
|
110
|
+
/>
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Cross-Tab Synchronization
|
|
114
|
+
|
|
115
|
+
The auth module automatically synchronizes authentication state across all open browser tabs:
|
|
116
|
+
|
|
117
|
+
- **Login in one tab** → All tabs automatically update to authenticated state
|
|
118
|
+
- **Logout in one tab** → All tabs automatically clear authentication state
|
|
119
|
+
- **Session expiration** → All tabs receive the update simultaneously
|
|
120
|
+
|
|
121
|
+
This is implemented using:
|
|
122
|
+
- **IndexedDB** for persistent token storage
|
|
123
|
+
- **BroadcastChannel API** for cross-tab messaging
|
|
124
|
+
- **Storage events** for fallback synchronization
|
|
125
|
+
|
|
126
|
+
## Protected Routes
|
|
127
|
+
|
|
128
|
+
Use the `ProtectedRoute` component to restrict access to authenticated users:
|
|
129
|
+
|
|
130
|
+
```tsx
|
|
131
|
+
import { ProtectedRoute } from '@smartlinks/auth-ui';
|
|
132
|
+
|
|
133
|
+
function App() {
|
|
134
|
+
return (
|
|
135
|
+
<Routes>
|
|
136
|
+
<Route path="/login" element={<LoginPage />} />
|
|
137
|
+
<Route
|
|
138
|
+
path="/dashboard"
|
|
139
|
+
element={
|
|
140
|
+
<ProtectedRoute fallback={<LoginPage />}>
|
|
141
|
+
<DashboardPage />
|
|
142
|
+
</ProtectedRoute>
|
|
143
|
+
}
|
|
144
|
+
/>
|
|
145
|
+
</Routes>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Props:**
|
|
151
|
+
- `children` - Protected content to render when authenticated
|
|
152
|
+
- `fallback` - Component to render when not authenticated (optional)
|
|
153
|
+
- `redirectTo` - URL to redirect to when not authenticated (optional)
|
|
154
|
+
|
|
155
|
+
## Complete Example: App with Auth State Management
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import React, { useEffect } from 'react';
|
|
159
|
+
import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom';
|
|
160
|
+
import { AuthProvider, useAuth, SmartlinksAuthUI, ProtectedRoute } from '@smartlinks/auth-ui';
|
|
161
|
+
|
|
162
|
+
// Login page component
|
|
163
|
+
function LoginPage() {
|
|
164
|
+
const navigate = useNavigate();
|
|
165
|
+
const { isAuthenticated } = useAuth();
|
|
166
|
+
|
|
167
|
+
// Redirect if already authenticated
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (isAuthenticated) {
|
|
170
|
+
navigate('/dashboard');
|
|
171
|
+
}
|
|
172
|
+
}, [isAuthenticated, navigate]);
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<div>
|
|
176
|
+
<h1>Sign In</h1>
|
|
177
|
+
<SmartlinksAuthUI
|
|
178
|
+
clientId="your-client-id"
|
|
179
|
+
apiEndpoint="https://smartlinks.app/api/v1"
|
|
180
|
+
onAuthSuccess={() => navigate('/dashboard')}
|
|
181
|
+
onAuthError={(error) => alert(error.message)}
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Protected dashboard component
|
|
188
|
+
function DashboardPage() {
|
|
189
|
+
const { user, logout } = useAuth();
|
|
190
|
+
|
|
191
|
+
return (
|
|
192
|
+
<div>
|
|
193
|
+
<h1>Dashboard</h1>
|
|
194
|
+
<p>Welcome, {user?.displayName}!</p>
|
|
195
|
+
<button onClick={logout}>Sign Out</button>
|
|
196
|
+
</div>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// App root with auth state monitoring
|
|
201
|
+
function AppContent() {
|
|
202
|
+
const { onAuthStateChange } = useAuth();
|
|
203
|
+
const navigate = useNavigate();
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const unsubscribe = onAuthStateChange((event) => {
|
|
207
|
+
if (event.type === 'LOGOUT') {
|
|
208
|
+
navigate('/login');
|
|
209
|
+
} else if (event.type === 'LOGIN' || event.type === 'CROSS_TAB_SYNC') {
|
|
210
|
+
if (event.user) {
|
|
211
|
+
navigate('/dashboard');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
return unsubscribe;
|
|
217
|
+
}, [onAuthStateChange, navigate]);
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<Routes>
|
|
221
|
+
<Route path="/login" element={<LoginPage />} />
|
|
222
|
+
<Route
|
|
223
|
+
path="/dashboard"
|
|
224
|
+
element={
|
|
225
|
+
<ProtectedRoute fallback={<LoginPage />}>
|
|
226
|
+
<DashboardPage />
|
|
227
|
+
</ProtectedRoute>
|
|
228
|
+
}
|
|
229
|
+
/>
|
|
230
|
+
</Routes>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// App root
|
|
235
|
+
export default function App() {
|
|
236
|
+
return (
|
|
237
|
+
<AuthProvider>
|
|
238
|
+
<BrowserRouter>
|
|
239
|
+
<AppContent />
|
|
240
|
+
</BrowserRouter>
|
|
241
|
+
</AuthProvider>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Best Practices
|
|
247
|
+
|
|
248
|
+
1. **Always wrap your app with `AuthProvider`** at the root level
|
|
249
|
+
2. **Use `onAuthStateChange`** for app-level routing and state synchronization
|
|
250
|
+
3. **Use `useAuth` hook** for component-level auth state access
|
|
251
|
+
4. **Use `ProtectedRoute`** to guard authenticated routes
|
|
252
|
+
5. **Handle loading states** with `isLoading` to avoid flickering
|
|
253
|
+
6. **Subscribe to `onAuthStateChange` early** in your app initialization to catch cross-tab events
|
|
254
|
+
|
|
255
|
+
## Session Persistence
|
|
256
|
+
|
|
257
|
+
Sessions are automatically persisted using:
|
|
258
|
+
- **Primary:** IndexedDB (survives browser restart, shared across tabs)
|
|
259
|
+
- **Fallback:** localStorage (for browsers without IndexedDB support)
|
|
260
|
+
- **In-memory:** Last resort for restricted environments
|
|
261
|
+
|
|
262
|
+
Tokens are stored with expiration metadata and automatically validated on load. Expired tokens are automatically cleared.
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Capacitor Native Auth (Google & Apple)
|
|
2
|
+
|
|
3
|
+
This guide shows how to enable **native** Google and Apple sign-in for apps that
|
|
4
|
+
wrap the auth UI in a [Capacitor](https://capacitorjs.com/) shell (including apps
|
|
5
|
+
shipped with [Capgo](https://capgo.app/) OTA updates).
|
|
6
|
+
|
|
7
|
+
Unlike the legacy [`window.AuthKit` bridge](./ANDROID_NATIVE_BRIDGE.md) — which
|
|
8
|
+
uses a stringified-JSON + `callbackId` callback structure — Capacitor plugins are
|
|
9
|
+
**Promise-based**. So instead of that bridge, you inject a small **`nativeAuth`
|
|
10
|
+
adapter** and the auth UI `await`s it directly.
|
|
11
|
+
|
|
12
|
+
> The library stays plugin-agnostic: it depends on **no** Capacitor packages. Your
|
|
13
|
+
> app owns the plugin and wires it into the adapter shape below.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 1. Install a native social-login plugin
|
|
18
|
+
|
|
19
|
+
Any plugin works as long as you can produce a Google **ID token** and an Apple
|
|
20
|
+
**identity token**. The natural fit for Capgo apps is
|
|
21
|
+
[`@capgo/capacitor-social-login`](https://github.com/Cap-go/capacitor-social-login):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @capgo/capacitor-social-login
|
|
25
|
+
npx cap sync
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Configure native projects per that plugin's README (iOS: add the Sign in with
|
|
29
|
+
Apple capability + URL scheme; Android: add your Google **Web Client ID** /
|
|
30
|
+
`serverClientId` and SHA-1). See [ANDROID_NATIVE_BRIDGE.md](./ANDROID_NATIVE_BRIDGE.md#prerequisites)
|
|
31
|
+
for the Google Cloud Console setup — the client IDs are identical.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 2. Build the adapter
|
|
36
|
+
|
|
37
|
+
The adapter implements the `NativeAuthAdapter` interface exported by this package.
|
|
38
|
+
Each method resolves a `NativeAuthResult` whose `idToken` is the token your backend
|
|
39
|
+
verifies (`googleLogin` / the Apple endpoint).
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { SocialLogin } from '@capgo/capacitor-social-login';
|
|
43
|
+
import { Capacitor } from '@capacitor/core';
|
|
44
|
+
import type { NativeAuthAdapter } from '@proveanything/smartlinks-auth-ui';
|
|
45
|
+
|
|
46
|
+
const GOOGLE_WEB_CLIENT_ID = '696509063554-xxx.apps.googleusercontent.com';
|
|
47
|
+
const APPLE_SERVICES_ID = 'com.yourapp.signin';
|
|
48
|
+
|
|
49
|
+
let initialized = false;
|
|
50
|
+
async function ensureInit() {
|
|
51
|
+
if (initialized) return;
|
|
52
|
+
await SocialLogin.initialize({
|
|
53
|
+
google: { webClientId: GOOGLE_WEB_CLIENT_ID },
|
|
54
|
+
apple: { clientId: APPLE_SERVICES_ID },
|
|
55
|
+
});
|
|
56
|
+
initialized = true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const capacitorNativeAuth: NativeAuthAdapter = {
|
|
60
|
+
async signInWithGoogle() {
|
|
61
|
+
await ensureInit();
|
|
62
|
+
const { result } = await SocialLogin.login({
|
|
63
|
+
provider: 'google',
|
|
64
|
+
options: { scopes: ['email', 'profile'] },
|
|
65
|
+
});
|
|
66
|
+
// Field names vary slightly by plugin version — log `result` once to confirm.
|
|
67
|
+
return {
|
|
68
|
+
idToken: (result as any).idToken,
|
|
69
|
+
email: (result as any).profile?.email,
|
|
70
|
+
name: (result as any).profile?.name,
|
|
71
|
+
picture: (result as any).profile?.imageUrl,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async signInWithApple() {
|
|
76
|
+
await ensureInit();
|
|
77
|
+
const { result } = await SocialLogin.login({
|
|
78
|
+
provider: 'apple',
|
|
79
|
+
options: { scopes: ['email', 'name'] },
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
idToken: (result as any).idToken, // Apple identity token (JWT)
|
|
83
|
+
authorizationCode: (result as any).accessToken,
|
|
84
|
+
email: (result as any).profile?.email,
|
|
85
|
+
name: (result as any).profile?.name,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
async signOut() {
|
|
90
|
+
try { await SocialLogin.logout({ provider: 'google' }); } catch {}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Only attach native auth when actually running in a native shell —
|
|
95
|
+
// on the web, leave it undefined so the normal browser OAuth flow is used.
|
|
96
|
+
export const nativeAuth = Capacitor.isNativePlatform() ? capacitorNativeAuth : undefined;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 3. Pass it to the auth UI
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { SmartlinksAuthUI } from '@proveanything/smartlinks-auth-ui';
|
|
105
|
+
import { nativeAuth } from './capacitorNativeAuth';
|
|
106
|
+
|
|
107
|
+
<SmartlinksAuthUI
|
|
108
|
+
clientId="your-client-id"
|
|
109
|
+
enabledProviders={['email', 'google', 'apple']}
|
|
110
|
+
nativeAuth={nativeAuth}
|
|
111
|
+
enableSilentNativeSignIn // optional: auto sign-in if a native session exists
|
|
112
|
+
customization={{ appleClientId: APPLE_SERVICES_ID }}
|
|
113
|
+
onAuthSuccess={(token, user) => console.log('Authenticated:', user.email)}
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
That's it. When `nativeAuth` is present:
|
|
118
|
+
|
|
119
|
+
- **Google** → uses `nativeAuth.signInWithGoogle()` (highest priority, before the
|
|
120
|
+
legacy `window.AuthKit` bridge, proxy popup, and in-browser GIS flow).
|
|
121
|
+
- **Apple** → uses `nativeAuth.signInWithApple()`. Apple is **native-only** in this
|
|
122
|
+
kit; without an adapter the Apple button reports it's unavailable.
|
|
123
|
+
- On the **web** build (`nativeAuth` undefined) everything falls back to the
|
|
124
|
+
existing browser flows automatically.
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 4. Mobile session persistence ("stay logged in forever")
|
|
129
|
+
|
|
130
|
+
Mobile users expect to open the app weeks later and still be signed in. The web
|
|
131
|
+
defaults (IndexedDB, 7-day token cache, refresh on next page load) are not enough.
|
|
132
|
+
Wire up the three pieces below in your Capacitor shell.
|
|
133
|
+
|
|
134
|
+
### 4a. Use OS-secure storage for tokens
|
|
135
|
+
|
|
136
|
+
By default the kit persists tokens in IndexedDB inside the WebView. That survives
|
|
137
|
+
app restarts and Capgo OTA updates, but it is **not encrypted at rest**. For any
|
|
138
|
+
app that handles sensitive data, swap in the OS keychain via the
|
|
139
|
+
`setStorageAdapter` hook exported from the kit. Install a secure-storage plugin
|
|
140
|
+
(e.g. [`capacitor-secure-storage-plugin`](https://github.com/martinkasa/capacitor-secure-storage-plugin)
|
|
141
|
+
or `@capacitor/preferences` for non-sensitive cases) and implement the
|
|
142
|
+
`PersistentStorage` interface against it:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin';
|
|
146
|
+
import { setStorageAdapter, type PersistentStorage } from '@proveanything/smartlinks-auth-ui';
|
|
147
|
+
|
|
148
|
+
const keychainStorage: PersistentStorage = {
|
|
149
|
+
async setItem(key, value) { await SecureStoragePlugin.set({ key, value: JSON.stringify(value) }); },
|
|
150
|
+
async getItem(key) { try { const { value } = await SecureStoragePlugin.get({ key }); return JSON.parse(value); } catch { return null; } },
|
|
151
|
+
async removeItem(key) { try { await SecureStoragePlugin.remove({ key }); } catch {} },
|
|
152
|
+
async clear() { const { keys } = await SecureStoragePlugin.keys(); await Promise.all(keys.map(k => SecureStoragePlugin.remove({ key: k }))); },
|
|
153
|
+
async isAvailable() { return true; },
|
|
154
|
+
getBackend() { return 'none'; }, // not one of the built-ins
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Call ONCE, before <SmartlinksAuthUI /> mounts (e.g. in main.tsx).
|
|
158
|
+
if (Capacitor.isNativePlatform()) setStorageAdapter(keychainStorage);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### 4b. Refresh the session on app resume
|
|
162
|
+
|
|
163
|
+
Capacitor fires `appStateChange` when the OS hands the app back. Call
|
|
164
|
+
`useAuth().refreshToken()` there so a cold-open after days re-validates the
|
|
165
|
+
session before the user touches anything. Today this re-verifies the existing
|
|
166
|
+
JWT and extends the local cache window; once the optional refresh-token endpoint
|
|
167
|
+
(see §5) is deployed it will also rotate to a fresh long-lived token.
|
|
168
|
+
|
|
169
|
+
```tsx
|
|
170
|
+
import { App } from '@capacitor/app';
|
|
171
|
+
import { useAuth } from '@proveanything/smartlinks-auth-ui';
|
|
172
|
+
|
|
173
|
+
function ResumeRefresh() {
|
|
174
|
+
const { refreshToken, token } = useAuth();
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
const sub = App.addListener('appStateChange', ({ isActive }) => {
|
|
177
|
+
if (isActive && token) refreshToken().catch(() => {});
|
|
178
|
+
});
|
|
179
|
+
return () => { sub.remove(); };
|
|
180
|
+
}, [refreshToken, token]);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### 4c. Wire logout to the native provider
|
|
186
|
+
|
|
187
|
+
`useAuth().logout()` clears the SmartLinks session. If the user signed in with
|
|
188
|
+
Google or Apple natively, also call `nativeAuth.signOut()` so the OS-level
|
|
189
|
+
session is dropped — otherwise the next "Continue with Google" silently
|
|
190
|
+
re-uses the previous account.
|
|
191
|
+
|
|
192
|
+
### 4d. Biometric gate (host responsibility)
|
|
193
|
+
|
|
194
|
+
Best mobile UX is "session lives forever, Face ID unlocks the UI on cold start".
|
|
195
|
+
That belongs in the host app (e.g.
|
|
196
|
+
[`@capgo/capacitor-native-biometric`](https://github.com/Cap-go/capacitor-native-biometric)) —
|
|
197
|
+
gate your protected routes behind a biometric prompt and leave the SmartLinks
|
|
198
|
+
session alone.
|
|
199
|
+
|
|
200
|
+
### 4e. Deep links (magic-link & email verify)
|
|
201
|
+
|
|
202
|
+
Magic-link and email-verification URLs must open the app, not Safari/Chrome.
|
|
203
|
+
Configure your custom scheme + Universal/App Links in `capacitor.config.ts`,
|
|
204
|
+
then forward the URL into the SDK:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { App } from '@capacitor/app';
|
|
208
|
+
|
|
209
|
+
App.addListener('appUrlOpen', ({ url }) => {
|
|
210
|
+
// Strip the scheme/host and replace the WebView's hash so the kit's
|
|
211
|
+
// existing deep-link detector (mode=verifyEmail|magicLink, token=...) picks it up.
|
|
212
|
+
const u = new URL(url);
|
|
213
|
+
window.location.hash = u.hash || `#${u.pathname}${u.search}`;
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 5. Backend: long-lived tokens & refresh
|
|
220
|
+
|
|
221
|
+
The kit's `tokenStorage` caches the JWT for 7 days by default; the real ceiling
|
|
222
|
+
is the JWT's own `exp` claim. Two backend options if you want true "never log out"
|
|
223
|
+
sessions:
|
|
224
|
+
|
|
225
|
+
1. **Issue longer JWTs for native clients.** Detect a `X-Client-Platform: native`
|
|
226
|
+
header on `/auth/*` endpoints and mint a 90-day token. Simplest path — no new
|
|
227
|
+
endpoints, no rotation logic. Trade-off: longer revocation window.
|
|
228
|
+
2. **Add a refresh-token endpoint** (recommended for production). Spec lives at
|
|
229
|
+
[`backend-reference/REFRESH_TOKEN_BACKEND_SPEC.md`](../backend-reference/REFRESH_TOKEN_BACKEND_SPEC.md).
|
|
230
|
+
The kit's `useAuth().refreshToken()` will start calling it transparently once
|
|
231
|
+
the endpoint ships.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## Apple backend (already shipped)
|
|
236
|
+
|
|
237
|
+
Sign in with Apple is delegated to `smartlinks.authKit.appleLogin()` in the SDK
|
|
238
|
+
(≥ 1.14.15) which posts to `POST /authkit/{clientId}/auth/apple`. No additional
|
|
239
|
+
work needed in this kit.
|
|
240
|
+
|
|
241
|
+
> **App Store note:** Apple's [App Store Review Guideline 4.8](https://developer.apple.com/app-store/review/guidelines/#sign-in-with-apple)
|
|
242
|
+
> requires offering Sign in with Apple when you offer Google sign-in in an iOS app,
|
|
243
|
+
> which is the main reason Apple support is bundled here.
|
|
244
|
+
|