@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 +50 -0
- package/app/(app)/(tabs)/_layout.tsx +16 -0
- package/app/(app)/(tabs)/index.tsx +31 -0
- package/app/(app)/(tabs)/profile.tsx +62 -0
- package/app/(app)/_layout.tsx +37 -0
- package/app/(app)/sign-in.tsx +230 -0
- package/app/(app)/sign-up.tsx +172 -0
- package/app/_layout.tsx +14 -0
- package/app.json +49 -0
- package/assets/images/android-icon-background.png +0 -0
- package/assets/images/android-icon-foreground.png +0 -0
- package/assets/images/android-icon-monochrome.png +0 -0
- package/assets/images/favicon.png +0 -0
- package/assets/images/icon.png +0 -0
- package/assets/images/partial-react-logo.png +0 -0
- package/assets/images/react-logo.png +0 -0
- package/assets/images/react-logo@2x.png +0 -0
- package/assets/images/react-logo@3x.png +0 -0
- package/assets/images/splash-icon.png +0 -0
- package/components/BiometricSignIn.tsx +104 -0
- package/components/GoogleSignIn.tsx +81 -0
- package/components/Passkey.tsx +101 -0
- package/components/SignOut.tsx +82 -0
- package/eslint.config.js +10 -0
- package/expo-env.d.ts +3 -0
- package/package.json +49 -0
- package/tsconfig.json +17 -0
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
|
+
});
|
package/app/_layout.tsx
ADDED
|
@@ -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
|
|
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
|
+
});
|
package/eslint.config.js
ADDED
package/expo-env.d.ts
ADDED
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