@wattsoft/auth-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # Welcome to your Expo app 👋
2
+
3
+ This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4
+
5
+ ## Get started
6
+
7
+ 1. Install dependencies
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ 2. Start the app
14
+
15
+ ```bash
16
+ npx expo start
17
+ ```
18
+
19
+ In the output, you'll find options to open the app in a
20
+
21
+ - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22
+ - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23
+ - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24
+ - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25
+
26
+ You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27
+
28
+ ## Get a fresh project
29
+
30
+ When you're ready, run:
31
+
32
+ ```bash
33
+ npm run reset-project
34
+ ```
35
+
36
+ This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37
+
38
+ ## Learn more
39
+
40
+ To learn more about developing your project with Expo, look at the following resources:
41
+
42
+ - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43
+ - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44
+
45
+ ## Join the community
46
+
47
+ Join our community of developers creating universal apps.
48
+
49
+ - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50
+ - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
@@ -0,0 +1,16 @@
1
+ import { Tabs } from "expo-router";
2
+ import React from "react";
3
+ import { StyleSheet } from "react-native";
4
+
5
+ const Layout = () => {
6
+ return (
7
+ <Tabs>
8
+ <Tabs.Screen name="index" options={{ title: "Home" }} />
9
+ <Tabs.Screen name="profile" options={{ title: "Profile" }} />
10
+ </Tabs>
11
+ );
12
+ };
13
+
14
+ export default Layout;
15
+
16
+ const styles = StyleSheet.create({});
@@ -0,0 +1,31 @@
1
+ import { useUser } from "@clerk/clerk-expo";
2
+ import React from "react";
3
+ import { StyleSheet, Text, View } from "react-native";
4
+
5
+ const Index = () => {
6
+ const { user } = useUser();
7
+ const fullName =
8
+ [user?.firstName, user?.lastName].filter(Boolean).join(" ") || "User";
9
+
10
+ return (
11
+ <View style={styles.container}>
12
+ <Text style={styles.welcome}>Welcome, {fullName} 👋</Text>
13
+ </View>
14
+ );
15
+ };
16
+
17
+ export default Index;
18
+
19
+ const styles = StyleSheet.create({
20
+ container: {
21
+ flex: 1,
22
+ backgroundColor: "#f4f6f8",
23
+ justifyContent: "center",
24
+ alignItems: "center",
25
+ },
26
+ welcome: {
27
+ fontSize: 22,
28
+ fontWeight: "600",
29
+ color: "#333",
30
+ },
31
+ });
@@ -0,0 +1,62 @@
1
+ import { SignOutButton } from "@/components/SignOut";
2
+ import { useUser } from "@clerk/clerk-expo";
3
+ import React from "react";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+
6
+ const Profile = () => {
7
+ const { user } = useUser();
8
+ const email = user?.primaryEmailAddress?.emailAddress;
9
+ const initials = email ? email[0].toUpperCase() : "?";
10
+
11
+ return (
12
+ <View style={styles.container}>
13
+ <View style={styles.avatar}>
14
+ <Text style={styles.avatarText}>{initials}</Text>
15
+ </View>
16
+
17
+ <Text style={styles.name}>{email}</Text>
18
+ <Text style={styles.label}>Email address</Text>
19
+
20
+ <View style={{ width: "100%" }}>
21
+ <SignOutButton />
22
+ </View>
23
+ </View>
24
+ );
25
+ };
26
+
27
+ export default Profile;
28
+
29
+ const styles = StyleSheet.create({
30
+ container: {
31
+ flex: 1,
32
+ padding: 24,
33
+ alignItems: "center",
34
+ backgroundColor: "#f8fafc",
35
+ paddingTop: 60,
36
+ },
37
+ avatar: {
38
+ width: 80,
39
+ height: 80,
40
+ borderRadius: 40,
41
+ backgroundColor: "#0a7ea4",
42
+ justifyContent: "center",
43
+ alignItems: "center",
44
+ marginBottom: 16,
45
+ },
46
+ avatarText: {
47
+ color: "#fff",
48
+ fontSize: 32,
49
+ fontWeight: "bold",
50
+ },
51
+ name: {
52
+ fontSize: 18,
53
+ fontWeight: "700",
54
+ color: "#0f172a",
55
+ marginBottom: 4,
56
+ },
57
+ label: {
58
+ fontSize: 13,
59
+ color: "#94a3b8",
60
+ marginBottom: 32,
61
+ },
62
+ });
@@ -0,0 +1,37 @@
1
+ import { useAuth } from "@clerk/clerk-expo";
2
+ import { Stack } from "expo-router";
3
+ import React from "react";
4
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
5
+
6
+ const Layout = () => {
7
+ const { isLoaded, isSignedIn } = useAuth();
8
+
9
+ if (!isLoaded) {
10
+ return (
11
+ <View style={styles.container}>
12
+ <ActivityIndicator size="large" color="blue" />
13
+ </View>
14
+ );
15
+ }
16
+ return (
17
+ <Stack screenOptions={{ headerShown: false }}>
18
+ <Stack.Protected guard={isSignedIn}>
19
+ <Stack.Screen name="(tabs)" />
20
+ </Stack.Protected>
21
+ <Stack.Protected guard={!isSignedIn}>
22
+ <Stack.Screen name="sign-in" />
23
+ <Stack.Screen name="sign-up" />
24
+ </Stack.Protected>
25
+ </Stack>
26
+ );
27
+ };
28
+
29
+ export default Layout;
30
+
31
+ const styles = StyleSheet.create({
32
+ container: {
33
+ flex: 1,
34
+ justifyContent: "center",
35
+ alignItems: "center",
36
+ },
37
+ });
@@ -0,0 +1,230 @@
1
+ import BiometricSignIn, {
2
+ saveCredentialsForBiometric,
3
+ } from "@/components/BiometricSignIn";
4
+ import GoogleSignIn from "@/components/GoogleSignIn";
5
+ import { SignInWithPasskeyButton } from "@/components/Passkey";
6
+ import { useSignIn } from "@clerk/clerk-expo";
7
+ import type { EmailCodeFactor } from "@clerk/types";
8
+ import { Link, useRouter } from "expo-router";
9
+ import * as React from "react";
10
+ import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
11
+ import { SafeAreaView } from "react-native-safe-area-context";
12
+
13
+ export default function Page() {
14
+ const { signIn, setActive, isLoaded } = useSignIn();
15
+ const router = useRouter();
16
+
17
+ const [emailAddress, setEmailAddress] = React.useState("");
18
+ const [password, setPassword] = React.useState("");
19
+ const [code, setCode] = React.useState("");
20
+ const [showEmailCode, setShowEmailCode] = React.useState(false);
21
+
22
+ const onSignInPress = React.useCallback(async () => {
23
+ if (!isLoaded) return;
24
+
25
+ try {
26
+ const signInAttempt = await signIn.create({
27
+ identifier: emailAddress,
28
+ password,
29
+ });
30
+
31
+ if (signInAttempt.status === "complete") {
32
+ await setActive({
33
+ session: signInAttempt.createdSessionId,
34
+ });
35
+ await saveCredentialsForBiometric(emailAddress, password);
36
+ router.replace("/");
37
+ } else if (signInAttempt.status === "needs_second_factor") {
38
+ const emailCodeFactor = signInAttempt.supportedSecondFactors?.find(
39
+ (factor): factor is EmailCodeFactor =>
40
+ factor.strategy === "email_code",
41
+ );
42
+
43
+ if (emailCodeFactor) {
44
+ await signIn.prepareSecondFactor({
45
+ strategy: "email_code",
46
+ emailAddressId: emailCodeFactor.emailAddressId,
47
+ });
48
+ setShowEmailCode(true);
49
+ }
50
+ } else {
51
+ console.error(JSON.stringify(signInAttempt, null, 2));
52
+ }
53
+ } catch (err) {
54
+ console.error(JSON.stringify(err, null, 2));
55
+ }
56
+ }, [isLoaded, signIn, setActive, router, emailAddress, password]);
57
+
58
+ const onVerifyPress = React.useCallback(async () => {
59
+ if (!isLoaded) return;
60
+
61
+ try {
62
+ const signInAttempt = await signIn.attemptSecondFactor({
63
+ strategy: "email_code",
64
+ code,
65
+ });
66
+
67
+ if (signInAttempt.status === "complete") {
68
+ await setActive({
69
+ session: signInAttempt.createdSessionId,
70
+ });
71
+ router.replace("/");
72
+ } else {
73
+ console.error(JSON.stringify(signInAttempt, null, 2));
74
+ }
75
+ } catch (err) {
76
+ console.error(JSON.stringify(err, null, 2));
77
+ }
78
+ }, [isLoaded, signIn, setActive, router, code]);
79
+
80
+ if (showEmailCode) {
81
+ return (
82
+ <View style={styles.container}>
83
+ <Text style={styles.title}>Verify your email</Text>
84
+ <Text style={styles.description}>
85
+ A verification code has been sent to your email.
86
+ </Text>
87
+
88
+ <TextInput
89
+ style={styles.input}
90
+ value={code}
91
+ placeholder="Enter verification code"
92
+ placeholderTextColor="#666"
93
+ onChangeText={setCode}
94
+ keyboardType="numeric"
95
+ />
96
+
97
+ <Pressable
98
+ style={({ pressed }) => [
99
+ styles.button,
100
+ pressed && styles.buttonPressed,
101
+ ]}
102
+ onPress={onVerifyPress}
103
+ >
104
+ <Text style={styles.buttonText}>Verify</Text>
105
+ </Pressable>
106
+ </View>
107
+ );
108
+ }
109
+
110
+ return (
111
+ <SafeAreaView style={styles.container}>
112
+ <Text style={styles.title}>Sign in</Text>
113
+
114
+ <Text style={styles.label}>Email address</Text>
115
+ <TextInput
116
+ style={styles.input}
117
+ autoCapitalize="none"
118
+ value={emailAddress}
119
+ placeholder="Enter email"
120
+ placeholderTextColor="#666"
121
+ onChangeText={setEmailAddress}
122
+ keyboardType="email-address"
123
+ />
124
+
125
+ <Text style={styles.label}>Password</Text>
126
+ <TextInput
127
+ style={styles.input}
128
+ value={password}
129
+ placeholder="Enter password"
130
+ placeholderTextColor="#666"
131
+ secureTextEntry
132
+ onChangeText={setPassword}
133
+ />
134
+
135
+ <Pressable
136
+ style={({ pressed }) => [
137
+ styles.button,
138
+ (!emailAddress || !password) && styles.buttonDisabled,
139
+ pressed && styles.buttonPressed,
140
+ ]}
141
+ onPress={onSignInPress}
142
+ disabled={!emailAddress || !password}
143
+ >
144
+ <Text style={styles.buttonText}>Sign in</Text>
145
+ </Pressable>
146
+
147
+ <View style={styles.linkContainer}>
148
+ <Text>Don't have an account? </Text>
149
+ <Link href="/sign-up">
150
+ <Text style={styles.link}>Sign up</Text>
151
+ </Link>
152
+ </View>
153
+ <View style={styles.orContainer}>
154
+ <Text style={styles.orText}>OR</Text>
155
+ </View>
156
+ <GoogleSignIn />
157
+ <BiometricSignIn />
158
+ <View>
159
+ <SignInWithPasskeyButton />
160
+ </View>
161
+ </SafeAreaView>
162
+ );
163
+ }
164
+
165
+ const styles = StyleSheet.create({
166
+ orContainer: {
167
+ justifyContent: "center",
168
+ alignItems: "center",
169
+ marginVertical: 8,
170
+ },
171
+ orText: {
172
+ fontSize: 16,
173
+ fontWeight: "600",
174
+ },
175
+ container: {
176
+ flex: 1,
177
+ padding: 20,
178
+ gap: 12,
179
+ },
180
+ title: {
181
+ fontSize: 24,
182
+ fontWeight: "700",
183
+ marginBottom: 8,
184
+ },
185
+ description: {
186
+ fontSize: 14,
187
+ marginBottom: 16,
188
+ opacity: 0.8,
189
+ },
190
+ label: {
191
+ fontWeight: "600",
192
+ fontSize: 14,
193
+ },
194
+ input: {
195
+ borderWidth: 1,
196
+ borderColor: "#ccc",
197
+ borderRadius: 8,
198
+ padding: 12,
199
+ fontSize: 16,
200
+ backgroundColor: "#fff",
201
+ },
202
+ button: {
203
+ backgroundColor: "#0a7ea4",
204
+ paddingVertical: 12,
205
+ paddingHorizontal: 24,
206
+ borderRadius: 8,
207
+ alignItems: "center",
208
+ marginTop: 8,
209
+ },
210
+ buttonPressed: {
211
+ opacity: 0.7,
212
+ },
213
+ buttonDisabled: {
214
+ opacity: 0.5,
215
+ },
216
+ buttonText: {
217
+ color: "#fff",
218
+ fontWeight: "600",
219
+ },
220
+ linkContainer: {
221
+ flexDirection: "row",
222
+ gap: 4,
223
+ marginTop: 12,
224
+ alignItems: "center",
225
+ },
226
+ link: {
227
+ color: "#0a7ea4",
228
+ fontWeight: "600",
229
+ },
230
+ });
@@ -0,0 +1,172 @@
1
+ import { useSignUp } from "@clerk/clerk-expo";
2
+ import { Link, useRouter } from "expo-router";
3
+ import React from "react";
4
+ import { Pressable, StyleSheet, Text, TextInput, View } from "react-native";
5
+
6
+ export default function Page() {
7
+ const { isLoaded, signUp, setActive } = useSignUp();
8
+ const router = useRouter();
9
+
10
+ const [emailAddress, setEmailAddress] = React.useState("");
11
+ const [password, setPassword] = React.useState("");
12
+ const [pendingVerification, setPendingVerification] = React.useState(false);
13
+ const [code, setCode] = React.useState("");
14
+
15
+ const onSignUpPress = async () => {
16
+ if (!isLoaded) return;
17
+
18
+ try {
19
+ await signUp.create({
20
+ emailAddress,
21
+ password,
22
+ });
23
+
24
+ await signUp.prepareEmailAddressVerification({
25
+ strategy: "email_code",
26
+ });
27
+
28
+ setPendingVerification(true);
29
+ } catch (err) {
30
+ console.error(JSON.stringify(err, null, 2));
31
+ }
32
+ };
33
+
34
+ const onVerifyPress = async () => {
35
+ if (!isLoaded) return;
36
+
37
+ try {
38
+ const signUpAttempt = await signUp.attemptEmailAddressVerification({
39
+ code,
40
+ });
41
+
42
+ if (signUpAttempt.status === "complete") {
43
+ await setActive({
44
+ session: signUpAttempt.createdSessionId,
45
+ });
46
+
47
+ router.replace("/");
48
+ } else {
49
+ console.error(JSON.stringify(signUpAttempt, null, 2));
50
+ }
51
+ } catch (err) {
52
+ console.error(JSON.stringify(err, null, 2));
53
+ }
54
+ };
55
+
56
+ if (pendingVerification) {
57
+ return (
58
+ <View style={styles.container}>
59
+ <Text style={styles.title}>Verify your email</Text>
60
+ <Text style={styles.description}>
61
+ A verification code has been sent to your email.
62
+ </Text>
63
+
64
+ <TextInput
65
+ style={styles.input}
66
+ value={code}
67
+ placeholder="Enter verification code"
68
+ onChangeText={setCode}
69
+ keyboardType="numeric"
70
+ />
71
+
72
+ <Pressable style={styles.button} onPress={onVerifyPress}>
73
+ <Text style={styles.buttonText}>Verify</Text>
74
+ </Pressable>
75
+ </View>
76
+ );
77
+ }
78
+
79
+ return (
80
+ <View style={styles.container}>
81
+ <Text style={styles.title}>Sign Up</Text>
82
+
83
+ <Text style={styles.label}>Email</Text>
84
+ <TextInput
85
+ style={styles.input}
86
+ autoCapitalize="none"
87
+ value={emailAddress}
88
+ placeholder="Enter email"
89
+ onChangeText={setEmailAddress}
90
+ keyboardType="email-address"
91
+ />
92
+
93
+ <Text style={styles.label}>Password</Text>
94
+ <TextInput
95
+ style={styles.input}
96
+ value={password}
97
+ placeholder="Enter password"
98
+ secureTextEntry
99
+ onChangeText={setPassword}
100
+ />
101
+
102
+ <Pressable
103
+ style={[
104
+ styles.button,
105
+ (!emailAddress || !password) && styles.buttonDisabled,
106
+ ]}
107
+ onPress={onSignUpPress}
108
+ disabled={!emailAddress || !password}
109
+ >
110
+ <Text style={styles.buttonText}>Continue</Text>
111
+ </Pressable>
112
+
113
+ <View style={styles.linkContainer}>
114
+ <Text>Already have an account? </Text>
115
+ <Link href="/sign-in">
116
+ <Text style={styles.linkText}>Sign In</Text>
117
+ </Link>
118
+ </View>
119
+ </View>
120
+ );
121
+ }
122
+
123
+ const styles = StyleSheet.create({
124
+ container: {
125
+ flex: 1,
126
+ padding: 20,
127
+ justifyContent: "center",
128
+ },
129
+ title: {
130
+ fontSize: 24,
131
+ fontWeight: "bold",
132
+ marginBottom: 20,
133
+ },
134
+ description: {
135
+ marginBottom: 20,
136
+ color: "gray",
137
+ },
138
+ label: {
139
+ fontWeight: "600",
140
+ marginTop: 10,
141
+ },
142
+ input: {
143
+ borderWidth: 1,
144
+ borderColor: "#ccc",
145
+ borderRadius: 8,
146
+ padding: 12,
147
+ marginTop: 5,
148
+ },
149
+ button: {
150
+ backgroundColor: "#0a7ea4",
151
+ padding: 14,
152
+ borderRadius: 8,
153
+ alignItems: "center",
154
+ marginTop: 20,
155
+ },
156
+ buttonDisabled: {
157
+ opacity: 0.5,
158
+ },
159
+ buttonText: {
160
+ color: "#fff",
161
+ fontWeight: "600",
162
+ },
163
+ linkContainer: {
164
+ flexDirection: "row",
165
+ marginTop: 20,
166
+ justifyContent: "center",
167
+ },
168
+ linkText: {
169
+ color: "#0a7ea4",
170
+ fontWeight: "600",
171
+ },
172
+ });
@@ -0,0 +1,14 @@
1
+ import { ClerkProvider } from "@clerk/clerk-expo";
2
+ import { tokenCache } from "@clerk/clerk-expo/token-cache";
3
+ import { Slot } from "expo-router";
4
+
5
+ export default function Layout() {
6
+ return (
7
+ <ClerkProvider
8
+ tokenCache={tokenCache}
9
+ publishableKey={process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY}
10
+ >
11
+ <Slot />
12
+ </ClerkProvider>
13
+ );
14
+ }
package/app.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "expo": {
3
+ "name": "auth-app",
4
+ "slug": "auth-app",
5
+ "version": "1.0.0",
6
+ "orientation": "portrait",
7
+ "icon": "./assets/images/icon.png",
8
+ "scheme": "authapp",
9
+ "userInterfaceStyle": "automatic",
10
+ "newArchEnabled": true,
11
+ "ios": {
12
+ "supportsTablet": true
13
+ },
14
+ "android": {
15
+ "adaptiveIcon": {
16
+ "backgroundColor": "#E6F4FE",
17
+ "foregroundImage": "./assets/images/android-icon-foreground.png",
18
+ "backgroundImage": "./assets/images/android-icon-background.png",
19
+ "monochromeImage": "./assets/images/android-icon-monochrome.png"
20
+ },
21
+ "edgeToEdgeEnabled": true,
22
+ "predictiveBackGestureEnabled": false,
23
+ "package": "com.anonymous.authapp"
24
+ },
25
+ "web": {
26
+ "output": "static",
27
+ "favicon": "./assets/images/favicon.png"
28
+ },
29
+ "plugins": [
30
+ "expo-router",
31
+ [
32
+ "expo-splash-screen",
33
+ {
34
+ "image": "./assets/images/splash-icon.png",
35
+ "imageWidth": 200,
36
+ "resizeMode": "contain",
37
+ "backgroundColor": "#ffffff",
38
+ "dark": {
39
+ "backgroundColor": "#000000"
40
+ }
41
+ }
42
+ ]
43
+ ],
44
+ "experiments": {
45
+ "typedRoutes": true,
46
+ "reactCompiler": true
47
+ }
48
+ }
49
+ }
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,104 @@
1
+ import { useSignIn } from "@clerk/clerk-expo";
2
+ import * as LocalAuthentication from "expo-local-authentication";
3
+ import { useRouter } from "expo-router";
4
+ import * as SecureStore from "expo-secure-store";
5
+ import React from "react";
6
+ import { Alert, Pressable, StyleSheet, Text } from "react-native";
7
+
8
+ const CREDENTIALS_KEY = "biometric_credentials";
9
+
10
+ // Call this after a successful normal sign-in to save credentials
11
+ export async function saveCredentialsForBiometric(
12
+ email: string,
13
+ password: string,
14
+ ) {
15
+ await SecureStore.setItemAsync(
16
+ CREDENTIALS_KEY,
17
+ JSON.stringify({ email, password }),
18
+ );
19
+ }
20
+
21
+ export default function BiometricSignIn() {
22
+ const { signIn, setActive, isLoaded } = useSignIn();
23
+ const router = useRouter();
24
+
25
+ const handleBiometricAuth = async () => {
26
+ if (!isLoaded) return;
27
+
28
+ try {
29
+ // 1. Check hardware support
30
+ const compatible = await LocalAuthentication.hasHardwareAsync();
31
+ if (!compatible) {
32
+ Alert.alert("Error", "Biometric hardware not available");
33
+ return;
34
+ }
35
+
36
+ // 2. Check enrollment
37
+ const enrolled = await LocalAuthentication.isEnrolledAsync();
38
+ if (!enrolled) {
39
+ Alert.alert("Error", "No biometrics enrolled on this device");
40
+ return;
41
+ }
42
+
43
+ // 3. Check saved credentials
44
+ const stored = await SecureStore.getItemAsync(CREDENTIALS_KEY);
45
+ if (!stored) {
46
+ Alert.alert(
47
+ "No saved credentials",
48
+ "Please sign in with email and password first to enable biometric sign-in.",
49
+ );
50
+ return;
51
+ }
52
+
53
+ // 4. Run biometric prompt
54
+ const result = await LocalAuthentication.authenticateAsync({
55
+ promptMessage: "Authenticate to sign in",
56
+ fallbackLabel: "Use Passcode",
57
+ });
58
+
59
+ if (!result.success) {
60
+ Alert.alert("Failed", "Biometric authentication failed");
61
+ return;
62
+ }
63
+
64
+ // 5. Sign into Clerk with saved credentials
65
+ const { email, password } = JSON.parse(stored);
66
+
67
+ const signInAttempt = await signIn.create({
68
+ identifier: email,
69
+ password,
70
+ });
71
+
72
+ if (signInAttempt.status === "complete") {
73
+ await setActive({ session: signInAttempt.createdSessionId });
74
+ router.replace("/");
75
+ } else {
76
+ Alert.alert("Error", "Sign in incomplete. Please sign in manually.");
77
+ }
78
+ } catch (error) {
79
+ Alert.alert("Error", "Something went wrong");
80
+ console.error(error);
81
+ }
82
+ };
83
+
84
+ return (
85
+ <Pressable style={styles.button} onPress={handleBiometricAuth}>
86
+ <Text style={styles.buttonText}>Sign in with Fingerprint</Text>
87
+ </Pressable>
88
+ );
89
+ }
90
+
91
+ const styles = StyleSheet.create({
92
+ button: {
93
+ backgroundColor: "#0a7ea4",
94
+ paddingVertical: 12,
95
+ paddingHorizontal: 24,
96
+ borderRadius: 8,
97
+ alignItems: "center",
98
+ marginTop: 12,
99
+ },
100
+ buttonText: {
101
+ color: "#fff",
102
+ fontWeight: "600",
103
+ },
104
+ });
@@ -0,0 +1,81 @@
1
+ import { useSSO } from "@clerk/clerk-expo";
2
+ import * as AuthSession from "expo-auth-session";
3
+ import { useRouter } from "expo-router";
4
+ import * as WebBrowser from "expo-web-browser";
5
+ import React, { useCallback, useEffect } from "react";
6
+ import { Platform, Pressable, StyleSheet, Text, View } from "react-native";
7
+
8
+ // Preload browser for Android
9
+ export const useWarmUpBrowser = () => {
10
+ useEffect(() => {
11
+ if (Platform.OS !== "android") return;
12
+ void WebBrowser.warmUpAsync();
13
+
14
+ return () => {
15
+ void WebBrowser.coolDownAsync();
16
+ };
17
+ }, []);
18
+ };
19
+
20
+ WebBrowser.maybeCompleteAuthSession();
21
+
22
+ export default function GoogleSignIn() {
23
+ useWarmUpBrowser();
24
+
25
+ const router = useRouter();
26
+ const { startSSOFlow } = useSSO();
27
+
28
+ const onPress = useCallback(async () => {
29
+ try {
30
+ const { createdSessionId, setActive } = await startSSOFlow({
31
+ strategy: "oauth_google",
32
+ redirectUrl: AuthSession.makeRedirectUri(),
33
+ });
34
+
35
+ if (createdSessionId) {
36
+ await setActive?.({
37
+ session: createdSessionId,
38
+ });
39
+
40
+ // router.replace("/");
41
+ }
42
+ } catch (err) {
43
+ console.error(JSON.stringify(err, null, 2));
44
+ }
45
+ }, [startSSOFlow, router]);
46
+
47
+ return (
48
+ <View style={styles.container}>
49
+ <Pressable
50
+ style={({ pressed }) => [
51
+ styles.button,
52
+ pressed && styles.buttonPressed,
53
+ ]}
54
+ onPress={onPress}
55
+ >
56
+ <Text style={styles.buttonText}>Sign in with Google</Text>
57
+ </Pressable>
58
+ </View>
59
+ );
60
+ }
61
+
62
+ const styles = StyleSheet.create({
63
+ container: {
64
+ justifyContent: "center",
65
+ },
66
+ button: {
67
+ backgroundColor: "#0a7ea4",
68
+ paddingVertical: 12,
69
+ paddingHorizontal: 24,
70
+ borderRadius: 8,
71
+ alignItems: "center",
72
+ marginTop: 8,
73
+ },
74
+ buttonPressed: {
75
+ opacity: 0.7,
76
+ },
77
+ buttonText: {
78
+ color: "#fff",
79
+ fontWeight: "600",
80
+ },
81
+ });
@@ -0,0 +1,101 @@
1
+ import { useSignIn } from "@clerk/clerk-expo";
2
+ import { useRouter } from "expo-router";
3
+ import { useState } from "react";
4
+ import { Alert, Pressable, StyleSheet, Text } from "react-native";
5
+
6
+ export function SignInWithPasskeyButton() {
7
+ const { signIn, setActive } = useSignIn();
8
+ const router = useRouter();
9
+ const [isLoading, setIsLoading] = useState(false);
10
+
11
+ const signInWithPasskey = async () => {
12
+ // 'discoverable' lets the user choose a passkey
13
+ // without auto-filling any of the options
14
+ try {
15
+ setIsLoading(true);
16
+ const signInAttempt = await signIn?.authenticateWithPasskey({
17
+ flow: "discoverable",
18
+ });
19
+
20
+ if (signInAttempt?.status === "complete") {
21
+ await setActive?.({
22
+ session: signInAttempt.createdSessionId,
23
+ redirectUrl: "/",
24
+ navigate: async ({ session }) => {
25
+ if (session?.currentTask) {
26
+ // Handle pending session tasks
27
+ // See https://clerk.com/docs/guides/development/custom-flows/authentication/session-tasks
28
+ console.log(session?.currentTask);
29
+ return;
30
+ }
31
+
32
+ // router.push("/(tabs)");
33
+ },
34
+ });
35
+ } else {
36
+ // If the status is not complete, check why. User may need to
37
+ // complete further steps.
38
+ console.error(JSON.stringify(signInAttempt, null, 2));
39
+ Alert.alert(
40
+ "Sign In Incomplete",
41
+ "Please complete all required steps.",
42
+ );
43
+ }
44
+ } catch (err: any) {
45
+ // See https://clerk.com/docs/guides/development/custom-flows/error-handling
46
+ // for more info on error handling
47
+ console.error("Passkey Error:", err);
48
+ console.error("Error Details:", {
49
+ message: err?.message,
50
+ code: err?.code,
51
+ errors: err?.errors,
52
+ clerkError: err?.clerkError,
53
+ });
54
+
55
+ const errorMessage =
56
+ err?.errors?.[0]?.longMessage ||
57
+ err?.message ||
58
+ "Passkey authentication failed. This feature may not be supported on your device.";
59
+
60
+ Alert.alert("Authentication Error", errorMessage);
61
+ } finally {
62
+ setIsLoading(false);
63
+ }
64
+ };
65
+
66
+ return (
67
+ <Pressable
68
+ style={({ pressed }) => [
69
+ styles.button,
70
+ pressed && styles.buttonPressed,
71
+ isLoading && styles.buttonDisabled,
72
+ ]}
73
+ // onPress={signInWithPasskey}
74
+ disabled={isLoading}
75
+ >
76
+ <Text style={styles.buttonText}>
77
+ {isLoading ? "Authenticating..." : "Sign in with a passkey"}
78
+ </Text>
79
+ </Pressable>
80
+ );
81
+ }
82
+
83
+ const styles = StyleSheet.create({
84
+ button: {
85
+ backgroundColor: "#0a7ea4",
86
+ paddingVertical: 12,
87
+ paddingHorizontal: 24,
88
+ borderRadius: 8,
89
+ alignItems: "center",
90
+ },
91
+ buttonPressed: {
92
+ opacity: 0.7,
93
+ },
94
+ buttonDisabled: {
95
+ opacity: 0.5,
96
+ },
97
+ buttonText: {
98
+ color: "#fff",
99
+ fontWeight: "600",
100
+ },
101
+ });
@@ -0,0 +1,82 @@
1
+ import { useClerk } from "@clerk/clerk-expo";
2
+ import { useRouter } from "expo-router";
3
+ import * as SecureStore from "expo-secure-store";
4
+ import { Alert, Pressable, StyleSheet, Text } from "react-native";
5
+
6
+ export const SignOutButton = () => {
7
+ const { signOut } = useClerk();
8
+ const router = useRouter();
9
+
10
+ const handleSignOut = async () => {
11
+ try {
12
+ const biometricEnabled =
13
+ await SecureStore.getItemAsync("biometricEnabled");
14
+
15
+ if (biometricEnabled === "true") {
16
+ // Lock the app without signing out
17
+ Alert.alert("Lock App", "Are you sure you want to lock the app?", [
18
+ {
19
+ text: "Cancel",
20
+ style: "cancel",
21
+ },
22
+ {
23
+ text: "Lock",
24
+ onPress: () => {
25
+ router.replace("/sign-in"); // Just lock, session remains
26
+ },
27
+ },
28
+ ]);
29
+ } else {
30
+ // Full sign out
31
+ Alert.alert("Sign Out", "Are you sure you want to sign out?", [
32
+ {
33
+ text: "Cancel",
34
+ style: "cancel",
35
+ },
36
+ {
37
+ text: "Sign Out",
38
+ style: "destructive",
39
+ onPress: () => {
40
+ signOut()
41
+ .then(() => {
42
+ router.replace("/sign-in");
43
+ })
44
+ .catch((err) => {
45
+ console.error("Sign out error:", err);
46
+ });
47
+ },
48
+ },
49
+ ]);
50
+ }
51
+ } catch (err) {
52
+ console.error("Error checking biometric status:", err);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <Pressable
58
+ style={({ pressed }) => [styles.button, pressed && styles.buttonPressed]}
59
+ onPress={handleSignOut}
60
+ >
61
+ <Text style={styles.buttonText}>Sign out</Text>
62
+ </Pressable>
63
+ );
64
+ };
65
+
66
+ const styles = StyleSheet.create({
67
+ button: {
68
+ backgroundColor: "#0a7ea4",
69
+ paddingVertical: 12,
70
+ paddingHorizontal: 24,
71
+ borderRadius: 8,
72
+ alignItems: "center",
73
+ marginTop: 8,
74
+ },
75
+ buttonPressed: {
76
+ opacity: 0.7,
77
+ },
78
+ buttonText: {
79
+ color: "#fff",
80
+ fontWeight: "600",
81
+ },
82
+ });
@@ -0,0 +1,10 @@
1
+ // https://docs.expo.dev/guides/using-eslint/
2
+ const { defineConfig } = require('eslint/config');
3
+ const expoConfig = require('eslint-config-expo/flat');
4
+
5
+ module.exports = defineConfig([
6
+ expoConfig,
7
+ {
8
+ ignores: ['dist/*'],
9
+ },
10
+ ]);
package/expo-env.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ /// <reference types="expo/types" />
2
+
3
+ // NOTE: This file should not be edited and should be in your git ignore
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@wattsoft/auth-app",
3
+ "main": "expo-router/entry",
4
+ "version": "1.0.0",
5
+ "scripts": {
6
+ "start": "expo start",
7
+ "reset-project": "node ./scripts/reset-project.js",
8
+ "android": "expo run:android",
9
+ "ios": "expo run:ios",
10
+ "web": "expo start --web",
11
+ "lint": "expo lint"
12
+ },
13
+ "dependencies": {
14
+ "@clerk/clerk-expo": "^2.19.25",
15
+ "@expo/vector-icons": "^15.0.3",
16
+ "@react-navigation/bottom-tabs": "^7.4.0",
17
+ "@react-navigation/elements": "^2.6.3",
18
+ "@react-navigation/native": "^7.1.8",
19
+ "expo": "~54.0.33",
20
+ "expo-constants": "~18.0.13",
21
+ "expo-font": "~14.0.11",
22
+ "expo-haptics": "~15.0.8",
23
+ "expo-image": "~3.0.11",
24
+ "expo-linking": "~8.0.11",
25
+ "expo-local-authentication": "~17.0.8",
26
+ "expo-router": "~6.0.23",
27
+ "expo-secure-store": "^15.0.8",
28
+ "expo-splash-screen": "~31.0.13",
29
+ "expo-status-bar": "~3.0.9",
30
+ "expo-symbols": "~1.0.8",
31
+ "expo-system-ui": "~6.0.9",
32
+ "expo-web-browser": "~15.0.10",
33
+ "react": "19.1.0",
34
+ "react-dom": "19.1.0",
35
+ "react-native": "0.81.5",
36
+ "react-native-gesture-handler": "~2.28.0",
37
+ "react-native-reanimated": "~4.1.1",
38
+ "react-native-safe-area-context": "~5.6.0",
39
+ "react-native-screens": "~4.16.0",
40
+ "react-native-web": "~0.21.0",
41
+ "react-native-worklets": "0.5.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/react": "~19.1.0",
45
+ "eslint": "^9.25.0",
46
+ "eslint-config-expo": "~10.0.0",
47
+ "typescript": "~5.9.2"
48
+ }
49
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "extends": "expo/tsconfig.base",
3
+ "compilerOptions": {
4
+ "strict": true,
5
+ "paths": {
6
+ "@/*": [
7
+ "./*"
8
+ ]
9
+ }
10
+ },
11
+ "include": [
12
+ "**/*.ts",
13
+ "**/*.tsx",
14
+ ".expo/types/**/*.ts",
15
+ "expo-env.d.ts"
16
+ ]
17
+ }