306 lines
8.5 KiB
TypeScript
306 lines
8.5 KiB
TypeScript
import React, { useState, useRef, useEffect } from "react";
|
|
import { View, TouchableOpacity } from "react-native";
|
|
import { Text } from "~/components/ui/text";
|
|
import { Button } from "~/components/ui/button";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { PhonePinKeypad } from "~/components/ui/PhonePinKeypad";
|
|
import { router } from "expo-router";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import { X } from "lucide-react-native";
|
|
|
|
type Step = "old" | "new" | "confirm";
|
|
|
|
export default function ChangePin() {
|
|
const { user } = useAuthWithProfile();
|
|
const [currentStep, setCurrentStep] = useState<Step>("old");
|
|
const [oldPin, setOldPin] = useState("");
|
|
const [newPin, setNewPin] = useState("");
|
|
const [confirmPin, setConfirmPin] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const [toastVisible, setToastVisible] = useState(false);
|
|
const [toastTitle, setToastTitle] = useState("");
|
|
const [toastDescription, setToastDescription] = useState<string | undefined>(
|
|
undefined
|
|
);
|
|
const [toastVariant, setToastVariant] = useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const showToast = (
|
|
title: string,
|
|
description?: string,
|
|
variant: "success" | "error" | "warning" | "info" = "info"
|
|
) => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
|
|
setToastTitle(title);
|
|
setToastDescription(description);
|
|
setToastVariant(variant);
|
|
setToastVisible(true);
|
|
|
|
toastTimeoutRef.current = setTimeout(() => {
|
|
setToastVisible(false);
|
|
toastTimeoutRef.current = null;
|
|
}, 2500);
|
|
};
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const getCurrentPin = () => {
|
|
switch (currentStep) {
|
|
case "old":
|
|
return oldPin;
|
|
case "new":
|
|
return newPin;
|
|
case "confirm":
|
|
return confirmPin;
|
|
}
|
|
};
|
|
|
|
const handleKeyPress = (key: string) => {
|
|
const currentPin = getCurrentPin();
|
|
|
|
if (key === "clear") {
|
|
switch (currentStep) {
|
|
case "old":
|
|
setOldPin("");
|
|
break;
|
|
case "new":
|
|
setNewPin("");
|
|
break;
|
|
case "confirm":
|
|
setConfirmPin("");
|
|
break;
|
|
}
|
|
} else if (key === "backspace") {
|
|
switch (currentStep) {
|
|
case "old":
|
|
setOldPin((prev) => prev.slice(0, -1));
|
|
break;
|
|
case "new":
|
|
setNewPin((prev) => prev.slice(0, -1));
|
|
break;
|
|
case "confirm":
|
|
setConfirmPin((prev) => prev.slice(0, -1));
|
|
break;
|
|
}
|
|
} else if (currentPin.length < 6) {
|
|
switch (currentStep) {
|
|
case "old":
|
|
setOldPin((prev) => prev + key);
|
|
break;
|
|
case "new":
|
|
setNewPin((prev) => prev + key);
|
|
break;
|
|
case "confirm":
|
|
setConfirmPin((prev) => prev + key);
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleContinue = async () => {
|
|
const currentPin = getCurrentPin();
|
|
|
|
if (currentPin.length !== 6) {
|
|
showToast("Error", "Please enter a 6-digit PIN", "error");
|
|
return;
|
|
}
|
|
|
|
if (currentStep === "old") {
|
|
// TODO: Verify old PIN with backend
|
|
// For now, just move to next step
|
|
setLoading(true);
|
|
setTimeout(() => {
|
|
setLoading(false);
|
|
setCurrentStep("new");
|
|
}, 500);
|
|
} else if (currentStep === "new") {
|
|
if (newPin === oldPin) {
|
|
showToast("Error", "New PIN must be different from old PIN", "error");
|
|
return;
|
|
}
|
|
setCurrentStep("confirm");
|
|
} else if (currentStep === "confirm") {
|
|
if (confirmPin !== newPin) {
|
|
showToast("Error", "PINs do not match", "error");
|
|
setConfirmPin("");
|
|
return;
|
|
}
|
|
|
|
// TODO: Update PIN in backend
|
|
setLoading(true);
|
|
setTimeout(() => {
|
|
setLoading(false);
|
|
showToast("Success", "PIN changed successfully", "success");
|
|
setTimeout(() => {
|
|
router.back();
|
|
}, 1500);
|
|
}, 1000);
|
|
}
|
|
};
|
|
|
|
const getStepTitle = () => {
|
|
switch (currentStep) {
|
|
case "old":
|
|
return "Enter Old PIN";
|
|
case "new":
|
|
return "Enter New PIN";
|
|
case "confirm":
|
|
return "Confirm New PIN";
|
|
}
|
|
};
|
|
|
|
const getStepDescription = () => {
|
|
switch (currentStep) {
|
|
case "old":
|
|
return "Enter your current PIN";
|
|
case "new":
|
|
return "Enter your new PIN";
|
|
case "confirm":
|
|
return "Re-enter your new PIN to confirm";
|
|
}
|
|
};
|
|
|
|
const renderPinDots = () => {
|
|
const currentPin = getCurrentPin();
|
|
return (
|
|
<View className="mb-8">
|
|
<Text className="text-center text-base font-dmsans text-gray-600 mb-6">
|
|
{getStepDescription()}
|
|
</Text>
|
|
<View className="flex-row justify-between items-center px-5">
|
|
{[0, 1, 2, 3, 4, 5].map((index) => (
|
|
<View
|
|
key={index}
|
|
style={{
|
|
width: 18,
|
|
height: 18,
|
|
borderRadius: 999,
|
|
borderWidth: 2,
|
|
borderColor: index < currentPin.length ? "#105D38" : "#D1D5DB",
|
|
backgroundColor:
|
|
index < currentPin.length ? "#105D38" : "transparent",
|
|
}}
|
|
/>
|
|
))}
|
|
</View>
|
|
</View>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<BackButton />
|
|
|
|
<View className="flex-1 bg-white px-5">
|
|
{/* Header */}
|
|
<View className="py-8">
|
|
<Text className="text-3xl font-dmsans-bold text-gray-900 mb-2">
|
|
Change PIN
|
|
</Text>
|
|
<Text className="text-base font-dmsans text-gray-500">
|
|
{getStepDescription()}
|
|
</Text>
|
|
</View>
|
|
|
|
{/* Step Indicator */}
|
|
<View className="flex-row justify-center items-center mb-8">
|
|
<View
|
|
className={`w-8 h-8 rounded-full items-center justify-center ${
|
|
currentStep === "old" ? "bg-primary" : "bg-gray-300"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`font-dmsans-bold text-sm ${
|
|
currentStep === "old" ? "text-white" : "text-gray-600"
|
|
}`}
|
|
>
|
|
1
|
|
</Text>
|
|
</View>
|
|
<View className="w-12 h-1 bg-gray-300 mx-2" />
|
|
<View
|
|
className={`w-8 h-8 rounded-full items-center justify-center ${
|
|
currentStep === "new" || currentStep === "confirm"
|
|
? "bg-primary"
|
|
: "bg-gray-300"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`font-dmsans-bold text-sm ${
|
|
currentStep === "new" || currentStep === "confirm"
|
|
? "text-white"
|
|
: "text-gray-600"
|
|
}`}
|
|
>
|
|
2
|
|
</Text>
|
|
</View>
|
|
<View className="w-12 h-1 bg-gray-300 mx-2" />
|
|
<View
|
|
className={`w-8 h-8 rounded-full items-center justify-center ${
|
|
currentStep === "confirm" ? "bg-primary" : "bg-gray-300"
|
|
}`}
|
|
>
|
|
<Text
|
|
className={`font-dmsans-bold text-sm ${
|
|
currentStep === "confirm" ? "text-white" : "text-gray-600"
|
|
}`}
|
|
>
|
|
3
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Spacer */}
|
|
<View className="flex-1" />
|
|
|
|
{/* PIN Dots - positioned right above keypad */}
|
|
{renderPinDots()}
|
|
|
|
{/* Keypad */}
|
|
<View className="mb-6">
|
|
<PhonePinKeypad onKeyPress={handleKeyPress} />
|
|
</View>
|
|
|
|
{/* Continue Button */}
|
|
<View className="pb-6">
|
|
<Button
|
|
className="bg-primary rounded-3xl"
|
|
onPress={handleContinue}
|
|
disabled={loading || getCurrentPin().length !== 6}
|
|
>
|
|
<Text className="font-dmsans text-white">
|
|
{loading
|
|
? "Processing..."
|
|
: currentStep === "confirm"
|
|
? "Change PIN"
|
|
: "Continue"}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
</View>
|
|
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|