@mspkapps/auth-client 0.1.15 → 0.1.17
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 +2076 -58
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,88 +1,2106 @@
|
|
|
1
|
-
# Auth
|
|
1
|
+
# Auth Server Demo - Complete Documentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A beginner-friendly guide to implementing authentication using the `@mspkapps/auth-client` package with **React Web** and **React Native**.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
- [Overview](#overview)
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [React Web](#react-web-setup)
|
|
9
|
+
- [React Native](#react-native-setup)
|
|
10
|
+
- [Setup](#setup)
|
|
11
|
+
- [React Web](#react-web-1)
|
|
12
|
+
- [React Native](#react-native-1)
|
|
13
|
+
- [Available Functions](#available-functions)
|
|
14
|
+
- [Implementation Examples](#implementation-examples)
|
|
15
|
+
- [React Web Examples](#react-web-examples)
|
|
16
|
+
- [React Native Examples](#react-native-examples)
|
|
17
|
+
- [Common Patterns](#common-patterns)
|
|
18
|
+
- [Troubleshooting](#troubleshooting)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Overview
|
|
23
|
+
|
|
24
|
+
This demo project showcases how to integrate the `@mspkapps/auth-client` authentication package in a React application. It includes:
|
|
25
|
+
|
|
26
|
+
- ✅ Email/Password Authentication
|
|
27
|
+
- ✅ Google Sign-In (OAuth)
|
|
28
|
+
- ✅ User Registration
|
|
29
|
+
- ✅ Password Reset Flow
|
|
30
|
+
- ✅ Email Verification
|
|
31
|
+
- ✅ Account Management (Delete Account)
|
|
32
|
+
- ✅ Protected User Profile
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
### React Web Setup
|
|
39
|
+
|
|
40
|
+
#### 1. Create a New React Project (with Vite)
|
|
4
41
|
|
|
5
|
-
## Install
|
|
6
42
|
```bash
|
|
7
|
-
npm
|
|
43
|
+
npm create vite@latest my-auth-app -- --template react
|
|
44
|
+
cd my-auth-app
|
|
8
45
|
```
|
|
9
46
|
|
|
10
|
-
|
|
11
|
-
```javascript
|
|
12
|
-
import { AuthClient } from '@mspk-apps/auth-client';
|
|
13
|
-
const auth = new AuthClient({ baseUrl: 'https://api.mspkapps.in/api/v1', apiKey: 'PUBLIC_KEY' });
|
|
47
|
+
#### 2. Install Required Dependencies
|
|
14
48
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
49
|
+
```bash
|
|
50
|
+
npm install @mspkapps/auth-client @react-oauth/google
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### React Native Setup
|
|
54
|
+
|
|
55
|
+
#### 1. Create a New React Native Project
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Using Expo (recommended for beginners)
|
|
59
|
+
npx create-expo-app my-auth-app
|
|
60
|
+
cd my-auth-app
|
|
61
|
+
|
|
62
|
+
# Or using React Native CLI
|
|
63
|
+
npx react-native init my-auth-app
|
|
64
|
+
cd my-auth-app
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
#### 2. Install Required Dependencies
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# For Expo
|
|
71
|
+
npx expo install @mspkapps/auth-client @react-native-google-signin/google-signin react-native-async-storage
|
|
72
|
+
|
|
73
|
+
# For React Native CLI
|
|
74
|
+
npm install @mspkapps/auth-client @react-native-google-signin/google-signin react-native-async-storage
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
#### 3. Configure Google Sign-In (Expo)
|
|
78
|
+
|
|
79
|
+
Add to your `app.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"expo": {
|
|
84
|
+
"plugins": [
|
|
85
|
+
[
|
|
86
|
+
"@react-native-google-signin/google-signin",
|
|
87
|
+
{
|
|
88
|
+
"androidClientId": "YOUR_ANDROID_CLIENT_ID.apps.googleusercontent.com",
|
|
89
|
+
"iosClientId": "YOUR_IOS_CLIENT_ID.apps.googleusercontent.com"
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
]
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Then run:
|
|
98
|
+
```bash
|
|
99
|
+
npx expo prebuild
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Setup
|
|
105
|
+
|
|
106
|
+
### React Web
|
|
107
|
+
|
|
108
|
+
#### Step 1: Create Authentication Context
|
|
109
|
+
|
|
110
|
+
Create `src/AuthContext.jsx` to manage authentication state across your app:
|
|
111
|
+
|
|
112
|
+
```jsx
|
|
113
|
+
import React, { createContext, useContext, useCallback, useState, useMemo } from 'react';
|
|
114
|
+
import { AuthClient } from '@mspkapps/auth-client';
|
|
115
|
+
|
|
116
|
+
// Replace these with your actual API keys
|
|
117
|
+
const PUBLIC_KEY = 'your_public_api_key_here';
|
|
118
|
+
const API_SECRET = 'your_api_secret_here';
|
|
119
|
+
|
|
120
|
+
// Initialize the auth client
|
|
121
|
+
// Note: baseUrl is already configured in the package, no need to override it
|
|
122
|
+
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET, {
|
|
123
|
+
keyInPath: true, // Use API key in URL path
|
|
124
|
+
storage: null, // Keep token in memory only (more secure)
|
|
125
|
+
fetch: (input, init = {}) => {
|
|
126
|
+
// Add required headers to all requests
|
|
127
|
+
const headers = new Headers(init.headers || {});
|
|
128
|
+
if (!headers.has('X-API-Key')) headers.set('X-API-Key', PUBLIC_KEY);
|
|
129
|
+
if (!headers.has('X-API-Secret')) headers.set('X-API-Secret', API_SECRET);
|
|
130
|
+
|
|
131
|
+
// Convert token format from "UserToken" to "Bearer" (if needed by your server)
|
|
132
|
+
if (headers.has('Authorization')) {
|
|
133
|
+
const authHeader = headers.get('Authorization');
|
|
134
|
+
if (authHeader.startsWith('UserToken ')) {
|
|
135
|
+
headers.set('Authorization', 'Bearer ' + authHeader.substring(10));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return window.fetch(input, { ...init, headers });
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const AuthContext = createContext(null);
|
|
144
|
+
|
|
145
|
+
export function AuthProvider({ children }) {
|
|
146
|
+
const [user, setUser] = useState(null);
|
|
147
|
+
const [token, setToken] = useState(null);
|
|
148
|
+
const [loading, setLoading] = useState(false);
|
|
149
|
+
const [error, setError] = useState(null);
|
|
150
|
+
|
|
151
|
+
// Email/Password Login
|
|
152
|
+
const login = useCallback(async ({ email, password }) => {
|
|
153
|
+
setError(null);
|
|
154
|
+
setLoading(true);
|
|
155
|
+
try {
|
|
156
|
+
const res = await authClient.login({ email, password });
|
|
157
|
+
const token = res?.data?.access_token;
|
|
158
|
+
if (token) {
|
|
159
|
+
authClient.setToken(token);
|
|
160
|
+
}
|
|
161
|
+
setUser(res?.data?.user || null);
|
|
162
|
+
setToken(authClient.token);
|
|
163
|
+
return res?.data?.user || null;
|
|
164
|
+
} catch (err) {
|
|
165
|
+
setError(err);
|
|
166
|
+
throw err;
|
|
167
|
+
} finally {
|
|
168
|
+
setLoading(false);
|
|
169
|
+
}
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
// Google Sign-In
|
|
173
|
+
const googleLogin = useCallback(async (idToken) => {
|
|
174
|
+
setError(null);
|
|
175
|
+
setLoading(true);
|
|
176
|
+
try {
|
|
177
|
+
const res = await authClient.googleAuth({ id_token: idToken });
|
|
178
|
+
const token = res?.data?.user_token || res?.data?.access_token;
|
|
179
|
+
if (token) {
|
|
180
|
+
authClient.setToken(token);
|
|
181
|
+
}
|
|
182
|
+
setUser(res?.data?.user || null);
|
|
183
|
+
setToken(authClient.token || token);
|
|
184
|
+
return { user: res?.data?.user, isNewUser: res?.data?.is_new_user };
|
|
185
|
+
} catch (err) {
|
|
186
|
+
setError(err);
|
|
187
|
+
throw err;
|
|
188
|
+
} finally {
|
|
189
|
+
setLoading(false);
|
|
190
|
+
}
|
|
191
|
+
}, []);
|
|
192
|
+
|
|
193
|
+
// Logout
|
|
194
|
+
const logout = useCallback(() => {
|
|
195
|
+
setUser(null);
|
|
196
|
+
setToken(null);
|
|
197
|
+
authClient.token = null;
|
|
198
|
+
}, []);
|
|
199
|
+
|
|
200
|
+
// Refresh User Profile
|
|
201
|
+
const refreshProfile = useCallback(async () => {
|
|
202
|
+
if (!token) return;
|
|
203
|
+
setLoading(true);
|
|
204
|
+
setError(null);
|
|
205
|
+
try {
|
|
206
|
+
const res = await authClient.getProfile();
|
|
207
|
+
setUser(res.data.user);
|
|
208
|
+
return res.data.user;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
setError(err);
|
|
211
|
+
throw err;
|
|
212
|
+
} finally {
|
|
213
|
+
setLoading(false);
|
|
214
|
+
}
|
|
215
|
+
}, [token]);
|
|
216
|
+
|
|
217
|
+
const value = useMemo(
|
|
218
|
+
() => ({ authClient, user, token, login, googleLogin, logout, refreshProfile, loading, error }),
|
|
219
|
+
[user, token, login, googleLogin, logout, refreshProfile, loading, error]
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Custom hook to use auth context
|
|
226
|
+
export function useAuth() {
|
|
227
|
+
const ctx = useContext(AuthContext);
|
|
228
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
229
|
+
return ctx;
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### React Native
|
|
234
|
+
|
|
235
|
+
#### Step 1: Create Authentication Context
|
|
236
|
+
|
|
237
|
+
Create `src/AuthContext.js` with AsyncStorage support for React Native:
|
|
238
|
+
|
|
239
|
+
```jsx
|
|
240
|
+
import React, { createContext, useContext, useCallback, useState, useMemo } from 'react';
|
|
241
|
+
import { AuthClient } from '@mspkapps/auth-client';
|
|
242
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
243
|
+
|
|
244
|
+
// Replace these with your actual API keys
|
|
245
|
+
const PUBLIC_KEY = 'your_public_api_key_here';
|
|
246
|
+
const API_SECRET = 'your_api_secret_here';
|
|
247
|
+
|
|
248
|
+
// Initialize the auth client with AsyncStorage for React Native
|
|
249
|
+
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET, {
|
|
250
|
+
keyInPath: true,
|
|
251
|
+
storage: AsyncStorage, // Use AsyncStorage instead of localStorage
|
|
252
|
+
fetch: (input, init = {}) => {
|
|
253
|
+
const headers = new Headers(init.headers || {});
|
|
254
|
+
if (!headers.has('X-API-Key')) headers.set('X-API-Key', PUBLIC_KEY);
|
|
255
|
+
if (!headers.has('X-API-Secret')) headers.set('X-API-Secret', API_SECRET);
|
|
256
|
+
|
|
257
|
+
if (headers.has('Authorization')) {
|
|
258
|
+
const authHeader = headers.get('Authorization');
|
|
259
|
+
if (authHeader.startsWith('UserToken ')) {
|
|
260
|
+
headers.set('Authorization', 'Bearer ' + authHeader.substring(10));
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Use global fetch available in React Native
|
|
265
|
+
return fetch(input, { ...init, headers });
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const AuthContext = createContext(null);
|
|
270
|
+
|
|
271
|
+
export function AuthProvider({ children }) {
|
|
272
|
+
const [user, setUser] = useState(null);
|
|
273
|
+
const [token, setToken] = useState(null);
|
|
274
|
+
const [loading, setLoading] = useState(false);
|
|
275
|
+
const [error, setError] = useState(null);
|
|
276
|
+
const [isReady, setIsReady] = useState(false);
|
|
277
|
+
|
|
278
|
+
// Load token from storage on mount
|
|
279
|
+
React.useEffect(() => {
|
|
280
|
+
const bootstrapAsync = async () => {
|
|
281
|
+
try {
|
|
282
|
+
const savedToken = await AsyncStorage.getItem('auth_user_token');
|
|
283
|
+
if (savedToken) {
|
|
284
|
+
authClient.setToken(savedToken);
|
|
285
|
+
setToken(savedToken);
|
|
286
|
+
|
|
287
|
+
// Fetch user profile
|
|
288
|
+
try {
|
|
289
|
+
const res = await authClient.getProfile();
|
|
290
|
+
setUser(res.data.user);
|
|
291
|
+
} catch (err) {
|
|
292
|
+
// Token might be invalid
|
|
293
|
+
await AsyncStorage.removeItem('auth_user_token');
|
|
294
|
+
authClient.setToken(null);
|
|
295
|
+
setToken(null);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (err) {
|
|
299
|
+
console.error('Failed to restore token', err);
|
|
300
|
+
} finally {
|
|
301
|
+
setIsReady(true);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
bootstrapAsync();
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const login = useCallback(async ({ email, password }) => {
|
|
309
|
+
setError(null);
|
|
310
|
+
setLoading(true);
|
|
311
|
+
try {
|
|
312
|
+
const res = await authClient.login({ email, password });
|
|
313
|
+
const token = res?.data?.access_token;
|
|
314
|
+
if (token) {
|
|
315
|
+
authClient.setToken(token);
|
|
316
|
+
}
|
|
317
|
+
setUser(res?.data?.user || null);
|
|
318
|
+
setToken(authClient.token);
|
|
319
|
+
return res?.data?.user || null;
|
|
320
|
+
} catch (err) {
|
|
321
|
+
setError(err);
|
|
322
|
+
throw err;
|
|
323
|
+
} finally {
|
|
324
|
+
setLoading(false);
|
|
325
|
+
}
|
|
326
|
+
}, []);
|
|
327
|
+
|
|
328
|
+
const googleLogin = useCallback(async (idToken) => {
|
|
329
|
+
setError(null);
|
|
330
|
+
setLoading(true);
|
|
331
|
+
try {
|
|
332
|
+
const res = await authClient.googleAuth({ id_token: idToken });
|
|
333
|
+
const token = res?.data?.user_token || res?.data?.access_token;
|
|
334
|
+
if (token) {
|
|
335
|
+
authClient.setToken(token);
|
|
336
|
+
}
|
|
337
|
+
setUser(res?.data?.user || null);
|
|
338
|
+
setToken(authClient.token || token);
|
|
339
|
+
return { user: res?.data?.user, isNewUser: res?.data?.is_new_user };
|
|
340
|
+
} catch (err) {
|
|
341
|
+
setError(err);
|
|
342
|
+
throw err;
|
|
343
|
+
} finally {
|
|
344
|
+
setLoading(false);
|
|
345
|
+
}
|
|
346
|
+
}, []);
|
|
347
|
+
|
|
348
|
+
const logout = useCallback(async () => {
|
|
349
|
+
setUser(null);
|
|
350
|
+
setToken(null);
|
|
351
|
+
authClient.token = null;
|
|
352
|
+
await AsyncStorage.removeItem('auth_user_token');
|
|
353
|
+
}, []);
|
|
354
|
+
|
|
355
|
+
const refreshProfile = useCallback(async () => {
|
|
356
|
+
if (!token) return;
|
|
357
|
+
setLoading(true);
|
|
358
|
+
setError(null);
|
|
359
|
+
try {
|
|
360
|
+
const res = await authClient.getProfile();
|
|
361
|
+
setUser(res.data.user);
|
|
362
|
+
return res.data.user;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
setError(err);
|
|
365
|
+
throw err;
|
|
366
|
+
} finally {
|
|
367
|
+
setLoading(false);
|
|
368
|
+
}
|
|
369
|
+
}, [token]);
|
|
370
|
+
|
|
371
|
+
const value = useMemo(
|
|
372
|
+
() => ({ authClient, user, token, login, googleLogin, logout, refreshProfile, loading, error, isReady }),
|
|
373
|
+
[user, token, login, googleLogin, logout, refreshProfile, loading, error, isReady]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
return (
|
|
377
|
+
<AuthContext.Provider value={value}>
|
|
378
|
+
{isReady ? children : null}
|
|
379
|
+
</AuthContext.Provider>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function useAuth() {
|
|
384
|
+
const ctx = useContext(AuthContext);
|
|
385
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
|
|
386
|
+
return ctx;
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
#### Step 2: Setup Navigation
|
|
391
|
+
|
|
392
|
+
Create `src/Navigation.js` for handling navigation between Login and Home:
|
|
393
|
+
|
|
394
|
+
```jsx
|
|
395
|
+
import React from 'react';
|
|
396
|
+
import { NavigationContainer } from '@react-navigation/native';
|
|
397
|
+
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
398
|
+
import { useAuth } from './AuthContext';
|
|
399
|
+
import LoginScreen from './screens/LoginScreen';
|
|
400
|
+
import RegisterScreen from './screens/RegisterScreen';
|
|
401
|
+
import HomeScreen from './screens/HomeScreen';
|
|
402
|
+
import SplashScreen from './screens/SplashScreen';
|
|
403
|
+
|
|
404
|
+
const Stack = createNativeStackNavigator();
|
|
405
|
+
|
|
406
|
+
export function RootNavigator() {
|
|
407
|
+
const { user, isReady } = useAuth();
|
|
408
|
+
|
|
409
|
+
if (!isReady) {
|
|
410
|
+
return <SplashScreen />;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<NavigationContainer>
|
|
415
|
+
<Stack.Navigator
|
|
416
|
+
screenOptions={{ headerShown: false }}
|
|
417
|
+
>
|
|
418
|
+
{user ? (
|
|
419
|
+
<Stack.Screen name="Home" component={HomeScreen} />
|
|
420
|
+
) : (
|
|
421
|
+
<Stack.Group screenOptions={{ animationEnabled: false }}>
|
|
422
|
+
<Stack.Screen name="Auth" component={AuthNavigator} />
|
|
423
|
+
</Stack.Group>
|
|
424
|
+
)}
|
|
425
|
+
</Stack.Navigator>
|
|
426
|
+
</NavigationContainer>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function AuthNavigator() {
|
|
431
|
+
const [showRegister, setShowRegister] = React.useState(false);
|
|
432
|
+
|
|
433
|
+
return (
|
|
434
|
+
<Stack.Navigator
|
|
435
|
+
screenOptions={{
|
|
436
|
+
headerShown: true,
|
|
437
|
+
animationEnabled: false,
|
|
438
|
+
}}
|
|
439
|
+
>
|
|
440
|
+
{showRegister ? (
|
|
441
|
+
<Stack.Screen
|
|
442
|
+
name="Register"
|
|
443
|
+
component={RegisterScreen}
|
|
444
|
+
options={{
|
|
445
|
+
title: 'Sign Up',
|
|
446
|
+
headerLeft: () => (
|
|
447
|
+
<TouchableOpacity onPress={() => setShowRegister(false)}>
|
|
448
|
+
<Text>Back</Text>
|
|
449
|
+
</TouchableOpacity>
|
|
450
|
+
),
|
|
451
|
+
}}
|
|
452
|
+
/>
|
|
453
|
+
) : (
|
|
454
|
+
<Stack.Screen
|
|
455
|
+
name="Login"
|
|
456
|
+
component={LoginScreen}
|
|
457
|
+
options={{
|
|
458
|
+
title: 'Login',
|
|
459
|
+
headerRight: () => (
|
|
460
|
+
<TouchableOpacity onPress={() => setShowRegister(true)}>
|
|
461
|
+
<Text>Sign Up</Text>
|
|
462
|
+
</TouchableOpacity>
|
|
463
|
+
),
|
|
464
|
+
}}
|
|
465
|
+
/>
|
|
466
|
+
)}
|
|
467
|
+
</Stack.Navigator>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### Step 3: Setup Root App Component
|
|
473
|
+
|
|
474
|
+
Update `App.js` (or `App.jsx`):
|
|
475
|
+
|
|
476
|
+
```jsx
|
|
477
|
+
import React from 'react';
|
|
478
|
+
import { AuthProvider } from './src/AuthContext';
|
|
479
|
+
import { RootNavigator } from './src/Navigation';
|
|
480
|
+
|
|
481
|
+
export default function App() {
|
|
482
|
+
return (
|
|
483
|
+
<AuthProvider>
|
|
484
|
+
<RootNavigator />
|
|
485
|
+
</AuthProvider>
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Update `src/main.jsx` to wrap your app with both AuthProvider and GoogleOAuthProvider:
|
|
491
|
+
|
|
492
|
+
```jsx
|
|
493
|
+
import React from 'react';
|
|
494
|
+
import ReactDOM from 'react-dom/client';
|
|
495
|
+
import App from './App.jsx';
|
|
496
|
+
import { AuthProvider } from './AuthContext.jsx';
|
|
497
|
+
import { GoogleOAuthProvider } from '@react-oauth/google';
|
|
498
|
+
import './index.css';
|
|
499
|
+
|
|
500
|
+
// Replace with your Google Client ID from Google Cloud Console
|
|
501
|
+
const GOOGLE_CLIENT_ID = 'your_google_client_id_here';
|
|
502
|
+
|
|
503
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
504
|
+
<React.StrictMode>
|
|
505
|
+
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
|
506
|
+
<AuthProvider>
|
|
507
|
+
<App />
|
|
508
|
+
</AuthProvider>
|
|
509
|
+
</GoogleOAuthProvider>
|
|
510
|
+
</React.StrictMode>
|
|
511
|
+
);
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### Step 3: Create Main App Component
|
|
515
|
+
|
|
516
|
+
Create `src/App.jsx` to handle routing between Login and Home:
|
|
517
|
+
|
|
518
|
+
```jsx
|
|
519
|
+
import React, { useState } from 'react';
|
|
520
|
+
import { useAuth } from './AuthContext.jsx';
|
|
521
|
+
import Login from './components/Login.jsx';
|
|
522
|
+
import Register from './components/Register.jsx';
|
|
523
|
+
import Home from './components/Home.jsx';
|
|
524
|
+
import './App.css';
|
|
525
|
+
|
|
526
|
+
export default function App() {
|
|
527
|
+
const { user } = useAuth();
|
|
528
|
+
const [showRegister, setShowRegister] = useState(false);
|
|
529
|
+
|
|
530
|
+
// If user is logged in, show Home page
|
|
531
|
+
if (user) {
|
|
532
|
+
return <Home />;
|
|
21
533
|
}
|
|
534
|
+
|
|
535
|
+
// Otherwise, show Login or Register page
|
|
536
|
+
return (
|
|
537
|
+
<div className="app-container">
|
|
538
|
+
<div className="auth-toggle">
|
|
539
|
+
<button
|
|
540
|
+
onClick={() => setShowRegister(false)}
|
|
541
|
+
className={!showRegister ? 'active' : ''}
|
|
542
|
+
>
|
|
543
|
+
Login
|
|
544
|
+
</button>
|
|
545
|
+
<button
|
|
546
|
+
onClick={() => setShowRegister(true)}
|
|
547
|
+
className={showRegister ? 'active' : ''}
|
|
548
|
+
>
|
|
549
|
+
Sign Up
|
|
550
|
+
</button>
|
|
551
|
+
</div>
|
|
552
|
+
{showRegister ? <Register /> : <Login />}
|
|
553
|
+
</div>
|
|
554
|
+
);
|
|
22
555
|
}
|
|
23
556
|
```
|
|
24
557
|
|
|
25
|
-
|
|
558
|
+
---
|
|
559
|
+
|
|
560
|
+
## Available Functions
|
|
561
|
+
|
|
562
|
+
### Core Authentication Methods
|
|
563
|
+
|
|
564
|
+
All functions are accessed through the `authClient` object or the `useAuth()` hook.
|
|
565
|
+
|
|
566
|
+
#### 1. **register** - Create a new user account
|
|
567
|
+
|
|
568
|
+
```javascript
|
|
569
|
+
await authClient.register({
|
|
570
|
+
email: 'user@example.com',
|
|
571
|
+
username: 'johndoe', // Optional
|
|
572
|
+
password: 'securePass123',
|
|
573
|
+
name: 'John Doe' // Optional
|
|
574
|
+
});
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Parameters:**
|
|
578
|
+
- `email` (required): User's email address
|
|
579
|
+
- `username` (optional): Unique username
|
|
580
|
+
- `password` (required): User's password
|
|
581
|
+
- `name` (optional): User's display name
|
|
582
|
+
|
|
583
|
+
**Response:** Returns user data and automatically sets authentication token.
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
#### 2. **login** - Login with email/username and password
|
|
26
588
|
|
|
27
|
-
## Node (Server-side proxy)
|
|
28
589
|
```javascript
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
590
|
+
await authClient.login({
|
|
591
|
+
email: 'user@example.com',
|
|
592
|
+
password: 'securePass123'
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
// Or login with username
|
|
596
|
+
await authClient.login({
|
|
597
|
+
username: 'johndoe',
|
|
598
|
+
password: 'securePass123'
|
|
34
599
|
});
|
|
35
600
|
```
|
|
36
601
|
|
|
37
|
-
|
|
38
|
-
- `
|
|
39
|
-
- `
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
602
|
+
**Parameters:**
|
|
603
|
+
- `email` OR `username` (required): User identifier
|
|
604
|
+
- `password` (required): User's password
|
|
605
|
+
|
|
606
|
+
**Response:** Returns user data and authentication token.
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
#### 3. **googleAuth** - Login/Register with Google Sign-In
|
|
611
|
+
|
|
612
|
+
```javascript
|
|
613
|
+
await authClient.googleAuth({
|
|
614
|
+
id_token: 'google_id_token_here'
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Alternative: use access_token
|
|
618
|
+
await authClient.googleAuth({
|
|
619
|
+
access_token: 'google_access_token_here'
|
|
620
|
+
});
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Parameters:**
|
|
624
|
+
- `id_token` (required if no access_token): Google ID token from credential response
|
|
625
|
+
- `access_token` (alternative): Google access token
|
|
626
|
+
|
|
627
|
+
**Response:** Returns user data, token, and `is_new_user` flag (true if this is first login).
|
|
628
|
+
|
|
629
|
+
**Note:** Use with `@react-oauth/google` package for the Google Sign-In button.
|
|
630
|
+
|
|
631
|
+
---
|
|
632
|
+
|
|
633
|
+
#### 4. **logout** - Clear authentication token
|
|
43
634
|
|
|
44
|
-
## Error Handling
|
|
45
|
-
Errors throw `AuthError`:
|
|
46
635
|
```javascript
|
|
636
|
+
authClient.logout();
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Parameters:** None
|
|
640
|
+
|
|
641
|
+
**Response:** Clears the stored token.
|
|
642
|
+
|
|
643
|
+
---
|
|
644
|
+
|
|
645
|
+
#### 5. **getProfile** - Get current user's profile
|
|
646
|
+
|
|
647
|
+
```javascript
|
|
648
|
+
const response = await authClient.getProfile();
|
|
649
|
+
console.log(response.data.user);
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Parameters:** None (requires authentication token)
|
|
653
|
+
|
|
654
|
+
**Response:** Returns current user's profile data.
|
|
655
|
+
|
|
656
|
+
---
|
|
657
|
+
|
|
658
|
+
### Password Management
|
|
659
|
+
|
|
660
|
+
#### 6. **requestPasswordReset** - Send password reset email
|
|
661
|
+
|
|
662
|
+
```javascript
|
|
663
|
+
await authClient.requestPasswordReset({
|
|
664
|
+
email: 'user@example.com'
|
|
665
|
+
});
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Parameters:**
|
|
669
|
+
- `email` (required): Email address to send reset link
|
|
670
|
+
|
|
671
|
+
**Response:** Success message confirming email was sent.
|
|
672
|
+
|
|
673
|
+
**Use Case:** When user forgot their password on login page.
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
#### 7. **requestChangePasswordLink** - Send password change email for logged-in users
|
|
678
|
+
|
|
679
|
+
```javascript
|
|
680
|
+
await authClient.requestChangePasswordLink({
|
|
681
|
+
email: 'user@example.com'
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
**Parameters:**
|
|
686
|
+
- `email` (required): User's email address
|
|
687
|
+
|
|
688
|
+
**Response:** Success message confirming email was sent.
|
|
689
|
+
|
|
690
|
+
**Use Case:** When authenticated user wants to change their password from settings/profile page.
|
|
691
|
+
|
|
692
|
+
---
|
|
693
|
+
|
|
694
|
+
#### 8. **sendGoogleUserSetPasswordEmail** - Send password setup email for Google users
|
|
695
|
+
|
|
696
|
+
```javascript
|
|
697
|
+
await authClient.sendGoogleUserSetPasswordEmail({
|
|
698
|
+
email: 'user@example.com'
|
|
699
|
+
});
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
**Parameters:**
|
|
703
|
+
- `email` (required): Google user's email address
|
|
704
|
+
|
|
705
|
+
**Response:** Success message confirming email was sent.
|
|
706
|
+
|
|
707
|
+
**Use Case:** When a user who registered via Google Sign-In wants to set a password for traditional login.
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
### Email Verification
|
|
712
|
+
|
|
713
|
+
#### 9. **resendVerificationEmail** - Resend email verification
|
|
714
|
+
|
|
715
|
+
```javascript
|
|
716
|
+
await authClient.resendVerificationEmail({
|
|
717
|
+
email: 'user@example.com',
|
|
718
|
+
purpose: 'New Account' // or 'Account Recovery'
|
|
719
|
+
});
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
**Parameters:**
|
|
723
|
+
- `email` (required): Email address to send verification
|
|
724
|
+
- `purpose` (optional): Purpose of verification (e.g., 'New Account')
|
|
725
|
+
|
|
726
|
+
**Response:** Success message confirming email was sent.
|
|
727
|
+
|
|
728
|
+
**Use Case:** When user's account is not verified and they need a new verification email.
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
732
|
+
### Account Management
|
|
733
|
+
|
|
734
|
+
#### 10. **deleteAccount** - Permanently delete user account
|
|
735
|
+
|
|
736
|
+
```javascript
|
|
737
|
+
await authClient.deleteAccount({
|
|
738
|
+
email: 'user@example.com',
|
|
739
|
+
password: 'userPassword123' // Optional, depending on server requirements
|
|
740
|
+
});
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**Parameters:**
|
|
744
|
+
- `email` (required): Email of account to delete
|
|
745
|
+
- `password` (optional): User's password for confirmation
|
|
746
|
+
|
|
747
|
+
**Response:** Success message. Token should be cleared after deletion.
|
|
748
|
+
|
|
749
|
+
**Warning:** This action is permanent and cannot be undone!
|
|
750
|
+
|
|
751
|
+
---
|
|
752
|
+
|
|
753
|
+
### Advanced Usage
|
|
754
|
+
|
|
755
|
+
#### 11. **authed** - Make custom authenticated API calls
|
|
756
|
+
|
|
757
|
+
```javascript
|
|
758
|
+
const response = await authClient.authed('custom/endpoint', {
|
|
759
|
+
method: 'POST',
|
|
760
|
+
body: { key: 'value' },
|
|
761
|
+
headers: { 'Custom-Header': 'value' }
|
|
762
|
+
});
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Parameters:**
|
|
766
|
+
- `path` (required): API endpoint path (without base URL)
|
|
767
|
+
- `options` (optional): Request options
|
|
768
|
+
- `method`: HTTP method (GET, POST, PUT, DELETE, etc.)
|
|
769
|
+
- `body`: Request body object
|
|
770
|
+
- `headers`: Additional headers
|
|
771
|
+
|
|
772
|
+
**Response:** Response data from custom endpoint.
|
|
773
|
+
|
|
774
|
+
**Use Case:** For custom endpoints not covered by the built-in methods.
|
|
775
|
+
|
|
776
|
+
---
|
|
777
|
+
|
|
778
|
+
## Implementation Examples
|
|
779
|
+
|
|
780
|
+
### React Web Examples
|
|
781
|
+
|
|
782
|
+
### Complete Login Component
|
|
783
|
+
|
|
784
|
+
Create `src/components/Login.jsx`:
|
|
785
|
+
|
|
786
|
+
```jsx
|
|
787
|
+
import React, { useState } from 'react';
|
|
788
|
+
import { useAuth } from '../AuthContext.jsx';
|
|
789
|
+
import { GoogleLogin } from '@react-oauth/google';
|
|
790
|
+
|
|
791
|
+
export default function Login() {
|
|
792
|
+
const { login, googleLogin, authClient } = useAuth();
|
|
793
|
+
|
|
794
|
+
// Form state
|
|
795
|
+
const [email, setEmail] = useState('');
|
|
796
|
+
const [password, setPassword] = useState('');
|
|
797
|
+
const [busy, setBusy] = useState(false);
|
|
798
|
+
const [error, setError] = useState(null);
|
|
799
|
+
|
|
800
|
+
// Password reset state
|
|
801
|
+
const [showReset, setShowReset] = useState(false);
|
|
802
|
+
const [resetEmail, setResetEmail] = useState('');
|
|
803
|
+
const [resetFeedback, setResetFeedback] = useState(null);
|
|
804
|
+
|
|
805
|
+
// Google user set password state
|
|
806
|
+
const [showGoogleSetPassword, setShowGoogleSetPassword] = useState(false);
|
|
807
|
+
const [googleSetPasswordEmail, setGoogleSetPasswordEmail] = useState('');
|
|
808
|
+
const [googleSetPasswordFeedback, setGoogleSetPasswordFeedback] = useState(null);
|
|
809
|
+
|
|
810
|
+
// Email verification state
|
|
811
|
+
const [resendFeedback, setResendFeedback] = useState(null);
|
|
812
|
+
|
|
813
|
+
// Handle standard email/password login
|
|
814
|
+
async function handleSubmit(e) {
|
|
815
|
+
e.preventDefault();
|
|
816
|
+
setError(null);
|
|
817
|
+
setResendFeedback(null);
|
|
818
|
+
setBusy(true);
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
await login({ email, password });
|
|
822
|
+
// Success - user will be redirected by App.jsx
|
|
823
|
+
} catch (err) {
|
|
824
|
+
setError(err.message || 'Login failed');
|
|
825
|
+
|
|
826
|
+
// Check if account needs verification
|
|
827
|
+
if (
|
|
828
|
+
err.code === 'EMAIL_NOT_VERIFIED' ||
|
|
829
|
+
err.message?.toLowerCase().includes('not been verified')
|
|
830
|
+
) {
|
|
831
|
+
setResendFeedback('Your account has not been verified. Click below to resend verification.');
|
|
832
|
+
}
|
|
833
|
+
} finally {
|
|
834
|
+
setBusy(false);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Handle Google Sign-In
|
|
839
|
+
async function handleGoogleSuccess(credentialResponse) {
|
|
840
|
+
setBusy(true);
|
|
841
|
+
try {
|
|
842
|
+
const result = await googleLogin(credentialResponse.credential);
|
|
843
|
+
if (result.isNewUser) {
|
|
844
|
+
console.log('Welcome, new user!');
|
|
845
|
+
}
|
|
846
|
+
} catch (err) {
|
|
847
|
+
setError(err.message || 'Google login failed');
|
|
848
|
+
} finally {
|
|
849
|
+
setBusy(false);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
// Handle password reset request
|
|
854
|
+
async function handlePasswordReset(e) {
|
|
855
|
+
e.preventDefault();
|
|
856
|
+
setBusy(true);
|
|
857
|
+
setResetFeedback(null);
|
|
858
|
+
|
|
859
|
+
try {
|
|
860
|
+
await authClient.requestPasswordReset({ email: resetEmail });
|
|
861
|
+
setResetFeedback('Password reset email sent. Check your inbox.');
|
|
862
|
+
setResetEmail('');
|
|
863
|
+
} catch (err) {
|
|
864
|
+
setResetFeedback(err.message || 'Failed to send reset email');
|
|
865
|
+
} finally {
|
|
866
|
+
setBusy(false);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Handle resend verification email
|
|
871
|
+
async function handleResendVerification() {
|
|
872
|
+
setBusy(true);
|
|
873
|
+
setResendFeedback(null);
|
|
874
|
+
|
|
875
|
+
try {
|
|
876
|
+
await authClient.resendVerificationEmail({
|
|
877
|
+
email,
|
|
878
|
+
purpose: 'New Account'
|
|
879
|
+
});
|
|
880
|
+
setResendFeedback('Verification email sent. Please check your inbox.');
|
|
881
|
+
} catch (err) {
|
|
882
|
+
setResendFeedback(err.message || 'Failed to resend verification');
|
|
883
|
+
} finally {
|
|
884
|
+
setBusy(false);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Handle Google user set password email
|
|
889
|
+
async function handleSendGoogleSetPasswordEmail(e) {
|
|
890
|
+
e.preventDefault();
|
|
891
|
+
setBusy(true);
|
|
892
|
+
setGoogleSetPasswordFeedback(null);
|
|
893
|
+
|
|
894
|
+
try {
|
|
895
|
+
await authClient.sendGoogleUserSetPasswordEmail({
|
|
896
|
+
email: googleSetPasswordEmail
|
|
897
|
+
});
|
|
898
|
+
setGoogleSetPasswordFeedback('Email sent. Check your inbox to set a password.');
|
|
899
|
+
setGoogleSetPasswordEmail('');
|
|
900
|
+
} catch (err) {
|
|
901
|
+
setGoogleSetPasswordFeedback(err.message || 'Failed to send email');
|
|
902
|
+
} finally {
|
|
903
|
+
setBusy(false);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
return (
|
|
908
|
+
<div className="login-container">
|
|
909
|
+
<h2>Login</h2>
|
|
910
|
+
|
|
911
|
+
{/* Email/Password Login Form */}
|
|
912
|
+
<form onSubmit={handleSubmit}>
|
|
913
|
+
<label>
|
|
914
|
+
Email
|
|
915
|
+
<input
|
|
916
|
+
type="email"
|
|
917
|
+
value={email}
|
|
918
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
919
|
+
required
|
|
920
|
+
/>
|
|
921
|
+
</label>
|
|
922
|
+
|
|
923
|
+
<label>
|
|
924
|
+
Password
|
|
925
|
+
<input
|
|
926
|
+
type="password"
|
|
927
|
+
value={password}
|
|
928
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
929
|
+
required
|
|
930
|
+
/>
|
|
931
|
+
</label>
|
|
932
|
+
|
|
933
|
+
<button type="submit" disabled={busy}>
|
|
934
|
+
{busy ? 'Please wait…' : 'Login'}
|
|
935
|
+
</button>
|
|
936
|
+
</form>
|
|
937
|
+
|
|
938
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
939
|
+
|
|
940
|
+
{/* Google Sign-In Button */}
|
|
941
|
+
<div style={{ marginTop: 16 }}>
|
|
942
|
+
<span>or</span>
|
|
943
|
+
<GoogleLogin
|
|
944
|
+
onSuccess={handleGoogleSuccess}
|
|
945
|
+
onError={() => setError('Google login failed')}
|
|
946
|
+
/>
|
|
947
|
+
</div>
|
|
948
|
+
|
|
949
|
+
{/* Resend Verification Email */}
|
|
950
|
+
{resendFeedback && (
|
|
951
|
+
<div>
|
|
952
|
+
<p>{resendFeedback}</p>
|
|
953
|
+
{resendFeedback.includes('has not been verified') && (
|
|
954
|
+
<button onClick={handleResendVerification} disabled={busy}>
|
|
955
|
+
Resend Verification Email
|
|
956
|
+
</button>
|
|
957
|
+
)}
|
|
958
|
+
</div>
|
|
959
|
+
)}
|
|
960
|
+
|
|
961
|
+
{/* Forgot Password */}
|
|
962
|
+
<div style={{ marginTop: 16 }}>
|
|
963
|
+
<button onClick={() => setShowReset(!showReset)}>
|
|
964
|
+
{showReset ? 'Hide' : 'Forgot password?'}
|
|
965
|
+
</button>
|
|
966
|
+
|
|
967
|
+
{showReset && (
|
|
968
|
+
<form onSubmit={handlePasswordReset}>
|
|
969
|
+
<input
|
|
970
|
+
type="email"
|
|
971
|
+
placeholder="Enter your email"
|
|
972
|
+
value={resetEmail}
|
|
973
|
+
onChange={(e) => setResetEmail(e.target.value)}
|
|
974
|
+
required
|
|
975
|
+
/>
|
|
976
|
+
<button type="submit" disabled={busy}>
|
|
977
|
+
Send Reset Link
|
|
978
|
+
</button>
|
|
979
|
+
{resetFeedback && <p>{resetFeedback}</p>}
|
|
980
|
+
</form>
|
|
981
|
+
)}
|
|
982
|
+
</div>
|
|
983
|
+
|
|
984
|
+
{/* Google User Set Password */}
|
|
985
|
+
<div style={{ marginTop: 12 }}>
|
|
986
|
+
<button onClick={() => setShowGoogleSetPassword(!showGoogleSetPassword)}>
|
|
987
|
+
{showGoogleSetPassword ? 'Hide' : 'No password? Are you a Google user?'}
|
|
988
|
+
</button>
|
|
989
|
+
|
|
990
|
+
{showGoogleSetPassword && (
|
|
991
|
+
<form onSubmit={handleSendGoogleSetPasswordEmail}>
|
|
992
|
+
<input
|
|
993
|
+
type="email"
|
|
994
|
+
placeholder="Enter your Google account email"
|
|
995
|
+
value={googleSetPasswordEmail}
|
|
996
|
+
onChange={(e) => setGoogleSetPasswordEmail(e.target.value)}
|
|
997
|
+
required
|
|
998
|
+
/>
|
|
999
|
+
<button type="submit" disabled={busy}>
|
|
1000
|
+
Send Set-Password Email
|
|
1001
|
+
</button>
|
|
1002
|
+
{googleSetPasswordFeedback && <p>{googleSetPasswordFeedback}</p>}
|
|
1003
|
+
</form>
|
|
1004
|
+
)}
|
|
1005
|
+
</div>
|
|
1006
|
+
</div>
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
---
|
|
1012
|
+
|
|
1013
|
+
### Complete Register Component
|
|
1014
|
+
|
|
1015
|
+
Create `src/components/Register.jsx`:
|
|
1016
|
+
|
|
1017
|
+
```jsx
|
|
1018
|
+
import React, { useState } from 'react';
|
|
1019
|
+
import { useAuth } from '../AuthContext.jsx';
|
|
1020
|
+
|
|
1021
|
+
export default function Register() {
|
|
1022
|
+
const { authClient } = useAuth();
|
|
1023
|
+
|
|
1024
|
+
const [email, setEmail] = useState('');
|
|
1025
|
+
const [password, setPassword] = useState('');
|
|
1026
|
+
const [name, setName] = useState('');
|
|
1027
|
+
const [username, setUsername] = useState('');
|
|
1028
|
+
const [error, setError] = useState(null);
|
|
1029
|
+
const [success, setSuccess] = useState(null);
|
|
1030
|
+
const [busy, setBusy] = useState(false);
|
|
1031
|
+
|
|
1032
|
+
async function handleSubmit(e) {
|
|
1033
|
+
e.preventDefault();
|
|
1034
|
+
setError(null);
|
|
1035
|
+
setSuccess(null);
|
|
1036
|
+
setBusy(true);
|
|
1037
|
+
|
|
1038
|
+
try {
|
|
1039
|
+
await authClient.register({
|
|
1040
|
+
email,
|
|
1041
|
+
username,
|
|
1042
|
+
password,
|
|
1043
|
+
name
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
setSuccess('Registered! Please verify your email, then login.');
|
|
1047
|
+
|
|
1048
|
+
// Clear form
|
|
1049
|
+
setEmail('');
|
|
1050
|
+
setPassword('');
|
|
1051
|
+
setName('');
|
|
1052
|
+
setUsername('');
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
setError(err.message || 'Registration failed');
|
|
1055
|
+
} finally {
|
|
1056
|
+
setBusy(false);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return (
|
|
1061
|
+
<div className="register-container">
|
|
1062
|
+
<h2>Sign Up</h2>
|
|
1063
|
+
|
|
1064
|
+
<form onSubmit={handleSubmit}>
|
|
1065
|
+
<label>
|
|
1066
|
+
Name
|
|
1067
|
+
<input
|
|
1068
|
+
type="text"
|
|
1069
|
+
value={name}
|
|
1070
|
+
onChange={(e) => setName(e.target.value)}
|
|
1071
|
+
/>
|
|
1072
|
+
</label>
|
|
1073
|
+
|
|
1074
|
+
<label>
|
|
1075
|
+
Username
|
|
1076
|
+
<input
|
|
1077
|
+
type="text"
|
|
1078
|
+
value={username}
|
|
1079
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
1080
|
+
/>
|
|
1081
|
+
</label>
|
|
1082
|
+
|
|
1083
|
+
<label>
|
|
1084
|
+
Email
|
|
1085
|
+
<input
|
|
1086
|
+
type="email"
|
|
1087
|
+
value={email}
|
|
1088
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
1089
|
+
required
|
|
1090
|
+
/>
|
|
1091
|
+
</label>
|
|
1092
|
+
|
|
1093
|
+
<label>
|
|
1094
|
+
Password
|
|
1095
|
+
<input
|
|
1096
|
+
type="password"
|
|
1097
|
+
value={password}
|
|
1098
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
1099
|
+
required
|
|
1100
|
+
/>
|
|
1101
|
+
</label>
|
|
1102
|
+
|
|
1103
|
+
<button type="submit" disabled={busy}>
|
|
1104
|
+
{busy ? 'Please wait…' : 'Create Account'}
|
|
1105
|
+
</button>
|
|
1106
|
+
</form>
|
|
1107
|
+
|
|
1108
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
1109
|
+
{success && <p style={{ color: 'green' }}>{success}</p>}
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
```
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
### Complete Home/Dashboard Component
|
|
1118
|
+
|
|
1119
|
+
Create `src/components/Home.jsx`:
|
|
1120
|
+
|
|
1121
|
+
```jsx
|
|
1122
|
+
import React, { useState } from 'react';
|
|
1123
|
+
import { useAuth } from '../AuthContext.jsx';
|
|
1124
|
+
|
|
1125
|
+
export default function Home() {
|
|
1126
|
+
const { user, token, logout, refreshProfile, loading, authClient } = useAuth();
|
|
1127
|
+
|
|
1128
|
+
const [feedback, setFeedback] = useState(null);
|
|
1129
|
+
const [busy, setBusy] = useState(false);
|
|
1130
|
+
|
|
1131
|
+
// Delete account state
|
|
1132
|
+
const [showDeleteForm, setShowDeleteForm] = useState(false);
|
|
1133
|
+
const [deleteFeedback, setDeleteFeedback] = useState(null);
|
|
1134
|
+
|
|
1135
|
+
// Handle password change request (sends email)
|
|
1136
|
+
async function doChangePassword(e) {
|
|
1137
|
+
e.preventDefault();
|
|
1138
|
+
setBusy(true);
|
|
1139
|
+
setFeedback(null);
|
|
1140
|
+
|
|
1141
|
+
try {
|
|
1142
|
+
await authClient.requestChangePasswordLink({ email: user?.email });
|
|
1143
|
+
setFeedback('Password change email sent. Check your inbox.');
|
|
1144
|
+
} catch (err) {
|
|
1145
|
+
setFeedback(err.message || 'Failed to send password change email');
|
|
1146
|
+
} finally {
|
|
1147
|
+
setBusy(false);
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Handle account deletion
|
|
1152
|
+
async function doDeleteAccount(e) {
|
|
1153
|
+
e.preventDefault();
|
|
1154
|
+
|
|
1155
|
+
if (!window.confirm('Are you sure you want to delete your account? This action cannot be undone.')) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
setBusy(true);
|
|
1160
|
+
setDeleteFeedback(null);
|
|
1161
|
+
|
|
1162
|
+
try {
|
|
1163
|
+
await authClient.deleteAccount({ email: user?.email });
|
|
1164
|
+
setDeleteFeedback('Account deleted successfully. Logging out...');
|
|
1165
|
+
|
|
1166
|
+
// Auto logout after 1.5 seconds
|
|
1167
|
+
setTimeout(() => {
|
|
1168
|
+
logout();
|
|
1169
|
+
}, 1500);
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
setDeleteFeedback(err.message || 'Delete account failed');
|
|
1172
|
+
} finally {
|
|
1173
|
+
setBusy(false);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
return (
|
|
1178
|
+
<div className="home-container">
|
|
1179
|
+
<h2>Welcome</h2>
|
|
1180
|
+
|
|
1181
|
+
{/* User Profile Display */}
|
|
1182
|
+
<div className="user-box">
|
|
1183
|
+
<p><strong>User ID:</strong> {user?.id || 'n/a'}</p>
|
|
1184
|
+
<p><strong>Email:</strong> {user?.email}</p>
|
|
1185
|
+
<p><strong>Name:</strong> {user?.name || '—'}</p>
|
|
1186
|
+
<p><strong>Login Method:</strong> {user?.login_method || '—'}</p>
|
|
1187
|
+
<p><strong>Google Linked:</strong> {user?.google_linked ? 'Yes' : '—'}</p>
|
|
1188
|
+
<p><strong>Token (truncated):</strong> {token ? token.slice(0, 18) + '…' : 'none'}</p>
|
|
1189
|
+
</div>
|
|
1190
|
+
|
|
1191
|
+
{/* Action Buttons */}
|
|
1192
|
+
<div className="actions">
|
|
1193
|
+
<button onClick={refreshProfile} disabled={loading}>
|
|
1194
|
+
Refresh Profile
|
|
1195
|
+
</button>
|
|
1196
|
+
<button onClick={logout}>Logout</button>
|
|
1197
|
+
</div>
|
|
1198
|
+
|
|
1199
|
+
{/* Change Password (Email Link) */}
|
|
1200
|
+
<form onSubmit={doChangePassword} style={{ marginTop: 24 }}>
|
|
1201
|
+
<h3>Change Password</h3>
|
|
1202
|
+
<p>We will email a secure link to {user?.email} to change your password.</p>
|
|
1203
|
+
<button type="submit" disabled={busy}>
|
|
1204
|
+
{busy ? 'Sending…' : 'Send Change Password Email'}
|
|
1205
|
+
</button>
|
|
1206
|
+
</form>
|
|
1207
|
+
{feedback && <p>{feedback}</p>}
|
|
1208
|
+
|
|
1209
|
+
{/* Delete Account */}
|
|
1210
|
+
<div style={{ marginTop: 24 }}>
|
|
1211
|
+
<button
|
|
1212
|
+
onClick={() => setShowDeleteForm(!showDeleteForm)}
|
|
1213
|
+
style={{ background: 'red', color: 'white' }}
|
|
1214
|
+
>
|
|
1215
|
+
{showDeleteForm ? 'Cancel' : 'Delete Account'}
|
|
1216
|
+
</button>
|
|
1217
|
+
|
|
1218
|
+
{showDeleteForm && (
|
|
1219
|
+
<form onSubmit={doDeleteAccount}>
|
|
1220
|
+
<p style={{ color: 'red' }}>
|
|
1221
|
+
Are you sure? This cannot be undone.
|
|
1222
|
+
</p>
|
|
1223
|
+
<button type="submit" disabled={busy}>
|
|
1224
|
+
{busy ? 'Deleting…' : 'Confirm Delete Account'}
|
|
1225
|
+
</button>
|
|
1226
|
+
{deleteFeedback && <p>{deleteFeedback}</p>}
|
|
1227
|
+
</form>
|
|
1228
|
+
)}
|
|
1229
|
+
</div>
|
|
1230
|
+
</div>
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
---
|
|
1236
|
+
|
|
1237
|
+
### React Native Examples
|
|
1238
|
+
|
|
1239
|
+
#### Complete React Native Login Screen
|
|
1240
|
+
|
|
1241
|
+
Create `src/screens/LoginScreen.js`:
|
|
1242
|
+
|
|
1243
|
+
```jsx
|
|
1244
|
+
import React, { useState } from 'react';
|
|
1245
|
+
import {
|
|
1246
|
+
View,
|
|
1247
|
+
Text,
|
|
1248
|
+
TextInput,
|
|
1249
|
+
TouchableOpacity,
|
|
1250
|
+
ScrollView,
|
|
1251
|
+
ActivityIndicator,
|
|
1252
|
+
Alert,
|
|
1253
|
+
} from 'react-native';
|
|
1254
|
+
import { GoogleSignin, GoogleSigninButton } from '@react-native-google-signin/google-signin';
|
|
1255
|
+
import { useAuth } from '../AuthContext';
|
|
1256
|
+
|
|
1257
|
+
GoogleSignin.configure({
|
|
1258
|
+
webClientId: 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com',
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
export default function LoginScreen() {
|
|
1262
|
+
const { login, googleLogin, authClient, loading } = useAuth();
|
|
1263
|
+
|
|
1264
|
+
const [email, setEmail] = useState('');
|
|
1265
|
+
const [password, setPassword] = useState('');
|
|
1266
|
+
const [busy, setBusy] = useState(false);
|
|
1267
|
+
const [error, setError] = useState(null);
|
|
1268
|
+
const [showReset, setShowReset] = useState(false);
|
|
1269
|
+
const [resetEmail, setResetEmail] = useState('');
|
|
1270
|
+
|
|
1271
|
+
async function handleSubmit() {
|
|
1272
|
+
setError(null);
|
|
1273
|
+
setBusy(true);
|
|
1274
|
+
|
|
1275
|
+
try {
|
|
1276
|
+
await login({ email, password });
|
|
1277
|
+
} catch (err) {
|
|
1278
|
+
setError(err.message || 'Login failed');
|
|
1279
|
+
} finally {
|
|
1280
|
+
setBusy(false);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
async function handleGoogleSignIn() {
|
|
1285
|
+
try {
|
|
1286
|
+
await GoogleSignin.hasPlayServices();
|
|
1287
|
+
const userInfo = await GoogleSignin.signIn();
|
|
1288
|
+
|
|
1289
|
+
const result = await googleLogin(userInfo.idToken);
|
|
1290
|
+
if (result.isNewUser) {
|
|
1291
|
+
Alert.alert('Welcome!', 'New account created');
|
|
1292
|
+
}
|
|
1293
|
+
} catch (err) {
|
|
1294
|
+
setError(err.message || 'Google sign in failed');
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
async function handlePasswordReset() {
|
|
1299
|
+
setBusy(true);
|
|
1300
|
+
try {
|
|
1301
|
+
await authClient.requestPasswordReset({ email: resetEmail });
|
|
1302
|
+
Alert.alert('Success', 'Password reset email sent');
|
|
1303
|
+
setResetEmail('');
|
|
1304
|
+
setShowReset(false);
|
|
1305
|
+
} catch (err) {
|
|
1306
|
+
setError(err.message || 'Failed to send reset email');
|
|
1307
|
+
} finally {
|
|
1308
|
+
setBusy(false);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return (
|
|
1313
|
+
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1314
|
+
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Login</Text>
|
|
1315
|
+
|
|
1316
|
+
<TextInput
|
|
1317
|
+
placeholder="Email"
|
|
1318
|
+
value={email}
|
|
1319
|
+
onChangeText={setEmail}
|
|
1320
|
+
keyboardType="email-address"
|
|
1321
|
+
editable={!loading}
|
|
1322
|
+
style={{
|
|
1323
|
+
borderWidth: 1,
|
|
1324
|
+
borderColor: '#ddd',
|
|
1325
|
+
padding: 12,
|
|
1326
|
+
marginBottom: 12,
|
|
1327
|
+
borderRadius: 8,
|
|
1328
|
+
}}
|
|
1329
|
+
/>
|
|
1330
|
+
|
|
1331
|
+
<TextInput
|
|
1332
|
+
placeholder="Password"
|
|
1333
|
+
value={password}
|
|
1334
|
+
onChangeText={setPassword}
|
|
1335
|
+
secureTextEntry
|
|
1336
|
+
editable={!loading}
|
|
1337
|
+
style={{
|
|
1338
|
+
borderWidth: 1,
|
|
1339
|
+
borderColor: '#ddd',
|
|
1340
|
+
padding: 12,
|
|
1341
|
+
marginBottom: 20,
|
|
1342
|
+
borderRadius: 8,
|
|
1343
|
+
}}
|
|
1344
|
+
/>
|
|
1345
|
+
|
|
1346
|
+
<TouchableOpacity
|
|
1347
|
+
onPress={handleSubmit}
|
|
1348
|
+
disabled={loading || busy}
|
|
1349
|
+
style={{
|
|
1350
|
+
backgroundColor: '#0066cc',
|
|
1351
|
+
padding: 12,
|
|
1352
|
+
borderRadius: 8,
|
|
1353
|
+
alignItems: 'center',
|
|
1354
|
+
marginBottom: 20,
|
|
1355
|
+
}}
|
|
1356
|
+
>
|
|
1357
|
+
{loading || busy ? (
|
|
1358
|
+
<ActivityIndicator color="#fff" />
|
|
1359
|
+
) : (
|
|
1360
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Login</Text>
|
|
1361
|
+
)}
|
|
1362
|
+
</TouchableOpacity>
|
|
1363
|
+
|
|
1364
|
+
{error && <Text style={{ color: 'red', marginBottom: 12 }}>{error}</Text>}
|
|
1365
|
+
|
|
1366
|
+
<View style={{ alignItems: 'center', marginVertical: 16 }}>
|
|
1367
|
+
<Text>or</Text>
|
|
1368
|
+
</View>
|
|
1369
|
+
|
|
1370
|
+
<GoogleSigninButton
|
|
1371
|
+
size={GoogleSigninButton.Size.Wide}
|
|
1372
|
+
color={GoogleSigninButton.Color.Dark}
|
|
1373
|
+
onPress={handleGoogleSignIn}
|
|
1374
|
+
disabled={loading}
|
|
1375
|
+
/>
|
|
1376
|
+
|
|
1377
|
+
<TouchableOpacity
|
|
1378
|
+
onPress={() => setShowReset(!showReset)}
|
|
1379
|
+
style={{ marginTop: 20 }}
|
|
1380
|
+
>
|
|
1381
|
+
<Text style={{ color: '#0066cc' }}>
|
|
1382
|
+
{showReset ? 'Hide' : 'Forgot password?'}
|
|
1383
|
+
</Text>
|
|
1384
|
+
</TouchableOpacity>
|
|
1385
|
+
|
|
1386
|
+
{showReset && (
|
|
1387
|
+
<View style={{ marginTop: 12 }}>
|
|
1388
|
+
<TextInput
|
|
1389
|
+
placeholder="Enter your email"
|
|
1390
|
+
value={resetEmail}
|
|
1391
|
+
onChangeText={setResetEmail}
|
|
1392
|
+
keyboardType="email-address"
|
|
1393
|
+
style={{
|
|
1394
|
+
borderWidth: 1,
|
|
1395
|
+
borderColor: '#ddd',
|
|
1396
|
+
padding: 12,
|
|
1397
|
+
marginBottom: 12,
|
|
1398
|
+
borderRadius: 8,
|
|
1399
|
+
}}
|
|
1400
|
+
/>
|
|
1401
|
+
<TouchableOpacity
|
|
1402
|
+
onPress={handlePasswordReset}
|
|
1403
|
+
disabled={busy}
|
|
1404
|
+
style={{
|
|
1405
|
+
backgroundColor: '#0066cc',
|
|
1406
|
+
padding: 12,
|
|
1407
|
+
borderRadius: 8,
|
|
1408
|
+
alignItems: 'center',
|
|
1409
|
+
}}
|
|
1410
|
+
>
|
|
1411
|
+
<Text style={{ color: '#fff' }}>
|
|
1412
|
+
{busy ? 'Sending...' : 'Send Reset Link'}
|
|
1413
|
+
</Text>
|
|
1414
|
+
</TouchableOpacity>
|
|
1415
|
+
</View>
|
|
1416
|
+
)}
|
|
1417
|
+
</ScrollView>
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
#### Complete React Native Register Screen
|
|
1423
|
+
|
|
1424
|
+
Create `src/screens/RegisterScreen.js`:
|
|
1425
|
+
|
|
1426
|
+
```jsx
|
|
1427
|
+
import React, { useState } from 'react';
|
|
1428
|
+
import {
|
|
1429
|
+
View,
|
|
1430
|
+
Text,
|
|
1431
|
+
TextInput,
|
|
1432
|
+
TouchableOpacity,
|
|
1433
|
+
ScrollView,
|
|
1434
|
+
ActivityIndicator,
|
|
1435
|
+
Alert,
|
|
1436
|
+
} from 'react-native';
|
|
1437
|
+
import { useAuth } from '../AuthContext';
|
|
1438
|
+
|
|
1439
|
+
export default function RegisterScreen() {
|
|
1440
|
+
const { authClient } = useAuth();
|
|
1441
|
+
|
|
1442
|
+
const [email, setEmail] = useState('');
|
|
1443
|
+
const [password, setPassword] = useState('');
|
|
1444
|
+
const [name, setName] = useState('');
|
|
1445
|
+
const [username, setUsername] = useState('');
|
|
1446
|
+
const [error, setError] = useState(null);
|
|
1447
|
+
const [busy, setBusy] = useState(false);
|
|
1448
|
+
|
|
1449
|
+
async function handleSubmit() {
|
|
1450
|
+
setError(null);
|
|
1451
|
+
setBusy(true);
|
|
1452
|
+
|
|
1453
|
+
try {
|
|
1454
|
+
await authClient.register({
|
|
1455
|
+
email,
|
|
1456
|
+
username,
|
|
1457
|
+
password,
|
|
1458
|
+
name,
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
Alert.alert(
|
|
1462
|
+
'Registration Successful',
|
|
1463
|
+
'Please verify your email, then login.'
|
|
1464
|
+
);
|
|
1465
|
+
|
|
1466
|
+
setEmail('');
|
|
1467
|
+
setPassword('');
|
|
1468
|
+
setName('');
|
|
1469
|
+
setUsername('');
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
setError(err.message || 'Registration failed');
|
|
1472
|
+
} finally {
|
|
1473
|
+
setBusy(false);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
return (
|
|
1478
|
+
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1479
|
+
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Sign Up</Text>
|
|
1480
|
+
|
|
1481
|
+
<TextInput
|
|
1482
|
+
placeholder="Name"
|
|
1483
|
+
value={name}
|
|
1484
|
+
onChangeText={setName}
|
|
1485
|
+
editable={!busy}
|
|
1486
|
+
style={{
|
|
1487
|
+
borderWidth: 1,
|
|
1488
|
+
borderColor: '#ddd',
|
|
1489
|
+
padding: 12,
|
|
1490
|
+
marginBottom: 12,
|
|
1491
|
+
borderRadius: 8,
|
|
1492
|
+
}}
|
|
1493
|
+
/>
|
|
1494
|
+
|
|
1495
|
+
<TextInput
|
|
1496
|
+
placeholder="Username"
|
|
1497
|
+
value={username}
|
|
1498
|
+
onChangeText={setUsername}
|
|
1499
|
+
editable={!busy}
|
|
1500
|
+
style={{
|
|
1501
|
+
borderWidth: 1,
|
|
1502
|
+
borderColor: '#ddd',
|
|
1503
|
+
padding: 12,
|
|
1504
|
+
marginBottom: 12,
|
|
1505
|
+
borderRadius: 8,
|
|
1506
|
+
}}
|
|
1507
|
+
/>
|
|
1508
|
+
|
|
1509
|
+
<TextInput
|
|
1510
|
+
placeholder="Email"
|
|
1511
|
+
value={email}
|
|
1512
|
+
onChangeText={setEmail}
|
|
1513
|
+
keyboardType="email-address"
|
|
1514
|
+
editable={!busy}
|
|
1515
|
+
style={{
|
|
1516
|
+
borderWidth: 1,
|
|
1517
|
+
borderColor: '#ddd',
|
|
1518
|
+
padding: 12,
|
|
1519
|
+
marginBottom: 12,
|
|
1520
|
+
borderRadius: 8,
|
|
1521
|
+
}}
|
|
1522
|
+
/>
|
|
1523
|
+
|
|
1524
|
+
<TextInput
|
|
1525
|
+
placeholder="Password"
|
|
1526
|
+
value={password}
|
|
1527
|
+
onChangeText={setPassword}
|
|
1528
|
+
secureTextEntry
|
|
1529
|
+
editable={!busy}
|
|
1530
|
+
style={{
|
|
1531
|
+
borderWidth: 1,
|
|
1532
|
+
borderColor: '#ddd',
|
|
1533
|
+
padding: 12,
|
|
1534
|
+
marginBottom: 20,
|
|
1535
|
+
borderRadius: 8,
|
|
1536
|
+
}}
|
|
1537
|
+
/>
|
|
1538
|
+
|
|
1539
|
+
<TouchableOpacity
|
|
1540
|
+
onPress={handleSubmit}
|
|
1541
|
+
disabled={busy}
|
|
1542
|
+
style={{
|
|
1543
|
+
backgroundColor: '#0066cc',
|
|
1544
|
+
padding: 12,
|
|
1545
|
+
borderRadius: 8,
|
|
1546
|
+
alignItems: 'center',
|
|
1547
|
+
}}
|
|
1548
|
+
>
|
|
1549
|
+
{busy ? (
|
|
1550
|
+
<ActivityIndicator color="#fff" />
|
|
1551
|
+
) : (
|
|
1552
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Create Account</Text>
|
|
1553
|
+
)}
|
|
1554
|
+
</TouchableOpacity>
|
|
1555
|
+
|
|
1556
|
+
{error && <Text style={{ color: 'red', marginTop: 12 }}>{error}</Text>}
|
|
1557
|
+
</ScrollView>
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
```
|
|
1561
|
+
|
|
1562
|
+
#### Complete React Native Home Screen
|
|
1563
|
+
|
|
1564
|
+
Create `src/screens/HomeScreen.js`:
|
|
1565
|
+
|
|
1566
|
+
```jsx
|
|
1567
|
+
import React, { useState } from 'react';
|
|
1568
|
+
import {
|
|
1569
|
+
View,
|
|
1570
|
+
Text,
|
|
1571
|
+
TouchableOpacity,
|
|
1572
|
+
ScrollView,
|
|
1573
|
+
ActivityIndicator,
|
|
1574
|
+
Alert,
|
|
1575
|
+
} from 'react-native';
|
|
1576
|
+
import { useAuth } from '../AuthContext';
|
|
1577
|
+
|
|
1578
|
+
export default function HomeScreen() {
|
|
1579
|
+
const { user, token, logout, refreshProfile, loading, authClient } = useAuth();
|
|
1580
|
+
|
|
1581
|
+
const [feedback, setFeedback] = useState(null);
|
|
1582
|
+
const [busy, setBusy] = useState(false);
|
|
1583
|
+
const [showDeleteForm, setShowDeleteForm] = useState(false);
|
|
1584
|
+
|
|
1585
|
+
async function doChangePassword() {
|
|
1586
|
+
setBusy(true);
|
|
1587
|
+
setFeedback(null);
|
|
1588
|
+
|
|
1589
|
+
try {
|
|
1590
|
+
await authClient.requestChangePasswordLink({ email: user?.email });
|
|
1591
|
+
setFeedback('Password change email sent. Check your inbox.');
|
|
1592
|
+
} catch (err) {
|
|
1593
|
+
setFeedback(err.message || 'Failed to send password change email');
|
|
1594
|
+
} finally {
|
|
1595
|
+
setBusy(false);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
async function doDeleteAccount() {
|
|
1600
|
+
Alert.alert(
|
|
1601
|
+
'Delete Account',
|
|
1602
|
+
'Are you sure? This cannot be undone.',
|
|
1603
|
+
[
|
|
1604
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
1605
|
+
{
|
|
1606
|
+
text: 'Delete',
|
|
1607
|
+
style: 'destructive',
|
|
1608
|
+
onPress: async () => {
|
|
1609
|
+
setBusy(true);
|
|
1610
|
+
try {
|
|
1611
|
+
await authClient.deleteAccount({ email: user?.email });
|
|
1612
|
+
setFeedback('Account deleted. Logging out...');
|
|
1613
|
+
setTimeout(() => logout(), 1500);
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
setFeedback(err.message || 'Delete account failed');
|
|
1616
|
+
} finally {
|
|
1617
|
+
setBusy(false);
|
|
1618
|
+
}
|
|
1619
|
+
},
|
|
1620
|
+
},
|
|
1621
|
+
]
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
return (
|
|
1626
|
+
<ScrollView style={{ flex: 1, padding: 20, backgroundColor: '#fff' }}>
|
|
1627
|
+
<Text style={{ fontSize: 24, fontWeight: 'bold', marginBottom: 24 }}>Welcome</Text>
|
|
1628
|
+
|
|
1629
|
+
<View style={{ backgroundColor: '#f5f5f5', padding: 16, borderRadius: 8, marginBottom: 24 }}>
|
|
1630
|
+
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>User ID:</Text> {user?.id || 'n/a'}</Text>
|
|
1631
|
+
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Email:</Text> {user?.email}</Text>
|
|
1632
|
+
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Name:</Text> {user?.name || '—'}</Text>
|
|
1633
|
+
<Text style={{ marginBottom: 8 }}><Text style={{ fontWeight: 'bold' }}>Login Method:</Text> {user?.login_method || '—'}</Text>
|
|
1634
|
+
<Text><Text style={{ fontWeight: 'bold' }}>Token:</Text> {token ? token.slice(0, 18) + '…' : 'none'}</Text>
|
|
1635
|
+
</View>
|
|
1636
|
+
|
|
1637
|
+
<View style={{ flexDirection: 'row', gap: 12, marginBottom: 24 }}>
|
|
1638
|
+
<TouchableOpacity
|
|
1639
|
+
onPress={refreshProfile}
|
|
1640
|
+
disabled={loading}
|
|
1641
|
+
style={{
|
|
1642
|
+
flex: 1,
|
|
1643
|
+
backgroundColor: '#0066cc',
|
|
1644
|
+
padding: 12,
|
|
1645
|
+
borderRadius: 8,
|
|
1646
|
+
alignItems: 'center',
|
|
1647
|
+
}}
|
|
1648
|
+
>
|
|
1649
|
+
{loading ? (
|
|
1650
|
+
<ActivityIndicator color="#fff" />
|
|
1651
|
+
) : (
|
|
1652
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Refresh Profile</Text>
|
|
1653
|
+
)}
|
|
1654
|
+
</TouchableOpacity>
|
|
1655
|
+
|
|
1656
|
+
<TouchableOpacity
|
|
1657
|
+
onPress={logout}
|
|
1658
|
+
style={{
|
|
1659
|
+
flex: 1,
|
|
1660
|
+
backgroundColor: '#999',
|
|
1661
|
+
padding: 12,
|
|
1662
|
+
borderRadius: 8,
|
|
1663
|
+
alignItems: 'center',
|
|
1664
|
+
}}
|
|
1665
|
+
>
|
|
1666
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Logout</Text>
|
|
1667
|
+
</TouchableOpacity>
|
|
1668
|
+
</View>
|
|
1669
|
+
|
|
1670
|
+
<View style={{ marginBottom: 24 }}>
|
|
1671
|
+
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 12 }}>Change Password</Text>
|
|
1672
|
+
<Text style={{ marginBottom: 12 }}>We will email a secure link to {user?.email} to change your password.</Text>
|
|
1673
|
+
<TouchableOpacity
|
|
1674
|
+
onPress={doChangePassword}
|
|
1675
|
+
disabled={busy}
|
|
1676
|
+
style={{
|
|
1677
|
+
backgroundColor: '#0066cc',
|
|
1678
|
+
padding: 12,
|
|
1679
|
+
borderRadius: 8,
|
|
1680
|
+
alignItems: 'center',
|
|
1681
|
+
}}
|
|
1682
|
+
>
|
|
1683
|
+
{busy ? (
|
|
1684
|
+
<ActivityIndicator color="#fff" />
|
|
1685
|
+
) : (
|
|
1686
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Send Change Password Email</Text>
|
|
1687
|
+
)}
|
|
1688
|
+
</TouchableOpacity>
|
|
1689
|
+
</View>
|
|
1690
|
+
|
|
1691
|
+
{feedback && (
|
|
1692
|
+
<Text style={{ color: feedback.includes('successfully') || feedback.includes('sent') ? 'green' : 'red', marginBottom: 12 }}>
|
|
1693
|
+
{feedback}
|
|
1694
|
+
</Text>
|
|
1695
|
+
)}
|
|
1696
|
+
|
|
1697
|
+
<TouchableOpacity
|
|
1698
|
+
onPress={doDeleteAccount}
|
|
1699
|
+
style={{
|
|
1700
|
+
backgroundColor: '#d32f2f',
|
|
1701
|
+
padding: 12,
|
|
1702
|
+
borderRadius: 8,
|
|
1703
|
+
alignItems: 'center',
|
|
1704
|
+
}}
|
|
1705
|
+
>
|
|
1706
|
+
<Text style={{ color: '#fff', fontWeight: 'bold' }}>Delete Account</Text>
|
|
1707
|
+
</TouchableOpacity>
|
|
1708
|
+
</ScrollView>
|
|
1709
|
+
);
|
|
1710
|
+
}
|
|
1711
|
+
```
|
|
1712
|
+
|
|
1713
|
+
#### React Native Splash Screen
|
|
1714
|
+
|
|
1715
|
+
Create `src/screens/SplashScreen.js`:
|
|
1716
|
+
|
|
1717
|
+
```jsx
|
|
1718
|
+
import React from 'react';
|
|
1719
|
+
import { View, ActivityIndicator, Text } from 'react-native';
|
|
1720
|
+
|
|
1721
|
+
export default function SplashScreen() {
|
|
1722
|
+
return (
|
|
1723
|
+
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#fff' }}>
|
|
1724
|
+
<ActivityIndicator size="large" color="#0066cc" />
|
|
1725
|
+
<Text style={{ marginTop: 12 }}>Loading...</Text>
|
|
1726
|
+
</View>
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
### 1. Check if User is Authenticated
|
|
1732
|
+
|
|
1733
|
+
```jsx
|
|
1734
|
+
import { useAuth } from './AuthContext';
|
|
1735
|
+
|
|
1736
|
+
function MyComponent() {
|
|
1737
|
+
const { user, token } = useAuth();
|
|
1738
|
+
|
|
1739
|
+
if (!user || !token) {
|
|
1740
|
+
return <p>Please login first</p>;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
return <p>Welcome, {user.name}!</p>;
|
|
1744
|
+
}
|
|
1745
|
+
```
|
|
1746
|
+
|
|
1747
|
+
### 2. Handle Authentication Errors
|
|
1748
|
+
|
|
1749
|
+
```jsx
|
|
47
1750
|
try {
|
|
48
|
-
await
|
|
1751
|
+
await authClient.login({ email, password });
|
|
49
1752
|
} catch (err) {
|
|
50
|
-
|
|
51
|
-
|
|
1753
|
+
// Check error code
|
|
1754
|
+
if (err.code === 'EMAIL_NOT_VERIFIED') {
|
|
1755
|
+
// Show verification resend option
|
|
1756
|
+
} else if (err.status === 401) {
|
|
1757
|
+
// Invalid credentials
|
|
1758
|
+
} else {
|
|
1759
|
+
// Generic error
|
|
1760
|
+
console.error(err.message);
|
|
1761
|
+
}
|
|
52
1762
|
}
|
|
53
1763
|
```
|
|
54
1764
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```
|
|
1765
|
+
### 3. Protect Routes/Components
|
|
1766
|
+
|
|
1767
|
+
```jsx
|
|
1768
|
+
function ProtectedRoute({ children }) {
|
|
1769
|
+
const { user } = useAuth();
|
|
1770
|
+
|
|
1771
|
+
if (!user) {
|
|
1772
|
+
return <Navigate to="/login" />;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
return children;
|
|
1776
|
+
}
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
### 4. Auto-refresh User Profile on Mount
|
|
1780
|
+
|
|
1781
|
+
```jsx
|
|
1782
|
+
useEffect(() => {
|
|
1783
|
+
if (user && token) {
|
|
1784
|
+
refreshProfile();
|
|
1785
|
+
}
|
|
1786
|
+
}, []);
|
|
1787
|
+
```
|
|
1788
|
+
|
|
1789
|
+
### 5. Handle Token Expiration
|
|
1790
|
+
|
|
1791
|
+
```jsx
|
|
1792
|
+
const { refreshProfile } = useAuth();
|
|
1793
|
+
|
|
1794
|
+
// Periodically check token validity
|
|
1795
|
+
useEffect(() => {
|
|
1796
|
+
const interval = setInterval(async () => {
|
|
1797
|
+
try {
|
|
1798
|
+
await refreshProfile();
|
|
1799
|
+
} catch (err) {
|
|
1800
|
+
if (err.status === 401) {
|
|
1801
|
+
// Token expired, redirect to login
|
|
1802
|
+
logout();
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}, 5 * 60 * 1000); // Check every 5 minutes
|
|
1806
|
+
|
|
1807
|
+
return () => clearInterval(interval);
|
|
1808
|
+
}, [refreshProfile, logout]);
|
|
1809
|
+
```
|
|
1810
|
+
|
|
1811
|
+
---
|
|
1812
|
+
|
|
1813
|
+
## React Native Specific Patterns
|
|
1814
|
+
|
|
1815
|
+
### 1. Handle Token Persistence (React Native)
|
|
1816
|
+
|
|
1817
|
+
```jsx
|
|
1818
|
+
// AuthContext.js already includes this in the bootstrap function
|
|
1819
|
+
React.useEffect(() => {
|
|
1820
|
+
const bootstrapAsync = async () => {
|
|
1821
|
+
try {
|
|
1822
|
+
const savedToken = await AsyncStorage.getItem('auth_user_token');
|
|
1823
|
+
if (savedToken) {
|
|
1824
|
+
authClient.setToken(savedToken);
|
|
1825
|
+
setToken(savedToken);
|
|
1826
|
+
// Fetch user profile
|
|
1827
|
+
const res = await authClient.getProfile();
|
|
1828
|
+
setUser(res.data.user);
|
|
1829
|
+
}
|
|
1830
|
+
} catch (err) {
|
|
1831
|
+
console.error('Failed to restore token', err);
|
|
1832
|
+
} finally {
|
|
1833
|
+
setIsReady(true);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
bootstrapAsync();
|
|
1837
|
+
}, []);
|
|
1838
|
+
```
|
|
1839
|
+
|
|
1840
|
+
### 2. Handle Deep Links (React Native)
|
|
1841
|
+
|
|
1842
|
+
```jsx
|
|
1843
|
+
import * as Linking from 'expo-linking';
|
|
1844
|
+
|
|
1845
|
+
const linking = {
|
|
1846
|
+
prefixes: ['myapp://', 'https://myapp.com'],
|
|
1847
|
+
config: {
|
|
1848
|
+
screens: {
|
|
1849
|
+
ResetPassword: 'reset/:token',
|
|
1850
|
+
VerifyEmail: 'verify/:token',
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
};
|
|
1854
|
+
|
|
1855
|
+
export function RootNavigator() {
|
|
1856
|
+
const { user } = useAuth();
|
|
1857
|
+
|
|
1858
|
+
return (
|
|
1859
|
+
<NavigationContainer linking={linking}>
|
|
1860
|
+
{/* Navigation config */}
|
|
1861
|
+
</NavigationContainer>
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1864
|
+
```
|
|
1865
|
+
|
|
1866
|
+
### 3. Handle Keyboard in React Native
|
|
1867
|
+
|
|
1868
|
+
```jsx
|
|
1869
|
+
import { KeyboardAvoidingView, Platform } from 'react-native';
|
|
1870
|
+
|
|
1871
|
+
export default function LoginScreen() {
|
|
1872
|
+
return (
|
|
1873
|
+
<KeyboardAvoidingView
|
|
1874
|
+
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
1875
|
+
style={{ flex: 1 }}
|
|
1876
|
+
>
|
|
1877
|
+
{/* Form content */}
|
|
1878
|
+
</KeyboardAvoidingView>
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
```
|
|
1882
|
+
|
|
1883
|
+
### 4. Handle Biometric Authentication (React Native)
|
|
1884
|
+
|
|
1885
|
+
```jsx
|
|
58
1886
|
import * as SecureStore from 'expo-secure-store';
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
1887
|
+
import * as LocalAuthentication from 'expo-local-authentication';
|
|
1888
|
+
|
|
1889
|
+
async function handleBiometricLogin() {
|
|
1890
|
+
try {
|
|
1891
|
+
const compatible = await LocalAuthentication.hasHardwareAsync();
|
|
1892
|
+
if (!compatible) return;
|
|
1893
|
+
|
|
1894
|
+
const savedEmail = await SecureStore.getItemAsync('saved_email');
|
|
1895
|
+
const savedPassword = await SecureStore.getItemAsync('saved_password');
|
|
1896
|
+
|
|
1897
|
+
if (savedEmail && savedPassword) {
|
|
1898
|
+
const result = await LocalAuthentication.authenticateAsync({
|
|
1899
|
+
disableDeviceFallback: false,
|
|
1900
|
+
});
|
|
1901
|
+
|
|
1902
|
+
if (result.success) {
|
|
1903
|
+
await login({ email: savedEmail, password: savedPassword });
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
console.error('Biometric login failed', err);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
62
1910
|
```
|
|
63
1911
|
|
|
64
|
-
|
|
65
|
-
- Never bundle apiSecret in client apps.
|
|
66
|
-
- Use HTTPS.
|
|
67
|
-
- Rotate keys via dashboard.
|
|
68
|
-
- Implement refresh token endpoint (future) for long sessions.
|
|
1912
|
+
---
|
|
69
1913
|
|
|
70
|
-
##
|
|
71
|
-
1. Set fields in package.json (name, version, author).
|
|
72
|
-
2. Login to npm: `npm login`
|
|
73
|
-
3. Publish: `npm publish --access public`
|
|
1914
|
+
## Troubleshooting
|
|
74
1915
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
1916
|
+
### Issue: "Failed to execute 'fetch' on 'Window': Illegal invocation"
|
|
1917
|
+
|
|
1918
|
+
**Solution:** Make sure you bind fetch correctly in AuthContext:
|
|
1919
|
+
|
|
1920
|
+
```javascript
|
|
1921
|
+
fetch: (input, init = {}) => {
|
|
1922
|
+
// ...
|
|
1923
|
+
return window.fetch(input, { ...init, headers });
|
|
1924
|
+
}
|
|
79
1925
|
```
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1926
|
+
|
|
1927
|
+
### Issue: 401 Unauthorized on authenticated requests
|
|
1928
|
+
|
|
1929
|
+
**Solution:** Ensure token is being set and sent correctly:
|
|
1930
|
+
|
|
1931
|
+
1. Check if `authClient.setToken()` is called after login
|
|
1932
|
+
2. Verify the Authorization header format matches your server's expectations
|
|
1933
|
+
3. Check if token is expired
|
|
1934
|
+
|
|
1935
|
+
### Issue: Google Sign-In not working
|
|
1936
|
+
|
|
1937
|
+
**Solution:**
|
|
1938
|
+
1. Verify Google Client ID is correct
|
|
1939
|
+
2. Make sure `GoogleOAuthProvider` wraps your app
|
|
1940
|
+
3. Check browser console for CORS errors
|
|
1941
|
+
4. Ensure your domain is authorized in Google Cloud Console
|
|
1942
|
+
|
|
1943
|
+
### Issue: Email verification emails not arriving
|
|
1944
|
+
|
|
1945
|
+
**Solution:**
|
|
1946
|
+
1. Check spam/junk folder
|
|
1947
|
+
2. Verify email is correct
|
|
1948
|
+
3. Wait a few minutes (email delivery can be delayed)
|
|
1949
|
+
4. Use `resendVerificationEmail()` to request a new one
|
|
1950
|
+
|
|
1951
|
+
### Issue: Password reset link not working
|
|
1952
|
+
|
|
1953
|
+
**Solution:**
|
|
1954
|
+
1. Check if link has expired (usually 1 hour)
|
|
1955
|
+
2. Request a new link using `requestPasswordReset()`
|
|
1956
|
+
3. Ensure you're using the correct email address
|
|
1957
|
+
|
|
1958
|
+
---
|
|
1959
|
+
|
|
1960
|
+
## Platform-Specific Considerations
|
|
1961
|
+
|
|
1962
|
+
### React Web vs React Native
|
|
1963
|
+
|
|
1964
|
+
| Feature | React Web | React Native |
|
|
1965
|
+
|---------|-----------|--------------|
|
|
1966
|
+
| **Storage** | localStorage (default) | AsyncStorage |
|
|
1967
|
+
| **Navigation** | React Router | React Navigation |
|
|
1968
|
+
| **Google Sign-In** | @react-oauth/google | @react-native-google-signin/google-signin |
|
|
1969
|
+
| **Token Persistence** | Optional (storage: null) | Recommended (AsyncStorage) |
|
|
1970
|
+
| **Network Requests** | window.fetch | Global fetch |
|
|
1971
|
+
| **UI Components** | HTML/CSS | React Native components |
|
|
1972
|
+
| **Deep Linking** | Browser URL | Expo Linking or Deep Linking |
|
|
1973
|
+
|
|
1974
|
+
### Key Differences in Implementation
|
|
1975
|
+
|
|
1976
|
+
#### 1. Token Storage
|
|
1977
|
+
|
|
1978
|
+
**React Web:**
|
|
1979
|
+
```javascript
|
|
1980
|
+
storage: null // In-memory only
|
|
1981
|
+
// or
|
|
1982
|
+
storage: window.localStorage // Persistent
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
**React Native:**
|
|
1986
|
+
```javascript
|
|
1987
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
1988
|
+
storage: AsyncStorage // Persistent and secure
|
|
1989
|
+
```
|
|
1990
|
+
|
|
1991
|
+
#### 2. Navigation
|
|
1992
|
+
|
|
1993
|
+
**React Web:**
|
|
1994
|
+
```jsx
|
|
1995
|
+
// Uses React Router or conditional rendering
|
|
1996
|
+
{user ? <Home /> : <Login />}
|
|
1997
|
+
```
|
|
1998
|
+
|
|
1999
|
+
**React Native:**
|
|
2000
|
+
```jsx
|
|
2001
|
+
// Uses React Navigation
|
|
2002
|
+
{user ? (
|
|
2003
|
+
<Stack.Screen name="Home" component={HomeScreen} />
|
|
2004
|
+
) : (
|
|
2005
|
+
<Stack.Screen name="Login" component={LoginScreen} />
|
|
2006
|
+
)}
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
#### 3. Loading States
|
|
2010
|
+
|
|
2011
|
+
**React Web:**
|
|
2012
|
+
```jsx
|
|
2013
|
+
<button disabled={loading}>
|
|
2014
|
+
{loading ? 'Please wait…' : 'Login'}
|
|
2015
|
+
</button>
|
|
2016
|
+
```
|
|
2017
|
+
|
|
2018
|
+
**React Native:**
|
|
2019
|
+
```jsx
|
|
2020
|
+
<TouchableOpacity disabled={loading}>
|
|
2021
|
+
{loading ? <ActivityIndicator /> : <Text>Login</Text>}
|
|
2022
|
+
</TouchableOpacity>
|
|
2023
|
+
```
|
|
2024
|
+
|
|
2025
|
+
#### 4. Styling
|
|
2026
|
+
|
|
2027
|
+
**React Web:**
|
|
2028
|
+
```jsx
|
|
2029
|
+
<button style={{ color: 'white', backgroundColor: 'blue' }}>
|
|
2030
|
+
Login
|
|
2031
|
+
</button>
|
|
2032
|
+
```
|
|
2033
|
+
|
|
2034
|
+
**React Native:**
|
|
2035
|
+
```jsx
|
|
2036
|
+
<TouchableOpacity style={{ backgroundColor: 'blue' }}>
|
|
2037
|
+
<Text style={{ color: 'white' }}>Login</Text>
|
|
2038
|
+
</TouchableOpacity>
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
---
|
|
2042
|
+
|
|
2043
|
+
## Security Best Practices
|
|
2044
|
+
|
|
2045
|
+
### 1. **Never expose API secrets in production**
|
|
2046
|
+
|
|
2047
|
+
In production, move API secret to your backend server:
|
|
2048
|
+
|
|
2049
|
+
```javascript
|
|
2050
|
+
// Instead of this (insecure):
|
|
2051
|
+
const authClient = AuthClient.create(PUBLIC_KEY, API_SECRET);
|
|
2052
|
+
|
|
2053
|
+
// Do this (secure):
|
|
2054
|
+
// Have your backend proxy all auth requests
|
|
2055
|
+
// Your frontend only sends requests to YOUR server
|
|
2056
|
+
// Your server adds the API secret and forwards to auth server
|
|
2057
|
+
```
|
|
2058
|
+
|
|
2059
|
+
### 2. **Use HTTPS in production**
|
|
2060
|
+
|
|
2061
|
+
Always serve your app over HTTPS to prevent token interception.
|
|
2062
|
+
|
|
2063
|
+
### 3. **Implement token refresh**
|
|
2064
|
+
|
|
2065
|
+
Periodically refresh the user's profile to detect token expiration early.
|
|
2066
|
+
|
|
2067
|
+
### 4. **Clear sensitive data on logout**
|
|
2068
|
+
|
|
2069
|
+
```javascript
|
|
2070
|
+
const logout = () => {
|
|
2071
|
+
setUser(null);
|
|
2072
|
+
setToken(null);
|
|
2073
|
+
authClient.token = null;
|
|
2074
|
+
// Clear any other sensitive state
|
|
2075
|
+
};
|
|
83
2076
|
```
|
|
84
2077
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
2078
|
+
### 5. **Validate user input**
|
|
2079
|
+
|
|
2080
|
+
Always validate email format, password strength, etc. before sending to server.
|
|
2081
|
+
|
|
2082
|
+
---
|
|
2083
|
+
|
|
2084
|
+
## Additional Resources
|
|
2085
|
+
|
|
2086
|
+
- **npm Package:** [@mspkapps/auth-client](https://www.npmjs.com/package/@mspkapps/auth-client)
|
|
2087
|
+
- **Google OAuth Setup (Web):** [React OAuth Google Docs](https://www.npmjs.com/package/@react-oauth/google)
|
|
2088
|
+
- **Google OAuth Setup (React Native):** [@react-native-google-signin](https://www.npmjs.com/package/@react-native-google-signin/google-signin)
|
|
2089
|
+
- **React Documentation:** [React Official Docs](https://react.dev)
|
|
2090
|
+
- **React Native Documentation:** [React Native Official Docs](https://reactnative.dev)
|
|
2091
|
+
- **React Navigation:** [React Navigation Docs](https://reactnavigation.org)
|
|
2092
|
+
- **AsyncStorage (React Native):** [AsyncStorage Documentation](https://react-native-async-storage.github.io)
|
|
2093
|
+
|
|
2094
|
+
---
|
|
2095
|
+
|
|
2096
|
+
## Support
|
|
2097
|
+
|
|
2098
|
+
For questions or issues:
|
|
2099
|
+
1. Check this documentation first
|
|
2100
|
+
2. Review the demo code in this project
|
|
2101
|
+
3. Check package documentation on npm
|
|
2102
|
+
4. Contact the auth server maintainers
|
|
2103
|
+
|
|
2104
|
+
---
|
|
2105
|
+
|
|
2106
|
+
**Happy Coding! 🚀**
|