@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.
Files changed (2) hide show
  1. package/README.md +2076 -58
  2. package/package.json +4 -4
package/README.md CHANGED
@@ -1,88 +1,2106 @@
1
- # Auth Client SDK
1
+ # Auth Server Demo - Complete Documentation
2
2
 
3
- Lightweight JavaScript client for Your Auth Service.
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 install @your-scope/auth-client
43
+ npm create vite@latest my-auth-app -- --template react
44
+ cd my-auth-app
8
45
  ```
9
46
 
10
- ## Quick Start (Browser / React)
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
- async function doLogin() {
16
- try {
17
- const res = await auth.login({ email: 'user@example.com', password: 'Pass123!' });
18
- console.log('Logged in user:', res.data.user);
19
- } catch (e) {
20
- console.error(e);
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
- Store `auth.token` (access token) in memory or secure cookie. Avoid localStorage for high-security apps.
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
- import { AuthClient } from '@mspk-apps/auth-client';
30
- const auth = new AuthClient({
31
- baseUrl: process.env.AUTH_BASE_URL,
32
- apiKey: process.env.AUTH_API_KEY,
33
- apiSecret: process.env.AUTH_API_SECRET // keep secret server-side only
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
- ## Methods
38
- - `register({ email, password, name?, username? })`
39
- - `login({ email, password })`
40
- - `getProfile()`
41
- - `verifyEmail(token)`
42
- - `authed('/custom-endpoint', { method:'POST', body:{...} })`
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 auth.login({ email, password });
1751
+ await authClient.login({ email, password });
49
1752
  } catch (err) {
50
- if (err.status === 401) { /* invalid credentials */ }
51
- console.log(err.code, err.message);
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
- ## React Native
56
- Use same API. For token persistence use `expo-secure-store`:
57
- ```javascript
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
- const auth = new AuthClient({ baseUrl: 'https://api.mspkapps.in/api/v1', apiKey: 'PUBLIC_KEY' });
60
- await auth.login({ email, password });
61
- await SecureStore.setItemAsync('auth_token', auth.token);
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
- ## Security
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
- ## Publish
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
- ## Local Development Test
76
- From `SDK` folder:
77
- ```bash
78
- npm link
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
- In app project:
81
- ```bash
82
- npm link @mspk-apps/auth-client
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
- ## Future Expansion
86
- - Add `refreshToken()` when backend supports it.
87
- - Add TypeScript definitions.
88
- - Add revoke & password reset helpers.
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! 🚀**