Refactor Address Handling in Signup and Profile Components
- Updated address handling in Salesforce account and contact services to remove unnecessary address fields during signup, improving data management. - Refactored address input fields in the SignupForm and AddressForm components to ensure proper labeling and validation for Japanese address formats. - Enhanced address display logic in ProfileContainer and AddressConfirmation components for better user experience. - Standardized address field names and improved placeholder texts for clarity and consistency across the application.
This commit is contained in:
parent
cc0200bd5e
commit
1a3032bbd3
@ -175,14 +175,8 @@ export class SalesforceAccountService {
|
||||
): Promise<{ accountId: string; accountNumber: string }> {
|
||||
this.logger.log("Creating new Salesforce Account", { email: data.email });
|
||||
|
||||
const accountPayload = {
|
||||
const accountPayload: Record<string, unknown> = {
|
||||
Name: `${data.firstName} ${data.lastName}`,
|
||||
BillingStreet:
|
||||
data.address.address1 + (data.address.address2 ? `\n${data.address.address2}` : ""),
|
||||
BillingCity: data.address.city,
|
||||
BillingState: data.address.state,
|
||||
BillingPostalCode: data.address.postcode,
|
||||
BillingCountry: data.address.country,
|
||||
Phone: data.phone,
|
||||
// Portal tracking fields
|
||||
[this.portalStatusField]: "Active",
|
||||
@ -242,18 +236,12 @@ export class SalesforceAccountService {
|
||||
email: data.email,
|
||||
});
|
||||
|
||||
const contactPayload = {
|
||||
const contactPayload: Record<string, unknown> = {
|
||||
AccountId: data.accountId,
|
||||
FirstName: data.firstName,
|
||||
LastName: data.lastName,
|
||||
Email: data.email,
|
||||
Phone: data.phone,
|
||||
MailingStreet:
|
||||
data.address.address1 + (data.address.address2 ? `\n${data.address.address2}` : ""),
|
||||
MailingCity: data.address.city,
|
||||
MailingState: data.address.state,
|
||||
MailingPostalCode: data.address.postcode,
|
||||
MailingCountry: data.address.country,
|
||||
};
|
||||
|
||||
try {
|
||||
@ -351,14 +339,6 @@ export interface CreateSalesforceAccountRequest {
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
country: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@ -370,12 +350,4 @@ export interface CreateSalesforceContactRequest {
|
||||
lastName: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
address: {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
country: string;
|
||||
};
|
||||
}
|
||||
|
||||
@ -271,14 +271,7 @@ export class SignupWorkflowService {
|
||||
lastName,
|
||||
email: normalizedEmail,
|
||||
phone,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
address2: address.address2 || undefined,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
},
|
||||
// Address not added to Salesforce during signup
|
||||
});
|
||||
|
||||
await this.salesforceAccountService.createContact({
|
||||
@ -287,14 +280,7 @@ export class SignupWorkflowService {
|
||||
lastName,
|
||||
email: normalizedEmail,
|
||||
phone,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
address2: address.address2 || undefined,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
},
|
||||
// Address not added to Salesforce during signup
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@ -78,14 +78,6 @@ export class SignupAccountResolverService {
|
||||
lastName,
|
||||
email: normalizedEmail,
|
||||
phone,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
address2: address.address2 || undefined,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
},
|
||||
});
|
||||
|
||||
await this.salesforceAccountService.createContact({
|
||||
@ -94,14 +86,6 @@ export class SignupAccountResolverService {
|
||||
lastName,
|
||||
email: normalizedEmail,
|
||||
phone,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
address2: address.address2 || undefined,
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postcode: address.postcode,
|
||||
country: address.country,
|
||||
},
|
||||
});
|
||||
|
||||
const snapshot: SignupAccountSnapshot = {
|
||||
|
||||
@ -105,7 +105,7 @@ export class SignupWhmcsService {
|
||||
companyname: params.company || "",
|
||||
phonenumber: params.phone,
|
||||
address1: params.address.address1,
|
||||
address2: params.address.address2 || "",
|
||||
address2: params.address.address2 ?? "",
|
||||
city: params.address.city,
|
||||
state: params.address.state,
|
||||
postcode: params.address.postcode,
|
||||
|
||||
@ -88,8 +88,12 @@ export function AddressCard({
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1.5 text-foreground">
|
||||
{address.address1 && <p className="font-semibold text-base">{address.address1}</p>}
|
||||
{address.address2 && <p className="text-muted-foreground">{address.address2}</p>}
|
||||
{(address.address2 || address.address1) && (
|
||||
<p className="font-semibold text-base">{address.address2 || address.address1}</p>
|
||||
)}
|
||||
{address.address2 && address.address1 && (
|
||||
<p className="text-muted-foreground">{address.address1}</p>
|
||||
)}
|
||||
{(address.city || address.state || address.postcode) && (
|
||||
<p className="text-muted-foreground">
|
||||
{[address.city, address.state, address.postcode].filter(Boolean).join(", ")}
|
||||
|
||||
@ -460,11 +460,13 @@ export default function ProfileContainer() {
|
||||
{address.values.address1 || address.values.city ? (
|
||||
<div className="bg-card rounded-lg p-5 border border-border shadow-sm">
|
||||
<div className="text-foreground space-y-1.5">
|
||||
{address.values.address1 && (
|
||||
<p className="font-medium text-base">{address.values.address1}</p>
|
||||
{(address.values.address2 || address.values.address1) && (
|
||||
<p className="font-medium text-base">
|
||||
{address.values.address2 || address.values.address1}
|
||||
</p>
|
||||
)}
|
||||
{address.values.address2 && (
|
||||
<p className="text-muted-foreground">{address.values.address2}</p>
|
||||
{address.values.address2 && address.values.address1 && (
|
||||
<p className="text-muted-foreground">{address.values.address1}</p>
|
||||
)}
|
||||
<p className="text-muted-foreground">
|
||||
{[address.values.city, address.values.state, address.values.postcode]
|
||||
|
||||
@ -160,6 +160,15 @@ export function LoginForm({
|
||||
Sign up
|
||||
</Link>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Existing customer?{" "}
|
||||
<Link
|
||||
href={`/auth/migrate${redirectQuery}`}
|
||||
className="font-medium text-primary hover:text-primary-hover transition-colors duration-200"
|
||||
>
|
||||
Migrate your account
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
@ -5,16 +5,19 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
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";
|
||||
@ -30,12 +33,20 @@ import { PasswordStep } from "./steps/PasswordStep";
|
||||
* - dateOfBirth: Required for signup (domain schema makes it optional)
|
||||
* - gender: Required for signup (domain schema makes it optional)
|
||||
*/
|
||||
const genderSchema = z.enum(["male", "female", "other"]);
|
||||
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: addressFormSchema,
|
||||
address: signupAddressSchema,
|
||||
dateOfBirth: z.string().min(1, "Date of birth is required"),
|
||||
gender: genderSchema,
|
||||
});
|
||||
@ -51,6 +62,7 @@ const signupFormSchema = signupFormBaseSchema
|
||||
});
|
||||
|
||||
type SignupFormData = z.infer<typeof signupFormSchema>;
|
||||
type SignupAddress = SignupFormData["address"];
|
||||
|
||||
interface SignupFormProps {
|
||||
onSuccess?: () => void;
|
||||
@ -122,6 +134,7 @@ export function SignupForm({
|
||||
initialEmail,
|
||||
showFooterLinks = true,
|
||||
}: SignupFormProps) {
|
||||
const formRef = useRef<HTMLFormElement | null>(null);
|
||||
const searchParams = useSearchParams();
|
||||
const { signup, loading, error, clearError } = useSignupWithRedirect({ redirectTo });
|
||||
const [step, setStep] = useState(0);
|
||||
@ -163,8 +176,10 @@ export function SignupForm({
|
||||
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({
|
||||
...data,
|
||||
...requestData,
|
||||
phone: formattedPhone,
|
||||
dateOfBirth: data.dateOfBirth || undefined,
|
||||
gender: data.gender || undefined,
|
||||
@ -193,6 +208,105 @@ export function SignupForm({
|
||||
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;
|
||||
}
|
||||
},
|
||||
[formatJapanesePostalCode]
|
||||
);
|
||||
|
||||
const syncStepValues = useCallback(
|
||||
(shouldFlush = true) => {
|
||||
const formNode = formRef.current;
|
||||
if (!formNode) {
|
||||
return values;
|
||||
}
|
||||
|
||||
const nextValues: SignupFormData = {
|
||||
...values,
|
||||
address: { ...values.address },
|
||||
};
|
||||
|
||||
const fields = formNode.querySelectorAll<HTMLInputElement | HTMLSelectElement>(
|
||||
"[data-field]"
|
||||
);
|
||||
fields.forEach(field => {
|
||||
const key = field.dataset.field;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
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<string, unknown>)[key] = boolValue;
|
||||
} else {
|
||||
// Only assign to string fields
|
||||
const stringKey = key as keyof Pick<
|
||||
SignupFormData,
|
||||
Exclude<keyof SignupFormData, "address" | "acceptTerms" | "marketingConsent">
|
||||
>;
|
||||
(nextValues as Record<string, unknown>)[stringKey] = normalized;
|
||||
}
|
||||
});
|
||||
|
||||
const applySyncedValues = () => {
|
||||
(Object.keys(nextValues) as Array<keyof SignupFormData>).forEach(key => {
|
||||
if (key === "address") {
|
||||
return;
|
||||
}
|
||||
if (nextValues[key] !== values[key]) {
|
||||
setFormValue(key, nextValues[key]);
|
||||
}
|
||||
});
|
||||
|
||||
const addressChanged = (Object.keys(nextValues.address) as Array<keyof SignupAddress>).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(
|
||||
@ -208,7 +322,7 @@ export function SignupForm({
|
||||
);
|
||||
|
||||
const isStepValid = useCallback(
|
||||
(stepIndex: number) => {
|
||||
(stepIndex: number, data: SignupFormData = values) => {
|
||||
const stepKey = STEPS[stepIndex]?.key;
|
||||
if (!stepKey) {
|
||||
return true;
|
||||
@ -217,12 +331,13 @@ export function SignupForm({
|
||||
if (!schema) {
|
||||
return true;
|
||||
}
|
||||
return schema.safeParse(values).success;
|
||||
return schema.safeParse(data).success;
|
||||
},
|
||||
[values]
|
||||
);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
const syncedValues = syncStepValues();
|
||||
markStepTouched(step);
|
||||
|
||||
if (isLastStep) {
|
||||
@ -230,12 +345,12 @@ export function SignupForm({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isStepValid(step)) {
|
||||
if (!isStepValid(step, syncedValues)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStep(s => Math.min(s + 1, STEPS.length - 1));
|
||||
}, [handleSubmit, isLastStep, isStepValid, markStepTouched, step]);
|
||||
}, [handleSubmit, isLastStep, isStepValid, markStepTouched, step, syncStepValues]);
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
setStep(s => Math.max(0, s - 1));
|
||||
@ -264,15 +379,24 @@ export function SignupForm({
|
||||
|
||||
return (
|
||||
<div className={`w-full ${className}`}>
|
||||
<MultiStepForm
|
||||
steps={steps}
|
||||
currentStep={step}
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
isLastStep={isLastStep}
|
||||
isSubmitting={isSubmitting || loading}
|
||||
canProceed={isLastStep || isStepValid(step)}
|
||||
/>
|
||||
<form
|
||||
ref={formRef}
|
||||
autoComplete="on"
|
||||
onSubmit={event => {
|
||||
event.preventDefault();
|
||||
handleNext();
|
||||
}}
|
||||
>
|
||||
<MultiStepForm
|
||||
steps={steps}
|
||||
currentStep={step}
|
||||
onNext={handleNext}
|
||||
onPrevious={handlePrevious}
|
||||
isLastStep={isLastStep}
|
||||
isSubmitting={isSubmitting || loading}
|
||||
canProceed={isLastStep || isStepValid(step)}
|
||||
/>
|
||||
</form>
|
||||
|
||||
{error && (
|
||||
<ErrorMessage className="mt-4 text-center p-3 bg-danger-soft rounded-lg">
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
import { Input } from "@/components/atoms";
|
||||
import { FormField } from "@/components/molecules/FormField/FormField";
|
||||
import { genderEnum } from "@customer-portal/domain/common";
|
||||
|
||||
interface AccountStepProps {
|
||||
form: {
|
||||
@ -29,6 +30,8 @@ interface AccountStepProps {
|
||||
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 (
|
||||
<div className="space-y-5">
|
||||
@ -41,8 +44,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
onChange={e => setValue("firstName", e.target.value)}
|
||||
onBlur={() => setTouchedField("firstName")}
|
||||
placeholder="Taro"
|
||||
autoComplete="given-name"
|
||||
autoComplete="section-signup given-name"
|
||||
autoFocus
|
||||
data-field="firstName"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField label="Last Name" error={getError("lastName")} required>
|
||||
@ -52,7 +56,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
onChange={e => setValue("lastName", e.target.value)}
|
||||
onBlur={() => setTouchedField("lastName")}
|
||||
placeholder="Yamada"
|
||||
autoComplete="family-name"
|
||||
autoComplete="section-signup family-name"
|
||||
data-field="lastName"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
@ -66,7 +71,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
onChange={e => setValue("email", e.target.value)}
|
||||
onBlur={() => setTouchedField("email")}
|
||||
placeholder="taro.yamada@example.com"
|
||||
autoComplete="email"
|
||||
autoComplete="section-signup email"
|
||||
data-field="email"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@ -90,8 +96,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
}}
|
||||
onBlur={() => setTouchedField("phoneCountryCode")}
|
||||
placeholder="+81"
|
||||
autoComplete="tel-country-code"
|
||||
autoComplete="section-signup tel-country-code"
|
||||
className="w-20 text-center"
|
||||
data-field="phoneCountryCode"
|
||||
/>
|
||||
<Input
|
||||
name="tel-national"
|
||||
@ -104,8 +111,9 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
}}
|
||||
onBlur={() => setTouchedField("phone")}
|
||||
placeholder="9012345678"
|
||||
autoComplete="tel-national"
|
||||
autoComplete="section-signup tel-national"
|
||||
className="flex-1"
|
||||
data-field="phone"
|
||||
/>
|
||||
</div>
|
||||
</FormField>
|
||||
@ -119,7 +127,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
value={values.dateOfBirth ?? ""}
|
||||
onChange={e => setValue("dateOfBirth", e.target.value || undefined)}
|
||||
onBlur={() => setTouchedField("dateOfBirth")}
|
||||
autoComplete="bday"
|
||||
autoComplete="section-signup bday"
|
||||
data-field="dateOfBirth"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@ -129,6 +138,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
value={values.gender ?? ""}
|
||||
onChange={e => setValue("gender", e.target.value || undefined)}
|
||||
onBlur={() => setTouchedField("gender")}
|
||||
autoComplete="section-signup sex"
|
||||
data-field="gender"
|
||||
className={[
|
||||
"flex h-10 w-full rounded-md border border-input bg-background text-foreground px-3 py-2 text-sm",
|
||||
"ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none",
|
||||
@ -141,9 +152,11 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
aria-invalid={Boolean(getError("gender")) || undefined}
|
||||
>
|
||||
<option value="">Select…</option>
|
||||
<option value="male">Male</option>
|
||||
<option value="female">Female</option>
|
||||
<option value="other">Other</option>
|
||||
{genderOptions.map(option => (
|
||||
<option key={option} value={option}>
|
||||
{formatGender(option)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
</div>
|
||||
@ -156,7 +169,8 @@ export function AccountStep({ form }: AccountStepProps) {
|
||||
onChange={e => setValue("company", e.target.value)}
|
||||
onBlur={() => setTouchedField("company")}
|
||||
placeholder="Company name"
|
||||
autoComplete="organization"
|
||||
autoComplete="section-signup organization"
|
||||
data-field="company"
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
@ -5,21 +5,22 @@
|
||||
* - postcode → postcode
|
||||
* - state → state (prefecture)
|
||||
* - city → city
|
||||
* - address1 → address1 (street/block address)
|
||||
* - address2 → address2 (building/room - optional)
|
||||
* - address1 → address1 (building/room)
|
||||
* - address2 → address2 (street/block)
|
||||
* - country → "JP"
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { useCallback } from "react";
|
||||
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;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
@ -64,9 +65,11 @@ export function AddressStep({ form }: AddressStepProps) {
|
||||
const markTouched = () => setTouchedField("address");
|
||||
|
||||
// Set Japan as default country on mount if empty
|
||||
if (!address.country) {
|
||||
setValue("address", { ...address, country: "JP", countryCode: "JP" });
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!address.country) {
|
||||
setValue("address", { ...address, country: "JP", countryCode: "JP" });
|
||||
}
|
||||
}, [address, setValue]);
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
@ -79,33 +82,42 @@ export function AddressStep({ form }: AddressStepProps) {
|
||||
>
|
||||
<Input
|
||||
name="postal-code"
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={address.postcode}
|
||||
onChange={handlePostcodeChange}
|
||||
onBlur={markTouched}
|
||||
placeholder="100-0001"
|
||||
autoComplete="postal-code"
|
||||
autoComplete="section-signup postal-code"
|
||||
maxLength={8}
|
||||
autoFocus
|
||||
data-field="address.postcode"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Prefecture Selection */}
|
||||
<FormField label="Prefecture" error={getError("state")} required>
|
||||
<select
|
||||
name="address-level1"
|
||||
value={address.state}
|
||||
onChange={e => updateAddress("state", e.target.value)}
|
||||
onBlur={markTouched}
|
||||
className="block w-full h-10 px-3 py-2 border border-input rounded-md shadow-sm bg-background text-foreground text-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||
autoComplete="address-level1"
|
||||
>
|
||||
<option value="">Select prefecture</option>
|
||||
{JAPAN_PREFECTURES.map(p => (
|
||||
<option key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="relative">
|
||||
<select
|
||||
name="address-level1"
|
||||
value={address.state}
|
||||
onChange={e => updateAddress("state", e.target.value)}
|
||||
onBlur={markTouched}
|
||||
className="block w-full h-11 pl-4 pr-10 py-2.5 border border-border rounded-lg appearance-none bg-card text-foreground text-sm shadow-sm focus:outline-none focus:ring-2 focus:ring-ring focus:border-primary transition-colors cursor-pointer"
|
||||
autoComplete="section-signup address-level1"
|
||||
data-field="address.state"
|
||||
>
|
||||
<option value="">Select prefecture</option>
|
||||
{JAPAN_PREFECTURES.map(p => (
|
||||
<option key={p.value} value={p.value}>
|
||||
{p.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 pointer-events-none">
|
||||
<ChevronDown className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
{/* City/Ward */}
|
||||
@ -121,46 +133,50 @@ export function AddressStep({ form }: AddressStepProps) {
|
||||
onChange={e => updateAddress("city", e.target.value)}
|
||||
onBlur={markTouched}
|
||||
placeholder="Shibuya-ku"
|
||||
autoComplete="address-level2"
|
||||
autoComplete="section-signup address-level2"
|
||||
data-field="address.city"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Street Address */}
|
||||
{/* Street / Block (Address 2) */}
|
||||
<FormField
|
||||
label="Street Address"
|
||||
error={getError("address1")}
|
||||
label="Street / Block (Address 2)"
|
||||
error={getError("address2")}
|
||||
required
|
||||
helperText="Block and building number (e.g., 3-8-2 Higashi Azabu)"
|
||||
helperText="e.g., 2-20-9 Wakabayashi"
|
||||
>
|
||||
<Input
|
||||
name="address-line1"
|
||||
value={address.address1}
|
||||
onChange={e => updateAddress("address1", e.target.value)}
|
||||
type="text"
|
||||
value={address.address2}
|
||||
onChange={e => updateAddress("address2", e.target.value)}
|
||||
onBlur={markTouched}
|
||||
placeholder="3-8-2 Higashi Azabu"
|
||||
autoComplete="address-line1"
|
||||
placeholder="2-20-9 Wakabayashi"
|
||||
autoComplete="section-signup address-line1"
|
||||
required
|
||||
data-field="address.address2"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Building & Room (Optional - for apartments) */}
|
||||
{/* Building / Room (Address 1) */}
|
||||
<FormField
|
||||
label="Building Name & Room Number"
|
||||
error={getError("address2")}
|
||||
helperText="e.g., 3F Azabu Maruka Bldg"
|
||||
label="Building / Room (Address 1)"
|
||||
error={getError("address1")}
|
||||
required
|
||||
helperText="e.g., Gramercy 201"
|
||||
>
|
||||
<Input
|
||||
name="address-line2"
|
||||
value={address.address2 ?? ""}
|
||||
onChange={e => updateAddress("address2", e.target.value)}
|
||||
type="text"
|
||||
value={address.address1}
|
||||
onChange={e => updateAddress("address1", e.target.value)}
|
||||
onBlur={markTouched}
|
||||
placeholder="3F Azabu Maruka Bldg"
|
||||
autoComplete="address-line2"
|
||||
placeholder="Gramercy 201"
|
||||
autoComplete="section-signup address-line2"
|
||||
required
|
||||
data-field="address.address1"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<p className="text-sm text-muted-foreground bg-info-soft border border-info/25 rounded-lg p-3">
|
||||
Please input your address in Japan. This will be used for service delivery and setup.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
||||
type="email"
|
||||
name="email"
|
||||
value={values.email}
|
||||
autoComplete="username email"
|
||||
autoComplete="section-signup username"
|
||||
readOnly
|
||||
className="sr-only"
|
||||
tabIndex={-1}
|
||||
@ -53,7 +53,8 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
||||
onChange={e => setValue("password", e.target.value)}
|
||||
onBlur={() => setTouchedField("password")}
|
||||
placeholder="Create a secure password"
|
||||
autoComplete="new-password"
|
||||
autoComplete="section-signup new-password"
|
||||
data-field="password"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
@ -97,7 +98,8 @@ export function PasswordStep({ form }: PasswordStepProps) {
|
||||
onChange={e => setValue("confirmPassword", e.target.value)}
|
||||
onBlur={() => setTouchedField("confirmPassword")}
|
||||
placeholder="Re-enter your password"
|
||||
autoComplete="new-password"
|
||||
autoComplete="section-signup new-password"
|
||||
data-field="confirmPassword"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ interface ReviewStepProps {
|
||||
gender?: "male" | "female" | "other";
|
||||
address: {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
address2: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
@ -95,8 +95,8 @@ export function ReviewStep({ form }: ReviewStepProps) {
|
||||
|
||||
// Format address for display
|
||||
const formattedAddress = [
|
||||
address.address1,
|
||||
address.address2,
|
||||
address.address1,
|
||||
address.city,
|
||||
address.state,
|
||||
address.postcode,
|
||||
|
||||
@ -305,24 +305,7 @@ export function AddressConfirmation({
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Street Address *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editedAddress?.address1 || ""}
|
||||
onChange={e => {
|
||||
setError(null); // Clear error on input
|
||||
setEditedAddress(prev => (prev ? { ...prev, address1: e.target.value } : null));
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||
placeholder="123 Main Street"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Street Address Line 2
|
||||
Street / Block (Address 2)
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
@ -332,7 +315,24 @@ export function AddressConfirmation({
|
||||
setEditedAddress(prev => (prev ? { ...prev, address2: e.target.value } : null));
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||
placeholder="Apartment, suite, etc. (optional)"
|
||||
placeholder="2-20-9 Wakabayashi"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-muted-foreground mb-1">
|
||||
Building / Room (Address 1) *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editedAddress?.address1 || ""}
|
||||
onChange={e => {
|
||||
setError(null); // Clear error on input
|
||||
setEditedAddress(prev => (prev ? { ...prev, address1: e.target.value } : null));
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors"
|
||||
placeholder="Gramercy 201"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -427,9 +427,11 @@ export function AddressConfirmation({
|
||||
{address?.address1 ? (
|
||||
<div className="space-y-4">
|
||||
<div className="text-foreground space-y-1">
|
||||
<p className="font-semibold text-base">{address.address1}</p>
|
||||
{address.address2 ? (
|
||||
<p className="text-muted-foreground">{address.address2}</p>
|
||||
{(address.address2 || address.address1) && (
|
||||
<p className="font-semibold text-base">{address.address2 || address.address1}</p>
|
||||
)}
|
||||
{address.address2 && address.address1 ? (
|
||||
<p className="text-muted-foreground">{address.address1}</p>
|
||||
) : null}
|
||||
<p className="text-muted-foreground">
|
||||
{[address.city, address.state].filter(Boolean).join(", ")}
|
||||
|
||||
@ -45,8 +45,8 @@ const normalizeCountryValue = (value?: string | null) => {
|
||||
};
|
||||
|
||||
const DEFAULT_LABELS: Partial<Record<keyof Address, string>> = {
|
||||
address1: "Address Line 1",
|
||||
address2: "Address Line 2",
|
||||
address1: "Building / Room (Address 1)",
|
||||
address2: "Street / Block (Address 2)",
|
||||
city: "City",
|
||||
state: "State/Prefecture",
|
||||
postcode: "Postcode",
|
||||
@ -57,8 +57,8 @@ const DEFAULT_LABELS: Partial<Record<keyof Address, string>> = {
|
||||
};
|
||||
|
||||
const DEFAULT_PLACEHOLDERS: Partial<Record<keyof Address, string>> = {
|
||||
address1: "123 Main Street",
|
||||
address2: "Apartment, suite, etc. (optional)",
|
||||
address1: "Gramercy 201",
|
||||
address2: "2-20-9 Wakabayashi",
|
||||
city: "Tokyo",
|
||||
state: "Tokyo",
|
||||
postcode: "100-0001",
|
||||
@ -308,12 +308,12 @@ export function AddressForm({
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Street Address */}
|
||||
{renderField("address1")}
|
||||
|
||||
{/* Street Address Line 2 */}
|
||||
{/* Street / Block (Address 2) */}
|
||||
{renderField("address2")}
|
||||
|
||||
{/* Building / Room (Address 1) */}
|
||||
{renderField("address1")}
|
||||
|
||||
{/* City, State, Postal Code Row */}
|
||||
<div
|
||||
className={`grid gap-4 ${variant === "compact" ? "grid-cols-1 sm:grid-cols-3" : "grid-cols-1 md:grid-cols-3"}`}
|
||||
|
||||
105
pnpm-lock.yaml
generated
105
pnpm-lock.yaml
generated
@ -192,9 +192,6 @@ importers:
|
||||
"@heroicons/react":
|
||||
specifier: ^2.2.0
|
||||
version: 2.2.0(react@19.2.3)
|
||||
"@react-google-maps/api":
|
||||
specifier: ^2.20.8
|
||||
version: 2.20.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.90.14
|
||||
version: 5.90.14(react@19.2.3)
|
||||
@ -849,18 +846,6 @@ packages:
|
||||
}
|
||||
engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 }
|
||||
|
||||
"@googlemaps/js-api-loader@1.16.8":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==,
|
||||
}
|
||||
|
||||
"@googlemaps/markerclusterer@2.5.3":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==,
|
||||
}
|
||||
|
||||
"@grpc/grpc-js@1.14.2":
|
||||
resolution:
|
||||
{
|
||||
@ -2078,27 +2063,6 @@ packages:
|
||||
integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==,
|
||||
}
|
||||
|
||||
"@react-google-maps/api@2.20.8":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-wtLYFtCGXK3qbIz1H5to3JxbosPnKsvjDKhqGylXUb859EskhzR7OpuNt0LqdLarXUtZCJTKzPn3BNaekNIahg==,
|
||||
}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19
|
||||
|
||||
"@react-google-maps/infobox@2.20.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-03PJHjohhaVLkX6+NHhlr8CIlvUxWaXhryqDjyaZ8iIqqix/nV8GFdz9O3m5OsjtxtNho09F/15j14yV0nuyLQ==,
|
||||
}
|
||||
|
||||
"@react-google-maps/marker-clusterer@2.20.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-tieX9Va5w1yP88vMgfH1pHTacDQ9TgDTjox3tLlisKDXRQWdjw+QeVVghhf5XqqIxXHgPdcGwBvKY6UP+SIvLw==,
|
||||
}
|
||||
|
||||
"@scarf/scarf@1.4.0":
|
||||
resolution:
|
||||
{
|
||||
@ -2519,12 +2483,6 @@ packages:
|
||||
integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==,
|
||||
}
|
||||
|
||||
"@types/google.maps@3.58.1":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-X9QTSvGJ0nCfMzYOnaVs/k6/4L+7F5uCS+4iUmkLEls6J9S/Phv+m/i3mDeyc49ZBgwab3EFO1HEoBY7k98EGQ==,
|
||||
}
|
||||
|
||||
"@types/http-cache-semantics@4.0.4":
|
||||
resolution:
|
||||
{
|
||||
@ -4803,12 +4761,6 @@ packages:
|
||||
integrity: sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==,
|
||||
}
|
||||
|
||||
invariant@2.2.4:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==,
|
||||
}
|
||||
|
||||
ioredis@5.8.2:
|
||||
resolution:
|
||||
{
|
||||
@ -5052,12 +5004,6 @@ packages:
|
||||
integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==,
|
||||
}
|
||||
|
||||
kdbush@4.0.2:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==,
|
||||
}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution:
|
||||
{
|
||||
@ -5289,13 +5235,6 @@ packages:
|
||||
integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==,
|
||||
}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==,
|
||||
}
|
||||
hasBin: true
|
||||
|
||||
lowercase-keys@3.0.0:
|
||||
resolution:
|
||||
{
|
||||
@ -6915,12 +6854,6 @@ packages:
|
||||
babel-plugin-macros:
|
||||
optional: true
|
||||
|
||||
supercluster@8.0.1:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==,
|
||||
}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution:
|
||||
{
|
||||
@ -7919,13 +7852,6 @@ snapshots:
|
||||
"@eslint/core": 0.17.0
|
||||
levn: 0.4.1
|
||||
|
||||
"@googlemaps/js-api-loader@1.16.8": {}
|
||||
|
||||
"@googlemaps/markerclusterer@2.5.3":
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
supercluster: 8.0.1
|
||||
|
||||
"@grpc/grpc-js@1.14.2":
|
||||
dependencies:
|
||||
"@grpc/proto-loader": 0.8.0
|
||||
@ -8636,21 +8562,6 @@ snapshots:
|
||||
|
||||
"@protobufjs/utf8@1.1.0": {}
|
||||
|
||||
"@react-google-maps/api@2.20.8(react-dom@19.2.3(react@19.2.3))(react@19.2.3)":
|
||||
dependencies:
|
||||
"@googlemaps/js-api-loader": 1.16.8
|
||||
"@googlemaps/markerclusterer": 2.5.3
|
||||
"@react-google-maps/infobox": 2.20.0
|
||||
"@react-google-maps/marker-clusterer": 2.20.0
|
||||
"@types/google.maps": 3.58.1
|
||||
invariant: 2.2.4
|
||||
react: 19.2.3
|
||||
react-dom: 19.2.3(react@19.2.3)
|
||||
|
||||
"@react-google-maps/infobox@2.20.0": {}
|
||||
|
||||
"@react-google-maps/marker-clusterer@2.20.0": {}
|
||||
|
||||
"@scarf/scarf@1.4.0": {}
|
||||
|
||||
"@sendgrid/client@8.1.6":
|
||||
@ -8903,8 +8814,6 @@ snapshots:
|
||||
"@types/express-serve-static-core": 5.1.0
|
||||
"@types/serve-static": 2.2.0
|
||||
|
||||
"@types/google.maps@3.58.1": {}
|
||||
|
||||
"@types/http-cache-semantics@4.0.4":
|
||||
optional: true
|
||||
|
||||
@ -10412,10 +10321,6 @@ snapshots:
|
||||
kind-of: 6.0.3
|
||||
optional: true
|
||||
|
||||
invariant@2.2.4:
|
||||
dependencies:
|
||||
loose-envify: 1.4.0
|
||||
|
||||
ioredis@5.8.2:
|
||||
dependencies:
|
||||
"@ioredis/commands": 1.4.0
|
||||
@ -10541,8 +10446,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
kdbush@4.0.2: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
@ -10663,10 +10566,6 @@ snapshots:
|
||||
|
||||
long@5.3.2: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
dependencies:
|
||||
js-tokens: 4.0.0
|
||||
|
||||
lowercase-keys@3.0.0:
|
||||
optional: true
|
||||
|
||||
@ -11619,10 +11518,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
"@babel/core": 7.28.5
|
||||
|
||||
supercluster@8.0.1:
|
||||
dependencies:
|
||||
kdbush: 4.0.2
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user