Files
beenvoice-app/app/(auth)/reset-password.tsx
T
soconnor 14c880123c Add beenvoice mobile companion app with full dark mode support.
Expo app with dashboard, time clock, invoices, and settings — native tabs, glass UI, theme-aware components, and iOS Live Activities.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 22:36:37 -04:00

183 lines
5.5 KiB
TypeScript

import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import {
KeyboardAvoidingView,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { FullScreen } from "@/components/Screen";
import { AuthBackground } from "@/components/AppBackground";
import { HeadingText } from "@/components/Logo";
import { Button } from "@/components/ui/Button";
import { Card } from "@/components/ui/Card";
import { Input } from "@/components/ui/Input";
import { fonts, radii, spacing } from "@/constants/theme";
import { useAppTheme } from "@/contexts/ThemeContext";
import { resetPassword } from "@/lib/auth-api";
import type { ThemeColors } from "@/lib/theme-palette";
import { useThemedStyles } from "@/lib/use-themed-styles";
export default function ResetPasswordScreen() {
const styles = useThemedStyles(createResetPasswordStyles);
const { token: tokenParam } = useLocalSearchParams<{ token?: string }>();
const [token, setToken] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (typeof tokenParam === "string" && tokenParam.length > 0) {
setToken(tokenParam);
}
}, [tokenParam]);
async function handleSubmit() {
setError(null);
if (password.length < 8) {
setError("Password must be at least 8 characters");
return;
}
if (password !== confirmPassword) {
setError("Passwords do not match");
return;
}
setLoading(true);
try {
await resetPassword(token.trim(), password);
setSuccess(true);
} catch (err) {
setError(err instanceof Error ? err.message : "Reset failed");
} finally {
setLoading(false);
}
}
return (
<AuthBackground>
<FullScreen style={styles.safe}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : undefined}
style={styles.flex}
>
<ScrollView contentContainerStyle={styles.container}>
<Pressable onPress={() => router.back()}>
<Text style={styles.back}> Back</Text>
</Pressable>
<Card style={styles.card}>
<View style={styles.header}>
<HeadingText style={styles.title}>Set new password</HeadingText>
<Text style={styles.subtitle}>
Paste the reset token from your email, or open the link on this device.
</Text>
</View>
{success ? (
<View style={styles.successBox}>
<Text style={styles.successTitle}>Password updated</Text>
<Text style={styles.successText}>
You can now sign in with your new password.
</Text>
<Button
title="Go to sign in"
onPress={() => router.replace("/(auth)/sign-in")}
/>
</View>
) : (
<View style={styles.form}>
<Input
label="Reset token"
autoCapitalize="none"
value={token}
onChangeText={setToken}
placeholder="Paste token from email"
/>
<Input
label="New password"
secureTextEntry
value={password}
onChangeText={setPassword}
placeholder="At least 8 characters"
/>
<Input
label="Confirm password"
secureTextEntry
value={confirmPassword}
onChangeText={setConfirmPassword}
placeholder="Repeat password"
/>
{error ? <Text style={styles.error}>{error}</Text> : null}
<Button title="Update password" loading={loading} onPress={handleSubmit} />
</View>
)}
</Card>
</ScrollView>
</KeyboardAvoidingView>
</FullScreen>
</AuthBackground>
);
}
const createResetPasswordStyles = (colors: ThemeColors, _isDark: boolean) =>
StyleSheet.create({
safe: { flex: 1 },
flex: { flex: 1 },
container: {
flexGrow: 1,
padding: spacing.lg,
gap: spacing.md,
justifyContent: "center",
},
back: {
color: colors.mutedForeground,
fontFamily: fonts.bodyMedium,
fontSize: 16,
marginBottom: spacing.sm,
},
card: { gap: spacing.lg },
header: { gap: spacing.sm },
title: { fontSize: 28 },
subtitle: {
fontSize: 14,
fontFamily: fonts.body,
color: colors.mutedForeground,
lineHeight: 20,
},
form: { gap: spacing.md },
error: {
color: colors.destructive,
fontSize: 14,
fontFamily: fonts.body,
},
successBox: {
gap: spacing.md,
padding: spacing.lg,
backgroundColor: colors.muted,
borderRadius: radii.xl,
borderWidth: 1,
borderColor: colors.border,
},
successTitle: {
fontSize: 20,
fontFamily: fonts.heading,
color: colors.foreground,
},
successText: {
color: colors.mutedForeground,
fontFamily: fonts.body,
lineHeight: 20,
},
});