diff --git a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts index 6cc22132..598943c7 100644 --- a/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts +++ b/apps/bff/src/integrations/salesforce/services/salesforce-account.service.ts @@ -155,18 +155,40 @@ export class SalesforceAccountService { * Check if a Salesforce account exists with the given email * Used to prevent duplicate account creation during checkout */ - async findByEmail(email: string): Promise<{ id: string; accountNumber: string } | null> { + async findByEmail(email: string): Promise { try { - // Search for Contact with matching email and get the associated Account + // Search for Contact with matching email and get the associated Account + Contact details const result = (await this.connection.query( - `SELECT Account.Id, Account.SF_Account_No__c FROM Contact WHERE Email = '${this.safeSoql(email)}' LIMIT 1`, + `SELECT Account.Id, Account.SF_Account_No__c, FirstName, LastName, MailingStreet, MailingCity, MailingState, MailingPostalCode, MailingCountry FROM Contact WHERE Email = '${this.safeSoql(email)}' LIMIT 1`, { label: "checkout:findAccountByEmail" } - )) as SalesforceResponse<{ Account: { Id: string; SF_Account_No__c: string } }>; + )) as SalesforceResponse<{ + Account: { Id: string; SF_Account_No__c: string }; + FirstName?: string | null; + LastName?: string | null; + MailingStreet?: string | null; + MailingCity?: string | null; + MailingState?: string | null; + MailingPostalCode?: string | null; + MailingCountry?: string | null; + }>; if (result.totalSize > 0 && result.records[0]?.Account) { + const record = result.records[0]; + const hasAddress = record.MailingCity || record.MailingState || record.MailingPostalCode; return { - id: result.records[0].Account.Id, - accountNumber: result.records[0].Account.SF_Account_No__c, + id: record.Account.Id, + accountNumber: record.Account.SF_Account_No__c, + ...(record.FirstName && { firstName: record.FirstName }), + ...(record.LastName && { lastName: record.LastName }), + ...(hasAddress && { + address: { + address1: record.MailingStreet || "", + city: record.MailingCity || "", + state: record.MailingState || "", + postcode: record.MailingPostalCode || "", + country: record.MailingCountry || "JP", + }, + }), }; } @@ -530,6 +552,23 @@ export interface SalesforceAccountPortalUpdate { whmcsAccountId?: string | number | null; } +/** + * Result from findByEmail — includes Contact name/address when available + */ +export interface FindByEmailResult { + id: string; + accountNumber: string; + firstName?: string; + lastName?: string; + address?: { + address1: string; + city: string; + state: string; + postcode: string; + country: string; + }; +} + /** * Request type for creating a new Salesforce Account */ diff --git a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts index 5aed6c68..b5dc76f4 100644 --- a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts @@ -25,11 +25,20 @@ import { GetStartedSessionService } from "../otp/get-started-session.service.js" * Result of account status detection across Portal, WHMCS, and Salesforce. * Optional fields are populated based on the detected status: * - WHMCS_UNMAPPED: whmcsClientId, whmcsFirstName, whmcsLastName - * - SF_UNMAPPED: sfAccountId + * - SF_UNMAPPED: sfAccountId, sfFirstName, sfLastName, sfAddress */ interface AccountStatusResult { status: AccountStatus; sfAccountId?: string; + sfFirstName?: string; + sfLastName?: string; + sfAddress?: { + address1: string; + city: string; + state: string; + postcode: string; + country: string; + }; whmcsClientId?: number; whmcsFirstName?: string; whmcsLastName?: string; @@ -233,7 +242,13 @@ export class VerificationWorkflowService { if (mapping) { return { status: ACCOUNT_STATUS.PORTAL_EXISTS }; } - return { status: ACCOUNT_STATUS.SF_UNMAPPED, sfAccountId: sfAccount.id }; + return { + status: ACCOUNT_STATUS.SF_UNMAPPED, + sfAccountId: sfAccount.id, + ...(sfAccount.firstName && { sfFirstName: sfAccount.firstName }), + ...(sfAccount.lastName && { sfLastName: sfAccount.lastName }), + ...(sfAccount.address && { sfAddress: sfAccount.address }), + }; } return { status: ACCOUNT_STATUS.NEW_CUSTOMER }; @@ -251,7 +266,12 @@ export class VerificationWorkflowService { }; } if (accountStatus.status === ACCOUNT_STATUS.SF_UNMAPPED && accountStatus.sfAccountId) { - return { email }; + return { + email, + ...(accountStatus.sfFirstName && { firstName: accountStatus.sfFirstName }), + ...(accountStatus.sfLastName && { lastName: accountStatus.sfLastName }), + ...(accountStatus.sfAddress && { address: accountStatus.sfAddress }), + }; } return undefined; } diff --git a/apps/portal/src/components/atoms/index.ts b/apps/portal/src/components/atoms/index.ts index 80d66fc0..b72a661c 100644 --- a/apps/portal/src/components/atoms/index.ts +++ b/apps/portal/src/components/atoms/index.ts @@ -10,6 +10,8 @@ export type { ButtonProps } from "./button"; export { Input } from "./input"; export type { InputProps } from "./input"; +export { PasswordInput } from "./password-input"; + export { Checkbox } from "./checkbox"; export type { CheckboxProps } from "./checkbox"; diff --git a/apps/portal/src/components/atoms/password-input.tsx b/apps/portal/src/components/atoms/password-input.tsx new file mode 100644 index 00000000..edb2b12b --- /dev/null +++ b/apps/portal/src/components/atoms/password-input.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { forwardRef, useState } from "react"; +import { Eye, EyeOff } from "lucide-react"; +import { cn } from "@/shared/utils"; +import { Input } from "./input"; +import type { InputProps } from "./input"; + +const PasswordInput = forwardRef>( + ({ className, ...props }, ref) => { + const [visible, setVisible] = useState(false); + + return ( +
+ + +
+ ); + } +); +PasswordInput.displayName = "PasswordInput"; + +export { PasswordInput }; diff --git a/apps/portal/src/features/account/components/PasswordChangeCard.tsx b/apps/portal/src/features/account/components/PasswordChangeCard.tsx index ddc33b35..9e6d5bb5 100644 --- a/apps/portal/src/features/account/components/PasswordChangeCard.tsx +++ b/apps/portal/src/features/account/components/PasswordChangeCard.tsx @@ -1,6 +1,9 @@ "use client"; +import { PasswordInput, Button, Label } from "@/components/atoms"; import { SubCard } from "@/components/molecules/SubCard/SubCard"; +import { PasswordRequirements, PasswordMatchIndicator } from "@/features/auth/components"; +import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; interface PasswordChangeCardProps { isChanging: boolean; @@ -19,65 +22,71 @@ export function PasswordChangeCard({ setForm, onSubmit, }: PasswordChangeCardProps) { + const { checks } = usePasswordValidation(form.newPassword); + const showPasswordMatch = form.confirmPassword.length > 0; + const passwordsMatch = form.newPassword === form.confirmPassword; + return ( -

Change Password

+

Change Password

{success && ( -
+
{success}
)} {error && ( -
+
{error}
)}
- - Current Password + setForm({ currentPassword: e.target.value })} - className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" placeholder="••••••••" + disabled={isChanging} />
- - New Password + setForm({ newPassword: e.target.value })} - className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" placeholder="New secure password" + disabled={isChanging} />
- - Confirm New Password + setForm({ confirmPassword: e.target.value })} - className="block w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition-colors" placeholder="Re-enter new password" + disabled={isChanging} />
-
-
+ {showPasswordMatch && ( +
+ +
+ )} +
+ + Change Password +
-

- Password must be at least 8 characters and include uppercase, lowercase, and a number. -

); } diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index cb293069..cbfc38b4 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -13,7 +13,7 @@ import { useState, useCallback } from "react"; import Link from "next/link"; import { useSearchParams } from "next/navigation"; -import { Button, Input, ErrorMessage } from "@/components/atoms"; +import { Button, Input, PasswordInput, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; import { LoginOtpStep } from "../LoginOtpStep"; import { useLoginWithOtp } from "../../hooks/use-auth"; @@ -179,8 +179,7 @@ export function LoginForm({ error={touched["password"] ? errors["password"] : undefined} required > - setValue("password", e.target.value)} onBlur={() => setTouchedField("password")} diff --git a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx index 0c7dff1a..41c3c9f4 100644 --- a/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx +++ b/apps/portal/src/features/auth/components/PasswordResetForm/PasswordResetForm.tsx @@ -7,9 +7,11 @@ import { useEffect } from "react"; import Link from "next/link"; -import { Button, Input, ErrorMessage } from "@/components/atoms"; +import { Button, Input, PasswordInput, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; +import { PasswordRequirements, PasswordMatchIndicator } from ".."; import { usePasswordReset } from "../../hooks/use-auth"; +import { usePasswordValidation } from "../../hooks/usePasswordValidation"; import { useZodForm } from "@/shared/hooks"; import { passwordResetRequestSchema, passwordResetSchema } from "@customer-portal/domain/auth"; import { z } from "zod"; @@ -87,6 +89,11 @@ export function PasswordResetForm({ }, }); + const { checks } = usePasswordValidation(resetForm.values.password); + const showPasswordMatch = + resetForm.values.confirmPassword.length > 0 && !resetForm.errors["confirmPassword"]; + const passwordsMatch = resetForm.values.password === resetForm.values.confirmPassword; + // Extract stable reset functions to avoid unnecessary effect runs. // The form objects change when internal state changes, but reset is stable. const requestFormReset = requestForm.reset; @@ -161,8 +168,7 @@ export function PasswordResetForm({ error={resetForm.touched["password"] ? resetForm.errors["password"] : undefined} required > - resetForm.setValue("password", e.target.value)} @@ -170,6 +176,7 @@ export function PasswordResetForm({ disabled={loading || resetForm.isSubmitting} className={resetForm.errors["password"] ? "border-red-300" : ""} /> + - resetForm.setValue("confirmPassword", e.target.value)} @@ -188,6 +194,7 @@ export function PasswordResetForm({ disabled={loading || resetForm.isSubmitting} className={resetForm.errors["confirmPassword"] ? "border-red-300" : ""} /> + {showPasswordMatch && } {error && {error}} diff --git a/apps/portal/src/features/auth/components/PasswordSection.tsx b/apps/portal/src/features/auth/components/PasswordSection.tsx new file mode 100644 index 00000000..6e119186 --- /dev/null +++ b/apps/portal/src/features/auth/components/PasswordSection.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { PasswordInput, Label, ErrorMessage } from "@/components/atoms"; +import { PasswordRequirements } from "./PasswordRequirements"; +import { PasswordMatchIndicator } from "./PasswordMatchIndicator"; +import { usePasswordValidation } from "../hooks/usePasswordValidation"; + +interface PasswordSectionProps { + password: string; + confirmPassword: string; + onPasswordChange: (value: string) => void; + onConfirmPasswordChange: (value: string) => void; + errors: { + password?: string | undefined; + confirmPassword?: string | undefined; + }; + clearError: (field: "password" | "confirmPassword") => void; + loading: boolean; +} + +export function PasswordSection({ + password, + confirmPassword, + onPasswordChange, + onConfirmPasswordChange, + errors, + clearError, + loading, +}: PasswordSectionProps) { + const { checks } = usePasswordValidation(password); + const showPasswordMatch = confirmPassword.length > 0 && !errors.confirmPassword; + const passwordsMatch = password === confirmPassword; + + return ( + <> +
+ + { + onPasswordChange(e.target.value); + clearError("password"); + }} + placeholder="Create a strong password" + disabled={loading} + error={errors.password} + autoComplete="new-password" + /> + {errors.password} + +
+ +
+ + { + onConfirmPasswordChange(e.target.value); + clearError("confirmPassword"); + }} + placeholder="Confirm your password" + disabled={loading} + error={errors.confirmPassword} + autoComplete="new-password" + /> + {errors.confirmPassword} + {showPasswordMatch && } +
+ + ); +} diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx index 0721abe7..513c935a 100644 --- a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx +++ b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx @@ -6,15 +6,13 @@ import { useEffect } from "react"; import Link from "next/link"; -import { Button, Input, ErrorMessage } from "@/components/atoms"; +import { Button, Input, PasswordInput, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; +import { PasswordRequirements, PasswordMatchIndicator } from ".."; import { useAuth } from "../../hooks/use-auth"; +import { usePasswordValidation } from "../../hooks/usePasswordValidation"; import { useZodForm } from "@/shared/hooks"; -import { - setPasswordRequestSchema, - checkPasswordStrength, - getPasswordStrengthDisplay, -} from "@customer-portal/domain/auth"; +import { setPasswordRequestSchema, getPasswordStrengthDisplay } from "@customer-portal/domain/auth"; import { z } from "zod"; // Extend domain schema with confirmPassword @@ -55,9 +53,11 @@ export function SetPasswordForm({ }, }); - const { requirements, strength, isValid } = checkPasswordStrength(form.values.password); + const { checks, strength, isValid } = usePasswordValidation(form.values.password); const { label, colorClass } = getPasswordStrengthDisplay(strength); const passwordsMatch = form.values.password === form.values.confirmPassword; + const showPasswordMatch = + form.values.confirmPassword.length > 0 && !form.errors["confirmPassword"]; const isLoading = loading || form.isSubmitting; const isEmailProvided = Boolean(email); @@ -95,11 +95,10 @@ export function SetPasswordForm({ error={form.touched["password"] ? form.errors["password"] : undefined} required > - form.setValue("password", e.target.value)} - onBlur={() => form.setTouched("password", true)} + onBlur={() => form.setTouchedField("password")} placeholder="Create a secure password" disabled={isLoading} autoComplete="new-password" @@ -122,16 +121,7 @@ export function SetPasswordForm({ {label}
-
- {requirements.map(r => ( -
- - {r.met ? "✓" : "○"} - - {r.label} -
- ))} -
+
)} @@ -140,22 +130,17 @@ export function SetPasswordForm({ error={form.touched["confirmPassword"] ? form.errors["confirmPassword"] : undefined} required > - form.setValue("confirmPassword", e.target.value)} - onBlur={() => form.setTouched("confirmPassword", true)} + onBlur={() => form.setTouchedField("confirmPassword")} placeholder="Re-enter your password" disabled={isLoading} autoComplete="new-password" /> - {form.values.confirmPassword && ( -

- {passwordsMatch ? "✓ Passwords match" : "✗ Passwords do not match"} -

- )} + {showPasswordMatch && } {(error || form.errors["_form"]) && ( {form.errors["_form"] || error} diff --git a/apps/portal/src/features/auth/components/index.ts b/apps/portal/src/features/auth/components/index.ts index 345469ca..ad2cc890 100644 --- a/apps/portal/src/features/auth/components/index.ts +++ b/apps/portal/src/features/auth/components/index.ts @@ -10,6 +10,7 @@ export { SetPasswordForm } from "./SetPasswordForm/SetPasswordForm"; export { AuthLayout } from "@/components/templates/AuthLayout"; // Account creation components +export { PasswordSection } from "./PasswordSection"; export { PasswordRequirements } from "./PasswordRequirements"; export { PasswordMatchIndicator } from "./PasswordMatchIndicator"; export { TermsCheckbox } from "./TermsCheckbox"; diff --git a/apps/portal/src/features/auth/hooks/usePasswordValidation.ts b/apps/portal/src/features/auth/hooks/usePasswordValidation.ts index 94e440fb..b6532987 100644 --- a/apps/portal/src/features/auth/hooks/usePasswordValidation.ts +++ b/apps/portal/src/features/auth/hooks/usePasswordValidation.ts @@ -1,4 +1,5 @@ import { useMemo } from "react"; +import { checkPasswordStrength } from "@customer-portal/domain/auth"; export interface PasswordChecks { minLength: boolean; @@ -9,32 +10,31 @@ export interface PasswordChecks { export interface PasswordValidation { checks: PasswordChecks; + strength: number; isValid: boolean; error: string | undefined; } -export function validatePasswordRules(password: string): string | undefined { - if (!password) return "Password is required"; - if (password.length < 8) return "Password must be at least 8 characters"; - if (!/[A-Z]/.test(password)) return "Password must contain an uppercase letter"; - if (!/[a-z]/.test(password)) return "Password must contain a lowercase letter"; - if (!/[0-9]/.test(password)) return "Password must contain a number"; - return undefined; -} - export function usePasswordValidation(password: string): PasswordValidation { return useMemo(() => { + const { requirements, strength, isValid } = checkPasswordStrength(password); + + const byKey = Object.fromEntries(requirements.map(r => [r.key, r.met])); const checks: PasswordChecks = { - minLength: password.length >= 8, - hasUppercase: /[A-Z]/.test(password), - hasLowercase: /[a-z]/.test(password), - hasNumber: /[0-9]/.test(password), + minLength: byKey["minLength"] ?? false, + hasUppercase: byKey["uppercase"] ?? false, + hasLowercase: byKey["lowercase"] ?? false, + hasNumber: byKey["number"] ?? false, }; - const isValid = - checks.minLength && checks.hasUppercase && checks.hasLowercase && checks.hasNumber; - const error = validatePasswordRules(password); + const firstFailing = requirements.find(r => !r.met); + let error: string | undefined; + if (password.length === 0) { + error = "Password is required"; + } else if (firstFailing) { + error = firstFailing.label; + } - return { checks, isValid, error }; + return { checks, strength, isValid, error }; }, [password]); } 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 index b5b49ec6..6a31cc35 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/MigrateAccountStep.tsx +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/MigrateAccountStep.tsx @@ -10,8 +10,10 @@ "use client"; import { useState } from "react"; -import { Button, Input, Label } from "@/components/atoms"; +import { Button, Input, PasswordInput, Label } from "@/components/atoms"; import { Checkbox } from "@/components/atoms/checkbox"; +import { PasswordRequirements, PasswordMatchIndicator } from "@/features/auth/components"; +import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; import { useGetStartedMachine } from "../../../hooks/useGetStartedMachine"; interface FormErrors { @@ -31,22 +33,15 @@ export function MigrateAccountStep() { 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 { checks, error: passwordValidationError } = usePasswordValidation(password); + const showPasswordMatch = confirmPassword.length > 0 && !localErrors.confirmPassword; + const passwordsMatch = password === confirmPassword; const validate = (): boolean => { const errors: FormErrors = {}; - const passwordError = validatePassword(password); - if (passwordError) { - errors.password = passwordError; + if (passwordValidationError) { + errors.password = passwordValidationError; } if (password !== confirmPassword) { @@ -146,9 +141,8 @@ export function MigrateAccountStep() { - { setPassword(e.target.value); @@ -160,9 +154,7 @@ export function MigrateAccountStep() { autoComplete="new-password" /> {localErrors.password &&

{localErrors.password}

} -

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

+ {/* Confirm Password */} @@ -170,9 +162,8 @@ export function MigrateAccountStep() { - { setConfirmPassword(e.target.value); @@ -186,6 +177,7 @@ export function MigrateAccountStep() { {localErrors.confirmPassword && (

{localErrors.confirmPassword}

)} + {showPasswordMatch && } {/* Terms & Marketing */} diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/PasswordSection.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/PasswordSection.tsx index 964ed3b8..68e78603 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/PasswordSection.tsx +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/PasswordSection.tsx @@ -1,73 +1 @@ -"use client"; - -import { Input, Label } from "@/components/atoms"; -import { PasswordRequirements } from "@/features/auth/components"; -import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; -import type { AccountFormErrors } from "./types"; - -interface PasswordSectionProps { - password: string; - confirmPassword: string; - onPasswordChange: (value: string) => void; - onConfirmPasswordChange: (value: string) => void; - errors: AccountFormErrors; - clearError: (field: keyof AccountFormErrors) => void; - loading: boolean; -} - -export function PasswordSection({ - password, - confirmPassword, - onPasswordChange, - onConfirmPasswordChange, - errors, - clearError, - loading, -}: PasswordSectionProps) { - const { checks } = usePasswordValidation(password); - - return ( - <> -
- - { - onPasswordChange(e.target.value); - clearError("password"); - }} - placeholder="Create a strong password" - disabled={loading} - error={errors.password} - autoComplete="new-password" - /> - {errors.password &&

{errors.password}

} - -
- -
- - { - onConfirmPasswordChange(e.target.value); - clearError("confirmPassword"); - }} - placeholder="Confirm your password" - disabled={loading} - error={errors.confirmPassword} - autoComplete="new-password" - /> - {errors.confirmPassword &&

{errors.confirmPassword}

} -
- - ); -} +export { PasswordSection } from "@/features/auth/components"; diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts index 9ec548c8..3adb139d 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts @@ -1,7 +1,7 @@ import { useState, useCallback } from "react"; import { type JapanAddressFormData } from "@/features/address/components/JapanAddressForm"; import { prepareWhmcsAddressFields } from "@customer-portal/domain/address"; -import { validatePasswordRules } from "@/features/auth/hooks/usePasswordValidation"; +import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; import { phoneSchema } from "@customer-portal/domain/common"; import type { AccountFormErrors } from "./types"; @@ -54,6 +54,7 @@ export function useCompleteAccountForm({ const [acceptTerms, setAcceptTerms] = useState(initialValues.acceptTerms || false); const [marketingConsent, setMarketingConsent] = useState(initialValues.marketingConsent || false); const [errors, setErrors] = useState({}); + const { error: passwordValidationError } = usePasswordValidation(password); const clearError = useCallback((field: keyof AccountFormErrors) => { setErrors(prev => ({ ...prev, [field]: undefined })); @@ -88,8 +89,7 @@ export function useCompleteAccountForm({ newErrors.address = "Please complete the address"; } - const passwordError = validatePasswordRules(password); - if (passwordError) newErrors.password = passwordError; + if (passwordValidationError) newErrors.password = passwordValidationError; if (password !== confirmPassword) newErrors.confirmPassword = "Passwords do not match"; if (phone.trim()) { const phoneResult = phoneSchema.safeParse(phone.trim()); @@ -111,6 +111,7 @@ export function useCompleteAccountForm({ firstName, lastName, isAddressComplete, + passwordValidationError, password, confirmPassword, phone, diff --git a/apps/portal/src/features/services/components/eligibility-check/steps/CompleteAccountStep.tsx b/apps/portal/src/features/services/components/eligibility-check/steps/CompleteAccountStep.tsx index e43ca47c..363492f0 100644 --- a/apps/portal/src/features/services/components/eligibility-check/steps/CompleteAccountStep.tsx +++ b/apps/portal/src/features/services/components/eligibility-check/steps/CompleteAccountStep.tsx @@ -11,10 +11,7 @@ import { useState, useCallback } from "react"; import { ArrowLeft } from "lucide-react"; import { Button, ErrorMessage } from "@/components/atoms"; import { TermsCheckbox, MarketingCheckbox } from "@/features/auth/components"; -import { - validatePasswordRules, - usePasswordValidation, -} from "@/features/auth/hooks/usePasswordValidation"; +import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; import { phoneSchema } from "@customer-portal/domain/common"; import { useEligibilityCheckStore } from "../../../stores/eligibility-check.store"; import { AccountInfoDisplay, PersonalInfoFields, PasswordSection } from "./complete-account"; @@ -50,14 +47,15 @@ export function CompleteAccountStep() { }); }, []); - const { isValid: isPasswordValid } = usePasswordValidation(accountData.password); + const { isValid: isPasswordValid, error: passwordValidationError } = usePasswordValidation( + accountData.password + ); const doPasswordsMatch = accountData.password === accountData.confirmPassword; const validateAccountForm = useCallback((): boolean => { const errors: AccountFormErrors = {}; - const passwordError = validatePasswordRules(accountData.password); - if (passwordError) errors.password = passwordError; + if (passwordValidationError) errors.password = passwordValidationError; if (accountData.password !== accountData.confirmPassword) errors.confirmPassword = "Passwords do not match"; if (accountData.phone.trim()) { @@ -74,7 +72,7 @@ export function CompleteAccountStep() { setAccountErrors(errors); return Object.keys(errors).length === 0; - }, [accountData]); + }, [accountData, passwordValidationError]); const handleSubmit = async () => { if (!validateAccountForm()) return; diff --git a/apps/portal/src/features/services/components/eligibility-check/steps/complete-account/PasswordSection.tsx b/apps/portal/src/features/services/components/eligibility-check/steps/complete-account/PasswordSection.tsx index 209c0afb..68e78603 100644 --- a/apps/portal/src/features/services/components/eligibility-check/steps/complete-account/PasswordSection.tsx +++ b/apps/portal/src/features/services/components/eligibility-check/steps/complete-account/PasswordSection.tsx @@ -1,78 +1 @@ -"use client"; - -import { Input, Label, ErrorMessage } from "@/components/atoms"; -import { PasswordRequirements, PasswordMatchIndicator } from "@/features/auth/components"; -import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation"; - -interface PasswordSectionProps { - password: string; - confirmPassword: string; - onPasswordChange: (value: string) => void; - onConfirmPasswordChange: (value: string) => void; - errors: { - password?: string | undefined; - confirmPassword?: string | undefined; - }; - clearError: (field: "password" | "confirmPassword") => void; - loading: boolean; -} - -export function PasswordSection({ - password, - confirmPassword, - onPasswordChange, - onConfirmPasswordChange, - errors, - clearError, - loading, -}: PasswordSectionProps) { - const { checks } = usePasswordValidation(password); - const showPasswordMatch = confirmPassword.length > 0 && !errors.confirmPassword; - const passwordsMatch = password === confirmPassword; - - return ( - <> -
- - { - onPasswordChange(e.target.value); - clearError("password"); - }} - placeholder="Create a strong password" - disabled={loading} - error={errors.password} - autoComplete="new-password" - /> - {errors.password} - -
- -
- - { - onConfirmPasswordChange(e.target.value); - clearError("confirmPassword"); - }} - placeholder="Confirm your password" - disabled={loading} - error={errors.confirmPassword} - autoComplete="new-password" - /> - {errors.confirmPassword} - {showPasswordMatch && } -
- - ); -} +export { PasswordSection } from "@/features/auth/components";