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:
barsa 2026-01-06 16:13:59 +09:00
parent cc0200bd5e
commit 1a3032bbd3
15 changed files with 290 additions and 280 deletions

View File

@ -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;
};
}

View File

@ -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 {

View File

@ -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 = {

View File

@ -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,

View File

@ -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(", ")}

View File

@ -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]

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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,

View File

@ -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(", ")}

View File

@ -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
View File

@ -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