@wattsoft/auth-app 1.0.1 → 1.0.2
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 +243 -19
- package/index.ts +33 -0
- package/lib/AuthProvider.tsx +70 -0
- package/lib/hooks.ts +245 -0
- package/lib/types.ts +131 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,33 +1,257 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @wattsoft/auth-app
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A complete, production-ready authentication library for React Native and Expo apps.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Zero authentication code needed. Just import and use.** 🎉
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
[](https://www.npmjs.com/package/@wattsoft/auth-app)
|
|
8
|
+
[](LICENSE)
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
npm install
|
|
11
|
-
```
|
|
10
|
+
## Features
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
- ✅ **Pre-built Auth Screens** - Sign-in & sign-up screens, ready to use
|
|
13
|
+
- ✅ **Email & Password Authentication** - Standard authentication flow
|
|
14
|
+
- ✅ **Google Sign-In** - OAuth 2.0 integration
|
|
15
|
+
- ✅ **Biometric Authentication** - Fingerprint & Face ID
|
|
16
|
+
- ✅ **Passkey Support** - Passwordless authentication
|
|
17
|
+
- ✅ **TypeScript** - Full type safety
|
|
18
|
+
- ✅ **Secure Storage** - Encrypted token storage
|
|
19
|
+
- ✅ **Clerk Integration** - Powered by Clerk
|
|
20
|
+
- ✅ **Custom Hooks** - For advanced use cases
|
|
21
|
+
- ✅ **Zero Config** - Works out of the box
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
npx expo start
|
|
17
|
-
```
|
|
23
|
+
## Quick Start
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
### 1. Install
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npm install @wattsoft/auth-app
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 2. Setup
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { AuthProvider } from "@wattsoft/auth-app";
|
|
35
|
+
|
|
36
|
+
export default function App() {
|
|
37
|
+
return (
|
|
38
|
+
<AuthProvider publishableKey="pk_test_....">{/* Your app */}</AuthProvider>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 3. Use Screens
|
|
44
|
+
|
|
45
|
+
```tsx
|
|
46
|
+
import { SignInScreen, SignUpScreen } from '@wattsoft/auth-app';
|
|
47
|
+
|
|
48
|
+
// In your navigation
|
|
49
|
+
<Stack.Screen name="sign-in" component={SignInScreen} />
|
|
50
|
+
<Stack.Screen name="sign-up" component={SignUpScreen} />
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**That's it!** Your app now has complete authentication. ✅
|
|
54
|
+
|
|
55
|
+
## Documentation
|
|
56
|
+
|
|
57
|
+
- **[SETUP_GUIDE.md](./SETUP_GUIDE.md)** - Complete setup instructions
|
|
58
|
+
- **[INTEGRATION_EXAMPLE.md](./INTEGRATION_EXAMPLE.md)** - Step-by-step integration guide
|
|
59
|
+
- **[PUBLISHING_GUIDE.md](./PUBLISHING_GUIDE.md)** - How to publish to npm
|
|
60
|
+
|
|
61
|
+
## Available Exports
|
|
62
|
+
|
|
63
|
+
### Screens (Ready to Use)
|
|
64
|
+
|
|
65
|
+
- `SignInScreen` - Complete sign-in UI
|
|
66
|
+
- `SignUpScreen` - Complete sign-up UI
|
|
67
|
+
|
|
68
|
+
### Components
|
|
69
|
+
|
|
70
|
+
- `BiometricSignIn` - Biometric authentication UI
|
|
71
|
+
- `GoogleSignIn` - Google OAuth UI
|
|
72
|
+
- `Passkey` - Passkey authentication UI
|
|
73
|
+
- `SignOut` - Sign-out button
|
|
74
|
+
|
|
75
|
+
### Hooks (Custom Implementation)
|
|
76
|
+
|
|
77
|
+
- `useAuth()` - Get auth state
|
|
78
|
+
- `useSignInForm()` - Email/password sign-in
|
|
79
|
+
- `useSignUpForm()` - Email/password sign-up
|
|
80
|
+
- `useBiometric()` - Biometric auth
|
|
81
|
+
- `useSignOutUser()` - Sign-out logic
|
|
82
|
+
|
|
83
|
+
### Provider
|
|
84
|
+
|
|
85
|
+
- `AuthProvider` - Setup component
|
|
86
|
+
- `initializeAuth()` - Initialize auth
|
|
87
|
+
|
|
88
|
+
## Example App Structure
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import {
|
|
92
|
+
AuthProvider,
|
|
93
|
+
useAuth,
|
|
94
|
+
SignInScreen,
|
|
95
|
+
SignUpScreen,
|
|
96
|
+
} from "@wattsoft/auth-app";
|
|
97
|
+
|
|
98
|
+
function AuthStack() {
|
|
99
|
+
return (
|
|
100
|
+
<Stack.Navigator screenOptions={{ headerShown: false }}>
|
|
101
|
+
<Stack.Screen name="sign-in" component={SignInScreen} />
|
|
102
|
+
<Stack.Screen name="sign-up" component={SignUpScreen} />
|
|
103
|
+
</Stack.Navigator>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function AppStack() {
|
|
108
|
+
return (
|
|
109
|
+
<Stack.Navigator>
|
|
110
|
+
<Stack.Screen name="home" component={HomeScreen} />
|
|
111
|
+
</Stack.Navigator>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function RootNavigator() {
|
|
116
|
+
const { isLoaded, isSignedIn } = useAuth();
|
|
117
|
+
|
|
118
|
+
if (!isLoaded) return null;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<NavigationContainer>
|
|
122
|
+
{isSignedIn ? <AppStack /> : <AuthStack />}
|
|
123
|
+
</NavigationContainer>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default function App() {
|
|
128
|
+
return (
|
|
129
|
+
<AuthProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
|
|
130
|
+
<RootNavigator />
|
|
131
|
+
</AuthProvider>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Requirements
|
|
137
|
+
|
|
138
|
+
- React 18.0+
|
|
139
|
+
- React Native 0.70+
|
|
140
|
+
- Expo 50+
|
|
141
|
+
- Clerk account (free tier available)
|
|
142
|
+
|
|
143
|
+
## Installation with Dependencies
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
npm install @wattsoft/auth-app expo-router @clerk/clerk-expo \
|
|
147
|
+
expo-local-authentication expo-secure-store @react-navigation/native \
|
|
148
|
+
react-native-gesture-handler react-native-reanimated \
|
|
149
|
+
react-native-safe-area-context react-native-screens
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Setup Clerk
|
|
153
|
+
|
|
154
|
+
1. Create free account at [clerk.com](https://clerk.com)
|
|
155
|
+
2. Create new application
|
|
156
|
+
3. Get your Publishable Key
|
|
157
|
+
4. Add to your app
|
|
158
|
+
|
|
159
|
+
```tsx
|
|
160
|
+
<AuthProvider publishableKey="pk_test_your_key_here">
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## What's NOT Needed
|
|
164
|
+
|
|
165
|
+
❌ No authentication logic to write
|
|
166
|
+
❌ No form validation to build
|
|
167
|
+
❌ No secure storage setup
|
|
168
|
+
❌ No OAuth configuration code
|
|
169
|
+
❌ No biometric handling code
|
|
170
|
+
❌ No error handling to implement
|
|
171
|
+
|
|
172
|
+
✅ All included and ready to use!
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### `<AuthProvider />`
|
|
177
|
+
|
|
178
|
+
Wraps your app to enable authentication.
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<AuthProvider publishableKey={string}>{children}</AuthProvider>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### `useAuth()`
|
|
185
|
+
|
|
186
|
+
Returns current authentication state.
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
const { isLoaded, isSignedIn, user, session } = useAuth();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `useSignInForm()`
|
|
193
|
+
|
|
194
|
+
Returns sign-in methods.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
const { signInWithEmail, loading, error } = useSignInForm();
|
|
198
|
+
await signInWithEmail("user@example.com", "password");
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `useSignOutUser()`
|
|
202
|
+
|
|
203
|
+
Returns sign-out method.
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
const { signOutUser, loading } = useSignOutUser();
|
|
207
|
+
await signOutUser();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Browser Support
|
|
211
|
+
|
|
212
|
+
- ✅ iOS 11+
|
|
213
|
+
- ✅ Android 6+
|
|
214
|
+
- ✅ Web (experimental)
|
|
215
|
+
|
|
216
|
+
## TypeScript
|
|
217
|
+
|
|
218
|
+
Fully typed with TypeScript. All types exported from `@wattsoft/auth-app`.
|
|
219
|
+
|
|
220
|
+
```tsx
|
|
221
|
+
import type {
|
|
222
|
+
AuthState,
|
|
223
|
+
SignInResult,
|
|
224
|
+
UseSignInFormResult,
|
|
225
|
+
} from "@wattsoft/auth-app";
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Troubleshooting
|
|
229
|
+
|
|
230
|
+
See [SETUP_GUIDE.md#Troubleshooting](./SETUP_GUIDE.md#troubleshooting)
|
|
231
|
+
|
|
232
|
+
## Contributing
|
|
233
|
+
|
|
234
|
+
Found a bug? Have a feature request? Open an issue!
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
239
|
+
|
|
240
|
+
## Support
|
|
241
|
+
|
|
242
|
+
- 📧 Email: support@wattsoft.com
|
|
243
|
+
- 🐛 GitHub Issues: [Report a bug](https://github.com/wattsoft/auth-app/issues)
|
|
244
|
+
- 📖 Docs: [SETUP_GUIDE.md](./SETUP_GUIDE.md)
|
|
245
|
+
|
|
246
|
+
---
|
|
25
247
|
|
|
26
|
-
|
|
248
|
+
Made with ❤️ by Wattsoft
|
|
27
249
|
|
|
28
|
-
|
|
250
|
+
**Quick Links:**
|
|
29
251
|
|
|
30
|
-
|
|
252
|
+
- [npm Package](https://www.npmjs.com/package/@wattsoft/auth-app)
|
|
253
|
+
- [GitHub Repository](https://github.com/wattsoft/auth-app)
|
|
254
|
+
- [Clerk Documentation](https://clerk.com/docs)
|
|
31
255
|
|
|
32
256
|
```bash
|
|
33
257
|
npm run reset-project
|
package/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Export all components from the auth-app package
|
|
2
|
+
|
|
3
|
+
// ===== AUTHENTICATION PROVIDER & SETUP =====
|
|
4
|
+
export { AuthProvider, initializeAuth } from "./lib/AuthProvider";
|
|
5
|
+
|
|
6
|
+
// ===== CUSTOM HOOKS =====
|
|
7
|
+
export {
|
|
8
|
+
useAuth, useBiometric, useSignInForm, useSignOutUser, useSignUpForm
|
|
9
|
+
} from "./lib/hooks";
|
|
10
|
+
|
|
11
|
+
// ===== TYPE DEFINITIONS =====
|
|
12
|
+
export type {
|
|
13
|
+
AuthProviderProps,
|
|
14
|
+
AuthState, BiometricSignInProps, GoogleSignInProps,
|
|
15
|
+
PasskeyProps, SignInResult, SignOutButtonProps, SignOutResult, SignUpResult, UseBiometricResult, UseSignInFormResult, UseSignOutUserResult, UseSignUpFormResult
|
|
16
|
+
} from "./lib/types";
|
|
17
|
+
|
|
18
|
+
// ===== UI COMPONENTS =====
|
|
19
|
+
export {
|
|
20
|
+
default as BiometricSignIn,
|
|
21
|
+
saveCredentialsForBiometric
|
|
22
|
+
} from "./components/BiometricSignIn";
|
|
23
|
+
export {
|
|
24
|
+
default as GoogleSignIn,
|
|
25
|
+
useWarmUpBrowser
|
|
26
|
+
} from "./components/GoogleSignIn";
|
|
27
|
+
export { SignInWithPasskeyButton as Passkey } from "./components/Passkey";
|
|
28
|
+
export { SignOutButton as SignOut } from "./components/SignOut";
|
|
29
|
+
|
|
30
|
+
// ===== READY-TO-USE SCREENS =====
|
|
31
|
+
export { default as SignInScreen } from "./app/(app)/sign-in";
|
|
32
|
+
export { default as SignUpScreen } from "./app/(app)/sign-up";
|
|
33
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ClerkProvider } from "@clerk/clerk-expo";
|
|
2
|
+
import * as SecureStore from "expo-secure-store";
|
|
3
|
+
import React, { ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
interface AuthProviderProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
publishableKey: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AuthProvider - Setup component for the auth library
|
|
12
|
+
* Wrap your app with this provider
|
|
13
|
+
*
|
|
14
|
+
* Usage example:
|
|
15
|
+
* import { AuthProvider } from '@wattsoft/auth-app';
|
|
16
|
+
*
|
|
17
|
+
* export default function App() {
|
|
18
|
+
* return (
|
|
19
|
+
* <AuthProvider publishableKey={CLERK_PUBLISHABLE_KEY}>
|
|
20
|
+
* {children}
|
|
21
|
+
* </AuthProvider>
|
|
22
|
+
* );
|
|
23
|
+
* }
|
|
24
|
+
*/
|
|
25
|
+
export function AuthProvider({ children, publishableKey }: AuthProviderProps) {
|
|
26
|
+
const tokenCache = {
|
|
27
|
+
async getToken(key: string) {
|
|
28
|
+
try {
|
|
29
|
+
const item = await SecureStore.getItemAsync(key);
|
|
30
|
+
if (item) {
|
|
31
|
+
console.log(`${key} was used securely`);
|
|
32
|
+
} else {
|
|
33
|
+
console.log("No values stored under key: " + key);
|
|
34
|
+
}
|
|
35
|
+
return item;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
console.error("SecureStore read error: ", error);
|
|
38
|
+
await SecureStore.deleteItemAsync(key);
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async saveToken(key: string, value: string) {
|
|
43
|
+
try {
|
|
44
|
+
await SecureStore.setItemAsync(key, value);
|
|
45
|
+
} catch (err) {
|
|
46
|
+
console.error("SecureStore error: ", err);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<ClerkProvider publishableKey={publishableKey} tokenCache={tokenCache}>
|
|
53
|
+
{children}
|
|
54
|
+
</ClerkProvider>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize auth library - Call this once in your app
|
|
60
|
+
* @param publishableKey - Your Clerk publishable key from https://dashboard.clerk.com
|
|
61
|
+
* @returns AuthProvider component
|
|
62
|
+
*/
|
|
63
|
+
export function initializeAuth(publishableKey: string) {
|
|
64
|
+
if (!publishableKey) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
"Clerk publishable key is required. Get it from https://dashboard.clerk.com",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return publishableKey;
|
|
70
|
+
}
|
package/lib/hooks.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useClerk,
|
|
3
|
+
useAuth as useClerkAuth,
|
|
4
|
+
useSession,
|
|
5
|
+
useSignIn,
|
|
6
|
+
useSignUp,
|
|
7
|
+
useUser,
|
|
8
|
+
} from "@clerk/clerk-expo";
|
|
9
|
+
import * as LocalAuthentication from "expo-local-authentication";
|
|
10
|
+
import { useRouter } from "expo-router";
|
|
11
|
+
import React from "react";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* useAuth - Get current authentication state
|
|
15
|
+
* @returns Object with user, isLoaded, isSignedIn
|
|
16
|
+
*/
|
|
17
|
+
export function useAuth() {
|
|
18
|
+
const { isLoaded, isSignedIn } = useClerkAuth();
|
|
19
|
+
const { user } = useUser();
|
|
20
|
+
const { session } = useSession();
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
isLoaded,
|
|
24
|
+
isSignedIn,
|
|
25
|
+
user,
|
|
26
|
+
session,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* useSignInForm - Handle email/password sign in
|
|
32
|
+
* @returns Object with methods and state for sign in
|
|
33
|
+
*/
|
|
34
|
+
export function useSignInForm() {
|
|
35
|
+
const { signIn, setActive, isLoaded } = useSignIn();
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const [loading, setLoading] = React.useState(false);
|
|
38
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
const signInWithEmail = React.useCallback(
|
|
41
|
+
async (email: string, password: string) => {
|
|
42
|
+
if (!isLoaded) return;
|
|
43
|
+
setLoading(true);
|
|
44
|
+
setError(null);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const signInAttempt = await signIn.create({
|
|
48
|
+
identifier: email,
|
|
49
|
+
password,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (signInAttempt.status === "complete") {
|
|
53
|
+
await setActive({
|
|
54
|
+
session: signInAttempt.createdSessionId,
|
|
55
|
+
});
|
|
56
|
+
router.replace("/");
|
|
57
|
+
return { success: true };
|
|
58
|
+
} else {
|
|
59
|
+
setError("Sign in incomplete. Please try again.");
|
|
60
|
+
return { success: false, status: signInAttempt.status };
|
|
61
|
+
}
|
|
62
|
+
} catch (err: any) {
|
|
63
|
+
const errorMessage =
|
|
64
|
+
err?.errors?.[0]?.message || err.message || "Sign in failed";
|
|
65
|
+
setError(errorMessage);
|
|
66
|
+
return { success: false, error: errorMessage };
|
|
67
|
+
} finally {
|
|
68
|
+
setLoading(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[isLoaded, signIn, setActive, router],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
signInWithEmail,
|
|
76
|
+
loading,
|
|
77
|
+
error,
|
|
78
|
+
setError,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* useSignUpForm - Handle email/password sign up
|
|
84
|
+
* @returns Object with methods and state for sign up
|
|
85
|
+
*/
|
|
86
|
+
export function useSignUpForm() {
|
|
87
|
+
const { signUp, setActive, isLoaded } = useSignUp();
|
|
88
|
+
const router = useRouter();
|
|
89
|
+
const [loading, setLoading] = React.useState(false);
|
|
90
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
91
|
+
|
|
92
|
+
const signUpWithEmail = React.useCallback(
|
|
93
|
+
async (
|
|
94
|
+
email: string,
|
|
95
|
+
password: string,
|
|
96
|
+
firstName?: string,
|
|
97
|
+
lastName?: string,
|
|
98
|
+
) => {
|
|
99
|
+
if (!isLoaded) return;
|
|
100
|
+
setLoading(true);
|
|
101
|
+
setError(null);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const signUpAttempt = await signUp.create({
|
|
105
|
+
emailAddress: email,
|
|
106
|
+
password,
|
|
107
|
+
firstName,
|
|
108
|
+
lastName,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (signUpAttempt.status === "complete") {
|
|
112
|
+
await setActive?.({
|
|
113
|
+
session: signUpAttempt.createdSessionId,
|
|
114
|
+
});
|
|
115
|
+
router.replace("/");
|
|
116
|
+
return { success: true };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
router.push({
|
|
120
|
+
pathname: "/sign-up",
|
|
121
|
+
params: { email },
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
return { success: true };
|
|
125
|
+
} catch (err: any) {
|
|
126
|
+
const errorMessage =
|
|
127
|
+
err?.errors?.[0]?.message || err.message || "Sign up failed";
|
|
128
|
+
setError(errorMessage);
|
|
129
|
+
return { success: false, error: errorMessage };
|
|
130
|
+
} finally {
|
|
131
|
+
setLoading(false);
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
[isLoaded, signUp, router, setActive],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
signUpWithEmail,
|
|
139
|
+
loading,
|
|
140
|
+
error,
|
|
141
|
+
setError,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* useBiometric - Handle biometric authentication
|
|
147
|
+
* @returns Object with methods for biometric sign in
|
|
148
|
+
*/
|
|
149
|
+
export function useBiometric() {
|
|
150
|
+
const { signIn, setActive, isLoaded } = useSignIn();
|
|
151
|
+
const router = useRouter();
|
|
152
|
+
const [loading, setLoading] = React.useState(false);
|
|
153
|
+
const [error, setError] = React.useState<string | null>(null);
|
|
154
|
+
const [isBiometricAvailable, setIsBiometricAvailable] = React.useState(false);
|
|
155
|
+
|
|
156
|
+
React.useEffect(() => {
|
|
157
|
+
checkBiometricAvailability();
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const checkBiometricAvailability = async () => {
|
|
161
|
+
try {
|
|
162
|
+
const compatible = await LocalAuthentication.hasHardwareAsync();
|
|
163
|
+
const enrolled = await LocalAuthentication.isEnrolledAsync();
|
|
164
|
+
setIsBiometricAvailable(compatible && enrolled);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
setIsBiometricAvailable(false);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const signInWithBiometric = React.useCallback(
|
|
171
|
+
async (email: string, password: string) => {
|
|
172
|
+
if (!isLoaded) return;
|
|
173
|
+
setLoading(true);
|
|
174
|
+
setError(null);
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const authenticated = await LocalAuthentication.authenticateAsync({
|
|
178
|
+
disableDeviceFallback: false,
|
|
179
|
+
fallbackLabel: "Use passcode",
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
if (authenticated.success) {
|
|
183
|
+
const signInAttempt = await signIn.create({
|
|
184
|
+
identifier: email,
|
|
185
|
+
password,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (signInAttempt.status === "complete") {
|
|
189
|
+
await setActive({
|
|
190
|
+
session: signInAttempt.createdSessionId,
|
|
191
|
+
});
|
|
192
|
+
router.replace("/");
|
|
193
|
+
return { success: true };
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
setError("Biometric authentication cancelled");
|
|
197
|
+
return { success: false, error: "Cancelled" };
|
|
198
|
+
}
|
|
199
|
+
} catch (err: any) {
|
|
200
|
+
const errorMessage = err?.message || "Biometric sign in failed";
|
|
201
|
+
setError(errorMessage);
|
|
202
|
+
return { success: false, error: errorMessage };
|
|
203
|
+
} finally {
|
|
204
|
+
setLoading(false);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
[isLoaded, signIn, setActive, router],
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
signInWithBiometric,
|
|
212
|
+
isBiometricAvailable,
|
|
213
|
+
loading,
|
|
214
|
+
error,
|
|
215
|
+
setError,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* useSignOutUser - Handle user sign out
|
|
221
|
+
* @returns Object with sign out method
|
|
222
|
+
*/
|
|
223
|
+
export function useSignOutUser() {
|
|
224
|
+
const { signOut } = useClerk();
|
|
225
|
+
const router = useRouter();
|
|
226
|
+
const [loading, setLoading] = React.useState(false);
|
|
227
|
+
|
|
228
|
+
const signOutUser = React.useCallback(async () => {
|
|
229
|
+
setLoading(true);
|
|
230
|
+
try {
|
|
231
|
+
await signOut();
|
|
232
|
+
router.replace("/sign-in");
|
|
233
|
+
return { success: true };
|
|
234
|
+
} catch (err: any) {
|
|
235
|
+
return { success: false, error: err.message };
|
|
236
|
+
} finally {
|
|
237
|
+
setLoading(false);
|
|
238
|
+
}
|
|
239
|
+
}, [signOut, router]);
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
signOutUser,
|
|
243
|
+
loading,
|
|
244
|
+
};
|
|
245
|
+
}
|
package/lib/types.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for @wattsoft/auth-app
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ReactNode } from "react";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Props for AuthProvider component
|
|
9
|
+
*/
|
|
10
|
+
export interface AuthProviderProps {
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
publishableKey: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Return type for sign-in operations
|
|
17
|
+
*/
|
|
18
|
+
export interface SignInResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
status?: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
createdSessionId?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Return type for sign-up operations
|
|
27
|
+
*/
|
|
28
|
+
export interface SignUpResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
error?: string;
|
|
31
|
+
emailAddress?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Return type for sign-out operations
|
|
36
|
+
*/
|
|
37
|
+
export interface SignOutResult {
|
|
38
|
+
success: boolean;
|
|
39
|
+
error?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Authentication state from useAuth hook
|
|
44
|
+
*/
|
|
45
|
+
export interface AuthState {
|
|
46
|
+
isLoaded: boolean;
|
|
47
|
+
isSignedIn: boolean | undefined;
|
|
48
|
+
user: any;
|
|
49
|
+
session: any;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sign-in form hook return type
|
|
54
|
+
*/
|
|
55
|
+
export interface UseSignInFormResult {
|
|
56
|
+
signInWithEmail: (email: string, password: string) => Promise<SignInResult>;
|
|
57
|
+
loading: boolean;
|
|
58
|
+
error: string | null;
|
|
59
|
+
setError: (error: string | null) => void;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Sign-up form hook return type
|
|
64
|
+
*/
|
|
65
|
+
export interface UseSignUpFormResult {
|
|
66
|
+
signUpWithEmail: (
|
|
67
|
+
email: string,
|
|
68
|
+
password: string,
|
|
69
|
+
firstName?: string,
|
|
70
|
+
lastName?: string,
|
|
71
|
+
) => Promise<SignUpResult>;
|
|
72
|
+
loading: boolean;
|
|
73
|
+
error: string | null;
|
|
74
|
+
setError: (error: string | null) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Biometric authentication hook return type
|
|
79
|
+
*/
|
|
80
|
+
export interface UseBiometricResult {
|
|
81
|
+
signInWithBiometric: (
|
|
82
|
+
email: string,
|
|
83
|
+
password: string,
|
|
84
|
+
) => Promise<SignInResult>;
|
|
85
|
+
isBiometricAvailable: boolean;
|
|
86
|
+
loading: boolean;
|
|
87
|
+
error: string | null;
|
|
88
|
+
setError: (error: string | null) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Sign-out hook return type
|
|
93
|
+
*/
|
|
94
|
+
export interface UseSignOutUserResult {
|
|
95
|
+
signOutUser: () => Promise<SignOutResult>;
|
|
96
|
+
loading: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Google Sign-In props
|
|
101
|
+
*/
|
|
102
|
+
export interface GoogleSignInProps {
|
|
103
|
+
onSuccess?: (result: any) => void;
|
|
104
|
+
onError?: (error: any) => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Passkey authentication props
|
|
109
|
+
*/
|
|
110
|
+
export interface PasskeyProps {
|
|
111
|
+
onSuccess?: (result: any) => void;
|
|
112
|
+
onError?: (error: any) => void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sign-out button props
|
|
117
|
+
*/
|
|
118
|
+
export interface SignOutButtonProps {
|
|
119
|
+
onSuccess?: () => void;
|
|
120
|
+
onError?: (error: Error) => void;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Biometric sign-in component props
|
|
125
|
+
*/
|
|
126
|
+
export interface BiometricSignInProps {
|
|
127
|
+
onSuccess?: (result: any) => void;
|
|
128
|
+
onError?: (error: Error) => void;
|
|
129
|
+
email?: string;
|
|
130
|
+
password?: string;
|
|
131
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wattsoft/auth-app",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Reusable authentication components for Expo/React Native apps with Clerk, biometric, Google sign-in, and passkey support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Wattsoft",
|
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
49
49
|
"components",
|
|
50
|
+
"lib",
|
|
50
51
|
"app",
|
|
51
|
-
"assets"
|
|
52
|
+
"assets",
|
|
53
|
+
"index.ts"
|
|
52
54
|
],
|
|
53
55
|
"keywords": [
|
|
54
56
|
"react",
|