diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/MigrateAccountStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/MigrateAccountStep.tsx new file mode 100644 index 00000000..7c222847 --- /dev/null +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/MigrateAccountStep.tsx @@ -0,0 +1,364 @@ +/** + * MigrateAccountStep - Migrate WHMCS account to portal (passwordless) + * + * For whmcs_unmapped users who have verified their email via OTP. + * Email verification serves as identity proof - no legacy password needed. + * Shows prefilled WHMCS data and collects password + required fields. + */ + +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { Button, Input, Label } from "@/components/atoms"; +import { Checkbox } from "@/components/atoms/checkbox"; +import { getSafeRedirect } from "@/features/auth/utils/route-protection"; +import { useGetStartedStore } from "../../../stores/get-started.store"; + +interface FormErrors { + password?: string | undefined; + confirmPassword?: string | undefined; + dateOfBirth?: string | undefined; + gender?: string | undefined; + acceptTerms?: string | undefined; +} + +export function MigrateAccountStep() { + const router = useRouter(); + const { + formData, + updateFormData, + migrateWhmcsAccount, + prefill, + loading, + error, + clearError, + goBack, + redirectTo, + serviceContext, + } = useGetStartedStore(); + + // Compute effective redirect URL from store state (with validation) + const effectiveRedirectTo = getSafeRedirect( + redirectTo || serviceContext?.redirectTo, + "/account/dashboard" + ); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [dateOfBirth, setDateOfBirth] = useState(formData.dateOfBirth); + const [gender, setGender] = useState<"male" | "female" | "other" | "">(formData.gender); + const [acceptTerms, setAcceptTerms] = useState(formData.acceptTerms); + const [marketingConsent, setMarketingConsent] = useState(formData.marketingConsent); + const [localErrors, setLocalErrors] = useState({}); + + const validatePassword = (pass: string): string | undefined => { + if (!pass) return "Password is required"; + if (pass.length < 8) return "Password must be at least 8 characters"; + if (!/[A-Z]/.test(pass)) return "Password must contain an uppercase letter"; + if (!/[a-z]/.test(pass)) return "Password must contain a lowercase letter"; + if (!/[0-9]/.test(pass)) return "Password must contain a number"; + return undefined; + }; + + const validate = (): boolean => { + const errors: FormErrors = {}; + + const passwordError = validatePassword(password); + if (passwordError) { + errors.password = passwordError; + } + + if (password !== confirmPassword) { + errors.confirmPassword = "Passwords do not match"; + } + + if (!dateOfBirth) { + errors.dateOfBirth = "Date of birth is required"; + } + + if (!gender) { + errors.gender = "Please select a gender"; + } + + if (!acceptTerms) { + errors.acceptTerms = "You must accept the terms of service"; + } + + setLocalErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSubmit = async () => { + clearError(); + + if (!validate()) { + return; + } + + // Update form data + updateFormData({ + password, + confirmPassword, + dateOfBirth, + gender: gender as "male" | "female" | "other", + acceptTerms, + marketingConsent, + }); + + const result = await migrateWhmcsAccount(); + if (result) { + // Redirect to the effective redirect URL on success + router.push(effectiveRedirectTo); + } + }; + + const canSubmit = password && confirmPassword && dateOfBirth && gender && acceptTerms; + + return ( +
+ {/* Header */} +
+

+ {prefill?.firstName + ? `Welcome back, ${prefill.firstName}! Set up your new portal password.` + : "Set up your new portal password to continue."} +

+
+ + {/* Pre-filled info display in gray disabled inputs */} +
+
+
+ + +
+
+ + +
+
+
+ + +
+ {(prefill?.phone || formData.phone) && ( +
+ + +
+ )} + {prefill?.address && ( +
+ + +
+ )} +
+ + {/* Date of Birth */} +
+ + { + setDateOfBirth(e.target.value); + setLocalErrors(prev => ({ ...prev, dateOfBirth: undefined })); + }} + disabled={loading} + error={localErrors.dateOfBirth} + max={new Date().toISOString().split("T")[0]} + /> + {localErrors.dateOfBirth && ( +

{localErrors.dateOfBirth}

+ )} +
+ + {/* Gender */} +
+ +
+ {(["male", "female", "other"] as const).map(option => ( + + ))} +
+ {localErrors.gender &&

{localErrors.gender}

} +
+ + {/* Password */} +
+ + { + setPassword(e.target.value); + setLocalErrors(prev => ({ ...prev, password: undefined })); + }} + placeholder="Create a strong password" + disabled={loading} + error={localErrors.password} + autoComplete="new-password" + /> + {localErrors.password &&

{localErrors.password}

} +

+ At least 8 characters with uppercase, lowercase, and numbers +

+
+ + {/* Confirm Password */} +
+ + { + setConfirmPassword(e.target.value); + setLocalErrors(prev => ({ ...prev, confirmPassword: undefined })); + }} + placeholder="Confirm your password" + disabled={loading} + error={localErrors.confirmPassword} + autoComplete="new-password" + /> + {localErrors.confirmPassword && ( +

{localErrors.confirmPassword}

+ )} +
+ + {/* Terms & Marketing */} +
+
+ { + setAcceptTerms(e.target.checked); + setLocalErrors(prev => ({ ...prev, acceptTerms: undefined })); + }} + disabled={loading} + /> + +
+ {localErrors.acceptTerms && ( +

{localErrors.acceptTerms}

+ )} + +
+ setMarketingConsent(e.target.checked)} + disabled={loading} + /> + +
+
+ + {/* Error display */} + {error && ( +
+

{error}

+
+ )} + + {/* Actions */} +
+ + + +
+
+ ); +}