From 7dff7dc7285095517ac6187874f0bc4c2aee79e8 Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 20 Jan 2026 19:40:55 +0900 Subject: [PATCH] refactor(auth): remove SignupForm and related steps, migrate to new account creation flow - Deleted SignupForm component and its associated steps (AccountStep, AddressStep, PasswordStep, ReviewStep). - Removed references to SignupForm in index files and views. - Updated MigrateAccountView to redirect to the new get-started flow. - Adjusted PublicInternetConfigureView to use InlineGetStartedSection instead of InlineAuthSection. - Cleaned up unused hooks and store methods related to WHMCS linking. --- .../app/(public)/(site)/auth/migrate/page.tsx | 4 +- apps/portal/src/core/api/index.ts | 1 - .../auth/components/AuthModal/AuthModal.tsx | 140 ------ .../auth/components/AuthModal/index.ts | 1 - .../InlineAuthSection/InlineAuthSection.tsx | 134 ------ .../LinkWhmcsForm/LinkWhmcsForm.tsx | 86 ---- .../auth/components/LoginForm/LoginForm.tsx | 4 +- .../SetPasswordForm/SetPasswordForm.tsx | 4 +- .../components/SignupForm/MultiStepForm.tsx | 122 ----- .../auth/components/SignupForm/SignupForm.tsx | 430 ------------------ .../auth/components/SignupForm/index.ts | 3 - .../SignupForm/steps/AccountStep.tsx | 178 -------- .../SignupForm/steps/AddressStep.tsx | 182 -------- .../SignupForm/steps/PasswordStep.tsx | 166 ------- .../SignupForm/steps/ReviewStep.tsx | 225 --------- .../auth/components/SignupForm/steps/index.ts | 4 - .../src/features/auth/components/index.ts | 3 - apps/portal/src/features/auth/hooks/index.ts | 1 - .../src/features/auth/hooks/use-auth.ts | 18 - .../src/features/auth/stores/auth.store.ts | 28 -- .../auth/views/MigrateAccountView.tsx | 104 ----- .../features/auth/views/SetPasswordView.tsx | 4 +- .../src/features/auth/views/SignupView.tsx | 31 -- apps/portal/src/features/auth/views/index.ts | 2 - .../views/PublicInternetConfigure.tsx | 7 +- 25 files changed, 12 insertions(+), 1870 deletions(-) delete mode 100644 apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx delete mode 100644 apps/portal/src/features/auth/components/AuthModal/index.ts delete mode 100644 apps/portal/src/features/auth/components/InlineAuthSection/InlineAuthSection.tsx delete mode 100644 apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/index.ts delete mode 100644 apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx delete mode 100644 apps/portal/src/features/auth/components/SignupForm/steps/index.ts delete mode 100644 apps/portal/src/features/auth/views/MigrateAccountView.tsx delete mode 100644 apps/portal/src/features/auth/views/SignupView.tsx diff --git a/apps/portal/src/app/(public)/(site)/auth/migrate/page.tsx b/apps/portal/src/app/(public)/(site)/auth/migrate/page.tsx index 97b8450a..67803013 100644 --- a/apps/portal/src/app/(public)/(site)/auth/migrate/page.tsx +++ b/apps/portal/src/app/(public)/(site)/auth/migrate/page.tsx @@ -1,5 +1,5 @@ -import { MigrateAccountView } from "@/features/auth/views/MigrateAccountView"; +import { redirect } from "next/navigation"; export default function MigrateAccountPage() { - return ; + redirect("/auth/get-started"); } diff --git a/apps/portal/src/core/api/index.ts b/apps/portal/src/core/api/index.ts index 01494fe4..bc41194f 100644 --- a/apps/portal/src/core/api/index.ts +++ b/apps/portal/src/core/api/index.ts @@ -25,7 +25,6 @@ export * from "./response-helpers"; const AUTH_ENDPOINTS = [ "/api/auth/login", "/api/auth/signup", - "/api/auth/migrate", "/api/auth/set-password", "/api/auth/reset-password", "/api/auth/check-password-needed", diff --git a/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx b/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx deleted file mode 100644 index bd65a7d5..00000000 --- a/apps/portal/src/features/auth/components/AuthModal/AuthModal.tsx +++ /dev/null @@ -1,140 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { XMarkIcon } from "@heroicons/react/24/outline"; -import { SignupForm } from "../SignupForm/SignupForm"; -import { LoginForm } from "../LoginForm/LoginForm"; -import { useAuthStore } from "../../stores/auth.store"; -import { useRouter } from "next/navigation"; - -interface AuthModalProps { - isOpen: boolean; - onClose: () => void; - initialMode?: "signup" | "login" | undefined; - redirectTo?: string | undefined; - title?: string | undefined; - description?: string | undefined; - showCloseButton?: boolean | undefined; -} - -export function AuthModal({ - isOpen, - onClose, - initialMode = "signup", - redirectTo, - title, - description, - showCloseButton = true, -}: AuthModalProps) { - const [mode, setMode] = useState<"signup" | "login">(initialMode); - const router = useRouter(); - const isAuthenticated = useAuthStore(state => state.isAuthenticated); - const hasCheckedAuth = useAuthStore(state => state.hasCheckedAuth); - - // Update mode when initialMode changes - useEffect(() => { - setMode(initialMode); - }, [initialMode]); - - // Close modal and redirect when authenticated - useEffect(() => { - if (isOpen && hasCheckedAuth && isAuthenticated && redirectTo) { - onClose(); - router.push(redirectTo); - } - }, [isOpen, hasCheckedAuth, isAuthenticated, redirectTo, onClose, router]); - - if (!isOpen) return null; - - const defaultTitle = mode === "signup" ? "Create your account" : "Sign in to continue"; - const defaultDescription = - mode === "signup" - ? "Create an account to continue with your order and access personalized plans." - : "Sign in to your account to continue with your order."; - - return ( -
{ - if (e.target === e.currentTarget) { - onClose(); - } - }} - > - {/* Backdrop */} - - ); -} diff --git a/apps/portal/src/features/auth/components/AuthModal/index.ts b/apps/portal/src/features/auth/components/AuthModal/index.ts deleted file mode 100644 index 11ee2cc1..00000000 --- a/apps/portal/src/features/auth/components/AuthModal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AuthModal } from "./AuthModal"; diff --git a/apps/portal/src/features/auth/components/InlineAuthSection/InlineAuthSection.tsx b/apps/portal/src/features/auth/components/InlineAuthSection/InlineAuthSection.tsx deleted file mode 100644 index fba5ceeb..00000000 --- a/apps/portal/src/features/auth/components/InlineAuthSection/InlineAuthSection.tsx +++ /dev/null @@ -1,134 +0,0 @@ -"use client"; - -import { useState } from "react"; -import { useRouter } from "next/navigation"; -import { Button } from "@/components/atoms/button"; -import { SignupForm } from "../SignupForm/SignupForm"; -import { LoginForm } from "../LoginForm/LoginForm"; -import { LinkWhmcsForm } from "../LinkWhmcsForm/LinkWhmcsForm"; -import { getSafeRedirect } from "@/features/auth/utils/route-protection"; - -interface HighlightItem { - title: string; - description: string; -} - -interface InlineAuthSectionProps { - title: string; - description?: string | undefined; - redirectTo?: string | undefined; - initialMode?: "signup" | "login" | undefined; - highlights?: HighlightItem[] | undefined; - className?: string | undefined; -} - -export function InlineAuthSection({ - title, - description, - redirectTo, - initialMode = "signup", - highlights = [], - className = "", -}: InlineAuthSectionProps) { - const router = useRouter(); - const [mode, setMode] = useState<"signup" | "login" | "migrate">(initialMode); - const safeRedirect = getSafeRedirect(redirectTo, "/account"); - - return ( -
-
-

{title}

- {description && ( -

{description}

- )} -
- -
-
- - - -
-
- -
-
- {mode === "signup" && ( - <> -

Create your account

-

- Set up your portal access in a few simple steps. -

- - - )} - {mode === "login" && ( - <> -

Sign in

-

Access your account to continue.

- - - )} - {mode === "migrate" && ( - <> -

Migrate your account

-

- Use your legacy portal credentials to transfer your account. -

- { - if (result.needsPasswordSet) { - const params = new URLSearchParams({ - email: result.user.email, - redirect: safeRedirect, - }); - router.push(`/auth/set-password?${params.toString()}`); - return; - } - router.push(safeRedirect); - }} - /> - - )} -
-
- - {highlights.length > 0 && ( -
-
- {highlights.map(item => ( -
-
{item.title}
-
{item.description}
-
- ))} -
-
- )} -
- ); -} diff --git a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx b/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx deleted file mode 100644 index 86d4fd59..00000000 --- a/apps/portal/src/features/auth/components/LinkWhmcsForm/LinkWhmcsForm.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Link WHMCS Form - Account migration form using domain schema - */ - -"use client"; - -import { Button, Input, ErrorMessage } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField/FormField"; -import { useWhmcsLink } from "@/features/auth/hooks"; -import { linkWhmcsRequestSchema, type LinkWhmcsResponse } from "@customer-portal/domain/auth"; -import { useZodForm } from "@/shared/hooks"; - -interface LinkWhmcsFormProps { - onTransferred?: ((result: LinkWhmcsResponse) => void) | undefined; - className?: string | undefined; - initialEmail?: string | undefined; -} - -export function LinkWhmcsForm({ onTransferred, className = "", initialEmail }: LinkWhmcsFormProps) { - const { linkWhmcs, loading, error, clearError } = useWhmcsLink(); - - const form = useZodForm({ - schema: linkWhmcsRequestSchema, - initialValues: { email: initialEmail ?? "", password: "" }, - onSubmit: async data => { - clearError(); - const result = await linkWhmcs(data); - onTransferred?.(result); - }, - }); - - const isLoading = form.isSubmitting || loading; - - return ( -
void form.handleSubmit(e)} className={`space-y-5 ${className}`}> - - form.setValue("email", e.target.value)} - onBlur={() => form.setTouchedField("email")} - placeholder="you@example.com" - disabled={isLoading} - autoComplete="email" - autoFocus - /> - - - - form.setValue("password", e.target.value)} - onBlur={() => form.setTouchedField("password")} - placeholder="Enter your legacy portal password" - disabled={isLoading} - autoComplete="current-password" - /> - - - {error && {error}} - - - -

- Your credentials are encrypted and used only to verify your identity -

-
- ); -} diff --git a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx index 628145d7..1b43e2f5 100644 --- a/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx +++ b/apps/portal/src/features/auth/components/LoginForm/LoginForm.tsx @@ -171,10 +171,10 @@ export function LoginForm({

Existing customer?{" "} - Migrate your account + Transfer your account

diff --git a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx index e713b001..0721abe7 100644 --- a/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx +++ b/apps/portal/src/features/auth/components/SetPasswordForm/SetPasswordForm.tsx @@ -8,7 +8,7 @@ import { useEffect } from "react"; import Link from "next/link"; import { Button, Input, ErrorMessage } from "@/components/atoms"; import { FormField } from "@/components/molecules/FormField/FormField"; -import { useWhmcsLink } from "../../hooks/use-auth"; +import { useAuth } from "../../hooks/use-auth"; import { useZodForm } from "@/shared/hooks"; import { setPasswordRequestSchema, @@ -38,7 +38,7 @@ export function SetPasswordForm({ onError, className = "", }: SetPasswordFormProps) { - const { setPassword, loading, error, clearError } = useWhmcsLink(); + const { setPassword, loading, error, clearError } = useAuth(); const form = useZodForm({ schema: setPasswordFormSchema, diff --git a/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx b/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx deleted file mode 100644 index 850790e9..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/MultiStepForm.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Multi-Step Form Wrapper - * Clean, modern UI for multi-step signup forms - */ - -"use client"; - -import { useEffect, type ReactNode } from "react"; -import { Button } from "@/components/atoms"; -import { CheckIcon } from "@heroicons/react/24/solid"; - -export interface FormStep { - key: string; - title: string; - description: string; - content: ReactNode; - isValid?: boolean; -} - -interface MultiStepFormProps { - steps: FormStep[]; - currentStep: number; - onNext: () => void; - onPrevious: () => void; - isLastStep: boolean; - isSubmitting?: boolean; - canProceed?: boolean; - onStepChange?: (stepIndex: number) => void; - className?: string; -} - -export function MultiStepForm({ - steps, - currentStep, - onNext, - onPrevious, - isLastStep, - isSubmitting = false, - canProceed = true, - onStepChange, - className = "", -}: MultiStepFormProps) { - useEffect(() => { - onStepChange?.(currentStep); - }, [currentStep, onStepChange]); - - const step = steps[currentStep] ?? steps[0]; - const isFirstStep = currentStep === 0; - const disableNext = isSubmitting || (!canProceed && !isLastStep); - - return ( -
- {/* Simple Step Indicators */} -
- {steps.map((s, idx) => { - const isCompleted = idx < currentStep; - const isCurrent = idx === currentStep; - - return ( -
-
- {isCompleted ? : idx + 1} -
- {idx < steps.length - 1 && ( -
- )} -
- ); - })} -
- - {/* Step Title & Description */} -
-

{step?.title}

-

{step?.description}

-
- - {/* Step Content */} -
{step?.content}
- - {/* Navigation Buttons */} -
- - - -
-
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx b/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx deleted file mode 100644 index ad701f90..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/SignupForm.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/** - * Signup Form - Multi-step signup for Japanese customers - * Uses domain schemas from @customer-portal/domain - */ - -"use client"; - -import { useState, useCallback, useEffect, useRef } from "react"; -import { flushSync } from "react-dom"; -import Link from "next/link"; -import { useSearchParams } from "next/navigation"; -import { ErrorMessage } from "@/components/atoms"; -import { useSignupWithRedirect } from "../../hooks/use-auth"; -import { signupInputSchema, buildSignupRequest } from "@customer-portal/domain/auth"; -import { genderEnum } from "@customer-portal/domain/common"; -import { addressFormSchema } from "@customer-portal/domain/customer"; -import { useZodForm } from "@/shared/hooks"; -import { z } from "zod"; -import { getSafeRedirect } from "@/features/auth/utils/route-protection"; -import { formatJapanesePostalCode } from "@/shared/constants"; - -import { MultiStepForm } from "./MultiStepForm"; -import { AccountStep } from "./steps/AccountStep"; -import { AddressStepJapan } from "@/features/address"; -import { PasswordStep } from "./steps/PasswordStep"; - -/** - * Frontend signup form schema - * Extends the domain signupInputSchema with: - * - confirmPassword: UI-only field for password confirmation - * - phoneCountryCode: Separate field for country code input - * - address: Required addressFormSchema (domain schema makes it optional) - * - dateOfBirth: Required for signup (domain schema makes it optional) - * - gender: Required for signup (domain schema makes it optional) - */ -const genderSchema = genderEnum; - -const signupAddressSchema = addressFormSchema.extend({ - address2: z - .string() - .min(1, "Address line 2 is required") - .max(200, "Address line 2 is too long") - .trim(), -}); - -const signupFormBaseSchema = signupInputSchema.omit({ sfNumber: true }).extend({ - confirmPassword: z.string().min(1, "Please confirm your password"), - phoneCountryCode: z.string().regex(/^\+\d{1,4}$/, "Enter a valid country code (e.g., +81)"), - address: signupAddressSchema, - dateOfBirth: z.string().min(1, "Date of birth is required"), - gender: genderSchema, -}); - -const signupFormSchema = signupFormBaseSchema - .refine(data => data.acceptTerms === true, { - message: "You must accept the terms and conditions", - path: ["acceptTerms"], - }) - .refine(data => data.password === data.confirmPassword, { - message: "Passwords do not match", - path: ["confirmPassword"], - }); - -type SignupFormData = z.infer; -type SignupAddress = SignupFormData["address"]; - -interface SignupFormProps { - onSuccess?: (() => void) | undefined; - onError?: ((error: string) => void) | undefined; - className?: string | undefined; - redirectTo?: string | undefined; - initialEmail?: string | undefined; - showFooterLinks?: boolean | undefined; -} - -const STEPS = [ - { - key: "account", - title: "Account Details", - description: "Your contact information", - }, - { - key: "address", - title: "Address", - description: "Used for service eligibility and delivery", - }, - { - key: "security", - title: "Security & Terms", - description: "Create a password and confirm agreements", - }, -] as const; - -const STEP_FIELD_KEYS: Record<(typeof STEPS)[number]["key"], Array> = { - account: ["firstName", "lastName", "email", "phone", "phoneCountryCode", "dateOfBirth", "gender"], - address: ["address"], - security: ["password", "confirmPassword", "acceptTerms", "marketingConsent"], -}; - -const STEP_VALIDATION_SCHEMAS: Record<(typeof STEPS)[number]["key"], z.ZodTypeAny | undefined> = { - account: signupFormBaseSchema.pick({ - firstName: true, - lastName: true, - email: true, - phone: true, - phoneCountryCode: true, - dateOfBirth: true, - gender: true, - }), - address: signupFormBaseSchema.pick({ - address: true, - }), - security: signupFormBaseSchema - .pick({ - password: true, - confirmPassword: true, - acceptTerms: true, - }) - .refine(data => data.password === data.confirmPassword, { - message: "Passwords do not match", - path: ["confirmPassword"], - }) - .refine(data => data.acceptTerms === true, { - message: "You must accept the terms and conditions", - path: ["acceptTerms"], - }), -}; - -export function SignupForm({ - onSuccess, - onError, - className = "", - redirectTo, - initialEmail, - showFooterLinks = true, -}: SignupFormProps) { - const formRef = useRef(null); - const searchParams = useSearchParams(); - const [step, setStep] = useState(0); - const redirectFromQuery = searchParams?.get("next") || searchParams?.get("redirect"); - const redirect = getSafeRedirect(redirectTo || redirectFromQuery, ""); - const redirectQuery = redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""; - const { signup, loading, error, clearError } = useSignupWithRedirect( - redirect ? { redirectTo: redirect } : undefined - ); - - const form = useZodForm({ - schema: signupFormSchema, - initialValues: { - firstName: "", - lastName: "", - email: initialEmail ?? "", - phone: "", - phoneCountryCode: "+81", - company: "", - dateOfBirth: "", - gender: "" as unknown as "male" | "female" | "other", // Will be validated on submit - address: { - address1: "", - address2: "", - city: "", - state: "", - postcode: "", - country: "JP", - countryCode: "JP", - }, - password: "", - confirmPassword: "", - acceptTerms: false, - marketingConsent: false, - }, - onSubmit: async data => { - clearError(); - try { - // Combine country code + phone for WHMCS format: +CC.NNNNNNNN - const countryDigits = data.phoneCountryCode.replace(/\D/g, ""); - const phoneDigits = data.phone.replace(/\D/g, ""); - const formattedPhone = `+${countryDigits}.${phoneDigits}`; - - // Build request with normalized address and phone - // Exclude UI-only fields (confirmPassword) from the request - const { confirmPassword: _confirmPassword, ...requestData } = data; - const request = buildSignupRequest({ - ...requestData, - phone: formattedPhone, - dateOfBirth: data.dateOfBirth || undefined, - gender: data.gender || undefined, - address: { - ...data.address, - country: "JP", - countryCode: "JP", - }, - }); - await signup(request); - onSuccess?.(); - } catch (err) { - onError?.(err instanceof Error ? err.message : "Signup failed"); - throw err; - } - }, - }); - - const { - values, - errors, - touched, - setValue: setFormValue, - setTouchedField: setFormTouchedField, - handleSubmit, - isSubmitting, - } = form; - - const normalizeAutofillValue = useCallback((field: string, value: string) => { - switch (field) { - case "phoneCountryCode": { - let normalized = value.replace(/[^\d+]/g, ""); - if (!normalized.startsWith("+")) normalized = "+" + normalized.replace(/\+/g, ""); - return normalized.slice(0, 5); - } - case "phone": - return value.replace(/\D/g, ""); - case "address.postcode": - return formatJapanesePostalCode(value); - default: - return value; - } - }, []); - - const syncStepValues = useCallback( - (shouldFlush = true) => { - const formNode = formRef.current; - if (!formNode) { - return values; - } - - const nextValues: SignupFormData = { - ...values, - address: { ...values.address }, - }; - - const fields = formNode.querySelectorAll( - "[data-field]" - ); - for (const field of fields) { - const key = field.dataset["field"]; - if (!key) { - continue; - } - const normalized = normalizeAutofillValue(key, field.value); - if (key.startsWith("address.")) { - const addressKey = key.replace("address.", "") as keyof SignupAddress; - nextValues.address[addressKey] = normalized; - } else if (key === "acceptTerms" || key === "marketingConsent") { - // Handle boolean fields separately - const boolValue = - field.type === "checkbox" ? (field as HTMLInputElement).checked : normalized === "true"; - (nextValues as Record)[key] = boolValue; - } else { - // Only assign to string fields - const stringKey = key as keyof Pick< - SignupFormData, - Exclude - >; - (nextValues as Record)[stringKey] = normalized; - } - } - - const applySyncedValues = () => { - for (const key of Object.keys(nextValues) as Array) { - if (key === "address") { - continue; - } - if (nextValues[key] !== values[key]) { - setFormValue(key, nextValues[key]); - } - } - - const addressChanged = (Object.keys(nextValues.address) as Array).some( - key => nextValues.address[key] !== values.address[key] - ); - if (addressChanged) { - setFormValue("address", nextValues.address); - } - }; - - if (shouldFlush) { - flushSync(() => { - applySyncedValues(); - }); - } else { - applySyncedValues(); - } - - return nextValues; - }, - [normalizeAutofillValue, setFormValue, values] - ); - - useEffect(() => { - const syncTimer = window.setTimeout(() => { - syncStepValues(false); - }, 0); - - return () => { - window.clearTimeout(syncTimer); - }; - }, [step, syncStepValues]); - - const isLastStep = step === STEPS.length - 1; - - const markStepTouched = useCallback( - (stepIndex: number) => { - const stepKey = STEPS[stepIndex]?.key; - if (!stepKey) { - return; - } - const fields = STEP_FIELD_KEYS[stepKey] ?? []; - for (const field of fields) setFormTouchedField(field); - }, - [setFormTouchedField] - ); - - const isStepValid = useCallback( - (stepIndex: number, data: SignupFormData = values) => { - const stepKey = STEPS[stepIndex]?.key; - if (!stepKey) { - return true; - } - const schema = STEP_VALIDATION_SCHEMAS[stepKey]; - if (!schema) { - return true; - } - return schema.safeParse(data).success; - }, - [values] - ); - - const handleNext = useCallback(() => { - const syncedValues = syncStepValues(); - markStepTouched(step); - - if (isLastStep) { - void handleSubmit(); - return; - } - - if (!isStepValid(step, syncedValues)) { - return; - } - - setStep(s => Math.min(s + 1, STEPS.length - 1)); - }, [handleSubmit, isLastStep, isStepValid, markStepTouched, step, syncStepValues]); - - const handlePrevious = useCallback(() => { - setStep(s => Math.max(0, s - 1)); - }, []); - - // Wrap form methods to have generic types for step components - const formProps = { - values, - errors, - touched, - setValue: (field: string, value: unknown) => - setFormValue(field as keyof SignupFormData, value as never), - setTouchedField: (field: string) => setFormTouchedField(field as keyof SignupFormData), - }; - - const stepContent = [ - , - , - , - ]; - - const steps = STEPS.map((s, i) => ({ - ...s, - content: stepContent[i], - })); - - return ( -
-
{ - event.preventDefault(); - handleNext(); - }} - > - - - - {error && ( - - {error} - - )} - - {showFooterLinks && ( -
-

- Already have an account?{" "} - - Sign in - -

-

- Existing customer?{" "} - - Migrate your account - -

-
- )} -
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/index.ts b/apps/portal/src/features/auth/components/SignupForm/index.ts deleted file mode 100644 index 7aec3070..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { SignupForm } from "./SignupForm"; -export { MultiStepForm } from "./MultiStepForm"; -export * from "./steps"; diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx deleted file mode 100644 index 2ffe8f93..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/steps/AccountStep.tsx +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Account Step - Contact info - */ - -"use client"; - -import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField/FormField"; -import { genderEnum } from "@customer-portal/domain/common"; - -interface AccountStepProps { - form: { - values: { - firstName: string; - lastName: string; - email: string; - phone: string; - phoneCountryCode: string; - company?: string | undefined; - dateOfBirth?: string | undefined; - gender?: "male" | "female" | "other" | undefined; - }; - errors: Record; - touched: Record; - setValue: (field: string, value: unknown) => void; - setTouchedField: (field: string) => void; - }; -} - -export function AccountStep({ form }: AccountStepProps) { - const { values, errors, touched, setValue, setTouchedField } = form; - const getError = (field: string) => (touched[field] ? errors[field] : undefined); - const genderOptions = genderEnum.options; - const formatGender = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); - - return ( -
- {/* Name Fields */} -
- - setValue("firstName", e.target.value)} - onBlur={() => setTouchedField("firstName")} - placeholder="Taro" - autoComplete="section-signup given-name" - autoFocus - data-field="firstName" - /> - - - setValue("lastName", e.target.value)} - onBlur={() => setTouchedField("lastName")} - placeholder="Yamada" - autoComplete="section-signup family-name" - data-field="lastName" - /> - -
- - {/* Email */} - - setValue("email", e.target.value)} - onBlur={() => setTouchedField("email")} - placeholder="taro.yamada@example.com" - autoComplete="section-signup email" - data-field="email" - /> - - - {/* Phone - Country code + number */} - -
- { - // Allow + and digits only, max 5 chars - let val = e.target.value.replace(/[^\d+]/g, ""); - if (!val.startsWith("+")) val = "+" + val.replace(/\+/g, ""); - if (val.length > 5) val = val.slice(0, 5); - setValue("phoneCountryCode", val); - }} - onBlur={() => setTouchedField("phoneCountryCode")} - placeholder="+81" - autoComplete="section-signup tel-country-code" - className="w-20 text-center" - data-field="phoneCountryCode" - /> - { - // Only allow digits - const cleaned = e.target.value.replace(/\D/g, ""); - setValue("phone", cleaned); - }} - onBlur={() => setTouchedField("phone")} - placeholder="9012345678" - autoComplete="section-signup tel-national" - className="flex-1" - data-field="phone" - /> -
-
- - {/* DOB + Gender (Required) */} -
- - setValue("dateOfBirth", e.target.value || undefined)} - onBlur={() => setTouchedField("dateOfBirth")} - autoComplete="section-signup bday" - data-field="dateOfBirth" - /> - - - - - -
- - {/* Company (Optional) */} - - setValue("company", e.target.value)} - onBlur={() => setTouchedField("company")} - placeholder="Company name" - autoComplete="section-signup organization" - data-field="company" - /> - -
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx deleted file mode 100644 index 1fed145f..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/steps/AddressStep.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/** - * Address Step - Japanese address input for WHMCS - * - * Field mapping to WHMCS: - * - postcode → postcode - * - state → state (prefecture) - * - city → city - * - address1 → address1 (building/room) - * - address2 → address2 (street/block) - * - country → "JP" - */ - -"use client"; - -import { useCallback, useEffect } from "react"; -import { ChevronDown } from "lucide-react"; -import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField/FormField"; -import { JAPAN_PREFECTURES, formatJapanesePostalCode } from "@/shared/constants"; - -interface AddressData { - address1: string; - address2: string; - city: string; - state: string; - postcode: string; - country: string; - countryCode?: string | undefined; -} - -interface AddressStepProps { - form: { - values: { address: AddressData }; - errors: Record; - touched: Record; - setValue: (field: string, value: unknown) => void; - setTouchedField: (field: string) => void; - }; -} - -export function AddressStep({ form }: AddressStepProps) { - const { values, errors, touched, setValue, setTouchedField } = form; - const address = values.address; - - const getError = (field: string) => { - const key = `address.${field}`; - return touched[key] || touched["address"] ? (errors[key] ?? errors[field]) : undefined; - }; - - const updateAddress = useCallback( - (field: keyof AddressData, value: string) => { - setValue("address", { ...address, [field]: value }); - }, - [address, setValue] - ); - - const handlePostcodeChange = useCallback( - (e: React.ChangeEvent) => { - const formatted = formatJapanesePostalCode(e.target.value); - updateAddress("postcode", formatted); - }, - [updateAddress] - ); - - const markTouched = () => setTouchedField("address"); - - // Set Japan as default country on mount if empty - useEffect(() => { - if (!address.country) { - setValue("address", { ...address, country: "JP", countryCode: "JP" }); - } - }, [address, setValue]); - - return ( -
- {/* Postal Code - First field for Japanese addresses */} - - - - - {/* Prefecture Selection */} - -
- -
- -
-
-
- - {/* City/Ward */} - - updateAddress("city", e.target.value)} - onBlur={markTouched} - placeholder="Shibuya-ku" - autoComplete="section-signup address-level2" - data-field="address.city" - /> - - - {/* Street / Block (Address 2) */} - - updateAddress("address2", e.target.value)} - onBlur={markTouched} - placeholder="2-20-9 Wakabayashi" - autoComplete="section-signup address-line1" - required - data-field="address.address2" - /> - - - {/* Building / Room (Address 1) */} - - updateAddress("address1", e.target.value)} - onBlur={markTouched} - placeholder="Gramercy 201" - autoComplete="section-signup address-line2" - required - data-field="address.address1" - /> - -
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx deleted file mode 100644 index 3c949477..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/steps/PasswordStep.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/** - * Password Step - Password creation with strength indicator - */ - -"use client"; - -import Link from "next/link"; -import { Input } from "@/components/atoms"; -import { FormField } from "@/components/molecules/FormField/FormField"; -import { checkPasswordStrength, getPasswordStrengthDisplay } from "@customer-portal/domain/auth"; - -interface PasswordStepProps { - form: { - values: { - email: string; - password: string; - confirmPassword: string; - acceptTerms: boolean; - marketingConsent?: boolean | undefined; - }; - errors: Record; - touched: Record; - setValue: (field: string, value: unknown) => void; - setTouchedField: (field: string) => void; - }; -} - -export function PasswordStep({ form }: PasswordStepProps) { - const { values, errors, touched, setValue, setTouchedField } = form; - const { requirements, strength, isValid } = checkPasswordStrength(values.password); - const { label, colorClass } = getPasswordStrengthDisplay(strength); - const passwordsMatch = values.password === values.confirmPassword; - - return ( -
- {/* Hidden email field for browser password manager to associate credentials */} - - - - setValue("password", e.target.value)} - onBlur={() => setTouchedField("password")} - placeholder="Create a secure password" - autoComplete="section-signup new-password" - data-field="password" - /> - - - {values.password && ( -
-
-
-
-
- - {label} - -
-
- {requirements.map(r => ( -
- - {r.met ? "✓" : "○"} - - {r.label} -
- ))} -
-
- )} - - - setValue("confirmPassword", e.target.value)} - onBlur={() => setTouchedField("confirmPassword")} - placeholder="Re-enter your password" - autoComplete="section-signup new-password" - data-field="confirmPassword" - /> - - - {values.confirmPassword && ( -

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

- )} - -
- - {touched["acceptTerms"] && errors["acceptTerms"] && ( -

{errors["acceptTerms"]}

- )} - - -
-
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx b/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx deleted file mode 100644 index a677314b..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/steps/ReviewStep.tsx +++ /dev/null @@ -1,225 +0,0 @@ -/** - * Review Step - Summary and terms acceptance - */ - -"use client"; - -import Link from "next/link"; - -type FormErrors = Record; - -/** Format field names for display (e.g., "address.city" → "City") */ -function formatFieldName(field: string): string { - return field - .replace("address.", "") - .replace(/([A-Z])/g, " $1") - .replace(/^./, s => s.toUpperCase()) - .trim(); -} - -/** Display validation errors from previous steps */ -function ValidationErrors({ errors }: { errors: FormErrors }) { - // Collect errors excluding acceptTerms (shown inline) and _form (shown separately) - const fieldErrors = Object.entries(errors) - .filter(([key, value]) => value && key !== "acceptTerms" && key !== "_form") - .map(([key, value]) => ({ field: key, message: value })); - - const hasErrors = fieldErrors.length > 0 || errors["_form"]; - if (!hasErrors) return null; - - return ( -
-

Please fix the following errors:

-
    - {errors["_form"] &&
  • {errors["_form"]}
  • } - {fieldErrors.map(({ field, message }) => ( -
  • - {formatFieldName(field)}: {message} -
  • - ))} -
-
- ); -} - -/** Ready message shown when form is valid */ -function ReadyMessage({ errors }: { errors: FormErrors }) { - const hasErrors = Object.values(errors).some(Boolean); - if (hasErrors) return null; - - return ( -
- 🚀 -
-

Ready to create your account!

-

- Click "Create Account" below to complete your registration. -

-
-
- ); -} - -interface ReviewStepProps { - form: { - values: { - firstName: string; - lastName: string; - email: string; - phone: string; - phoneCountryCode: string; - company?: string; - dateOfBirth?: string; - gender?: "male" | "female" | "other"; - address: { - address1: string; - address2: string; - city: string; - state: string; - postcode: string; - country: string; - }; - acceptTerms: boolean; - marketingConsent?: boolean; - }; - errors: Record; - touched: Record; - setValue: (field: string, value: unknown) => void; - setTouchedField: (field: string) => void; - }; -} - -export function ReviewStep({ form }: ReviewStepProps) { - const { values, errors, setValue, setTouchedField } = form; - const address = values.address; - - // Format address for display - const formattedAddress = [ - address.address2, - address.address1, - address.city, - address.state, - address.postcode, - ] - .filter(Boolean) - .join(", "); - - return ( -
- {/* Account Summary */} -
-

- - ✓ - - Account Summary -

-
-
-
Name
-
- {values.firstName} {values.lastName} -
-
-
-
Email
-
{values.email}
-
-
-
Phone
-
- {values.phoneCountryCode} {values.phone} -
-
- {values.company && ( -
-
Company
-
{values.company}
-
- )} - {values.dateOfBirth && ( -
-
Date of Birth
-
{values.dateOfBirth}
-
- )} - {values.gender && ( -
-
Gender
-
{values.gender}
-
- )} -
-
- - {/* Address Summary */} - {address?.address1 && ( -
-

- - 📍 - - Delivery Address -

-

{formattedAddress}

-
- )} - - {/* Terms & Conditions */} -
-

Terms & Agreements

- - - {errors["acceptTerms"] && ( -

{errors["acceptTerms"]}

- )} - - -
- - - - -
- ); -} diff --git a/apps/portal/src/features/auth/components/SignupForm/steps/index.ts b/apps/portal/src/features/auth/components/SignupForm/steps/index.ts deleted file mode 100644 index 7bf590d7..00000000 --- a/apps/portal/src/features/auth/components/SignupForm/steps/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { AccountStep } from "./AccountStep"; -export { AddressStep } from "./AddressStep"; -export { PasswordStep } from "./PasswordStep"; -export { ReviewStep } from "./ReviewStep"; diff --git a/apps/portal/src/features/auth/components/index.ts b/apps/portal/src/features/auth/components/index.ts index b98495f1..577dcb66 100644 --- a/apps/portal/src/features/auth/components/index.ts +++ b/apps/portal/src/features/auth/components/index.ts @@ -4,9 +4,6 @@ */ export { LoginForm } from "./LoginForm/LoginForm"; -export { SignupForm } from "./SignupForm/SignupForm"; export { PasswordResetForm } from "./PasswordResetForm/PasswordResetForm"; export { SetPasswordForm } from "./SetPasswordForm/SetPasswordForm"; -export { LinkWhmcsForm } from "./LinkWhmcsForm/LinkWhmcsForm"; -export { InlineAuthSection } from "./InlineAuthSection/InlineAuthSection"; export { AuthLayout } from "@/components/templates/AuthLayout"; diff --git a/apps/portal/src/features/auth/hooks/index.ts b/apps/portal/src/features/auth/hooks/index.ts index a1dea43a..645912dd 100644 --- a/apps/portal/src/features/auth/hooks/index.ts +++ b/apps/portal/src/features/auth/hooks/index.ts @@ -9,7 +9,6 @@ export { useSignup, usePasswordReset, usePasswordChange, - useWhmcsLink, useSession, useUser, usePermissions, diff --git a/apps/portal/src/features/auth/hooks/use-auth.ts b/apps/portal/src/features/auth/hooks/use-auth.ts index 0a8f2978..a27f9b42 100644 --- a/apps/portal/src/features/auth/hooks/use-auth.ts +++ b/apps/portal/src/features/auth/hooks/use-auth.ts @@ -36,7 +36,6 @@ export function useAuth() { resetPassword, changePassword, checkPasswordNeeded, - linkWhmcs, setPassword, checkAuth, refreshSession, @@ -93,7 +92,6 @@ export function useAuth() { resetPassword, changePassword, checkPasswordNeeded, - linkWhmcs, setPassword, checkAuth, refreshSession, @@ -165,22 +163,6 @@ export function usePasswordChange() { }; } -/** - * Hook for WHMCS linking functionality - */ -export function useWhmcsLink() { - const { checkPasswordNeeded, linkWhmcs, setPassword, loading, error, clearError } = useAuth(); - - return { - checkPasswordNeeded, - linkWhmcs, - setPassword, - loading, - error, - clearError, - }; -} - /** * Hook for session management */ diff --git a/apps/portal/src/features/auth/stores/auth.store.ts b/apps/portal/src/features/auth/stores/auth.store.ts index 6a513510..bc27e487 100644 --- a/apps/portal/src/features/auth/stores/auth.store.ts +++ b/apps/portal/src/features/auth/stores/auth.store.ts @@ -10,11 +10,8 @@ import { logger } from "@/core/logger"; import { authResponseSchema, checkPasswordNeededResponseSchema, - linkWhmcsResponseSchema, type AuthSession, type CheckPasswordNeededResponse, - type LinkWhmcsRequest, - type LinkWhmcsResponse, type LoginRequest, type SignupRequest, } from "@customer-portal/domain/auth"; @@ -47,7 +44,6 @@ export interface AuthState { resetPassword: (token: string, password: string) => Promise; changePassword: (currentPassword: string, newPassword: string) => Promise; checkPasswordNeeded: (email: string) => Promise; - linkWhmcs: (request: LinkWhmcsRequest) => Promise; setPassword: (email: string, password: string) => Promise; refreshUser: () => Promise; refreshSession: () => Promise; @@ -280,30 +276,6 @@ export const useAuthStore = create()((set, get) => { } }, - linkWhmcs: async (linkRequest: LinkWhmcsRequest) => { - set({ loading: true, error: null }); - try { - const response = await apiClient.POST("/api/auth/migrate", { - body: linkRequest, - disableCsrf: true, // Public auth endpoint, exempt from CSRF - }); - - const parsed = linkWhmcsResponseSchema.safeParse(response.data); - if (!parsed.success) { - throw new Error(parsed.error.issues?.[0]?.message ?? "WHMCS link failed"); - } - - set({ loading: false }); - return parsed.data; - } catch (error) { - set({ - loading: false, - error: error instanceof Error ? error.message : "WHMCS link failed", - }); - throw error; - } - }, - setPassword: async (email: string, password: string) => { set({ loading: true, error: null }); try { diff --git a/apps/portal/src/features/auth/views/MigrateAccountView.tsx b/apps/portal/src/features/auth/views/MigrateAccountView.tsx deleted file mode 100644 index 54c85a0e..00000000 --- a/apps/portal/src/features/auth/views/MigrateAccountView.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Migrate Account View - Account migration page - */ - -"use client"; - -import Link from "next/link"; -import { useRouter, useSearchParams } from "next/navigation"; -import { AuthLayout } from "../components"; -import { LinkWhmcsForm } from "@/features/auth/components"; -import { getSafeRedirect } from "@/features/auth/utils/route-protection"; -import { MIGRATION_TRANSFER_ITEMS, MIGRATION_STEPS } from "@customer-portal/domain/auth"; - -export function MigrateAccountView() { - const router = useRouter(); - const searchParams = useSearchParams(); - const initialEmail = searchParams.get("email") ?? undefined; - const redirectTo = getSafeRedirect(searchParams.get("redirect"), "/account"); - - return ( - -
- {/* What transfers */} -
-

What gets transferred:

-
    - {MIGRATION_TRANSFER_ITEMS.map((item, i) => ( -
  • - {item} -
  • - ))} -
-
- - {/* Form */} -
-

- Enter Legacy Portal Credentials -

-

- Use your previous Assist Solutions portal email and password. -

- { - if (result.needsPasswordSet) { - const params = new URLSearchParams({ - email: result.user.email, - redirect: redirectTo, - }); - router.push(`/auth/set-password?${params.toString()}`); - } else { - router.push(redirectTo); - } - }} - /> -
- - {/* Links */} -
- - New customer?{" "} - - Create account - - - - Already transferred?{" "} - - Sign in - - -
- - {/* Steps */} -
-

How it works

-
    - {MIGRATION_STEPS.map((step, i) => ( -
  1. - - {i + 1} - - {step} -
  2. - ))} -
-
- -

- Need help?{" "} - - Contact support - -

-
-
- ); -} - -export default MigrateAccountView; diff --git a/apps/portal/src/features/auth/views/SetPasswordView.tsx b/apps/portal/src/features/auth/views/SetPasswordView.tsx index 32df4755..67338559 100644 --- a/apps/portal/src/features/auth/views/SetPasswordView.tsx +++ b/apps/portal/src/features/auth/views/SetPasswordView.tsx @@ -16,7 +16,7 @@ function SetPasswordContent() { useEffect(() => { if (!email) { - router.replace("/auth/migrate"); + router.replace("/auth/get-started"); } }, [email, router]); @@ -33,7 +33,7 @@ function SetPasswordContent() { again so we can verify your account.

Go to account transfer diff --git a/apps/portal/src/features/auth/views/SignupView.tsx b/apps/portal/src/features/auth/views/SignupView.tsx deleted file mode 100644 index dbd71eba..00000000 --- a/apps/portal/src/features/auth/views/SignupView.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client"; - -import { AuthLayout } from "../components"; -import { SignupForm } from "@/features/auth/components"; -import { useAuthStore } from "../stores/auth.store"; -import { LoadingOverlay } from "@/components/atoms"; - -export function SignupView() { - const { loading, isAuthenticated } = useAuthStore(); - - return ( - <> - - - - - {/* Full-page loading overlay during authentication */} - - - ); -} - -export default SignupView; diff --git a/apps/portal/src/features/auth/views/index.ts b/apps/portal/src/features/auth/views/index.ts index 7e3ecdc2..7efe9c65 100644 --- a/apps/portal/src/features/auth/views/index.ts +++ b/apps/portal/src/features/auth/views/index.ts @@ -1,6 +1,4 @@ export { LoginView } from "./LoginView"; -export { SignupView } from "./SignupView"; export { ForgotPasswordView } from "./ForgotPasswordView"; export { ResetPasswordView } from "./ResetPasswordView"; export { SetPasswordView } from "./SetPasswordView"; -export { MigrateAccountView } from "./MigrateAccountView"; diff --git a/apps/portal/src/features/services/views/PublicInternetConfigure.tsx b/apps/portal/src/features/services/views/PublicInternetConfigure.tsx index 47c70209..5db3a23d 100644 --- a/apps/portal/src/features/services/views/PublicInternetConfigure.tsx +++ b/apps/portal/src/features/services/views/PublicInternetConfigure.tsx @@ -3,7 +3,7 @@ import { useSearchParams } from "next/navigation"; import { WifiIcon, ClockIcon, EnvelopeIcon, CheckCircleIcon } from "@heroicons/react/24/outline"; import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink"; -import { InlineAuthSection } from "@/features/auth/components/InlineAuthSection/InlineAuthSection"; +import { InlineGetStartedSection } from "@/features/get-started"; import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath"; import { usePublicInternetPlan } from "@/features/services/hooks"; import { CardPricing } from "@/features/services/components/base/CardPricing"; @@ -76,9 +76,10 @@ export function PublicInternetConfigureView() { )} {/* Auth Section - Primary focus */} -