@nuskin/address-lookup
v1.1.2
Published
React component for address autocomplete and validation using Smarty services
Downloads
1,225
Readme
@nuskin/address-lookup
React address entry component with:
- Smarty autocomplete (US + international)
- optional Smarty verification
- Contentstack-backed form definitions
- required-field and email-format validation state
- localized field labels (Contentstack or parent-provided)
Install
Using npm:
npm add @nuskin/address-lookupUsing yarn:
yarn add @nuskin/address-lookupExports
import AddressLookup, {
normalizeAddressForm,
SUPPORTED_COUNTRIES
} from '@nuskin/address-lookup'AddressLookup(default + named export)normalizeAddressFormandDEFAULT_ADDRESS_FORMfor parent-side form-definition reuseSUPPORTED_COUNTRIESfor the list of supported ISO2 market codes
Basic usage
import { useState } from 'react'
import AddressLookup from '@nuskin/address-lookup'
export default function ShippingAddress() {
const [validationRequestId, setValidationRequestId] = useState(0)
const [latest, setLatest] = useState(null)
const handleSave = () => {
// Triggers child to show validation UI + emit validation-request callback
setValidationRequestId((n) => n + 1)
}
return (
<>
<AddressLookup
country='US'
language='en'
requireValidation={true}
validationRequestId={validationRequestId}
onAddressSelect={setLatest}
/>
<button onClick={handleSave}>Save</button>
<pre>{JSON.stringify(latest, null, 2)}</pre>
</>
)
}Props
AddressLookup props:
onAddressSelect: (payload) => voidcountry?: string(default:'US')language?: string(default:'en')address?: object | nullinitial form values, including optional verification metadatarequireValidation?: boolean(default:true)showManualValidateButton?: boolean(default:false)validationRequestId?: number(default:0)strings?: object | nulloptional label overridesformDefinition?: object | nulloptional normalized form-definition override
strings shape
If passed, these labels are used instead of loading from Contentstack.
The component accepts either:
- a flat normalized label object
- a raw Contentstack-style object with grouped validation strings
Flat normalized example:
{
address1Label: 'Street Address',
address2Label: 'Apartment, suite, etc.',
cityLabel: 'City',
districtLabel: 'District',
regionLabel: 'Region',
postalCodeLabel: 'Postal Code',
invalidEmail: 'Invalid email address.',
fieldRequired: 'This field is required.',
invalidPostalCode: 'Invalid postal code.',
rateLimited: 'Too many requests. Please wait a moment and try again.',
switchToAutocomplete: 'Switch to Autocomplete',
switchToManualEntry: 'Switch to Manual Entry',
addressLocation: 'Address location',
openInMaps: 'Open in Google Maps',
usVerified: 'Verified',
usVerifiedMessage: 'This address is fully verified.',
usNoResults: 'We couldn’t verify this address. Check the address and try again.',
intlVerifiedTitle: 'Verified',
intlVerifiedMessage: 'This address is verified.',
intlNoResults: 'We couldn’t verify this address. Check the address and try again.'
}Raw Contentstack-style grouped example:
{
address1Label: 'Street Address',
address2Label: 'Apartment, suite, etc.',
cityLabel: 'City',
districtLabel: 'District',
regionLabel: 'Region',
postalCodeLabel: 'Postal Code',
invalidPostalCode: 'Invalid postal code.',
fieldRequired: 'This field is required.',
rateLimited: 'Too many requests. Please wait a moment and try again.',
switchToAutocomplete: 'Switch to Autocomplete',
switchToManuelEntry: 'Switch to Manual Entry',
addressLocation: 'Address location',
openInMaps: 'Open in Google Maps',
smartyUsValidationStrings: {
usVerified: 'Verified',
usVerifiedMessage: 'This address is fully verified.',
usMissingSecondaryTitle: 'Apartment or suite needed',
usMissingSecondaryMessage: 'The main address is valid, but a unit number is required.',
usInvalidSecondaryRequiredTitle: 'Apartment or suite not recognized',
usInvalidSecondaryRequiredMessage: 'The street address is valid, but the unit number was not recognized and is required.',
usInvalidSecondaryOptionalTitle: 'Unit not recognized',
usInvalidSecondaryOptionalMessage: 'The main address is valid, but the unit number was not recognized.',
usVacantTitle: 'Verified, but marked vacant',
usVacantMessage: 'This address is valid but marked as vacant.',
usNoStatTitle: 'Verified with caution',
usNoStatMessage: 'This address is valid, but USPS indicates limited delivery status.',
usNotConfirmedTitle: 'Not verified',
usNotConfirmedMessage: 'This address could not be confirmed.',
usNoResults: 'We couldn’t verify this address. Check the address and try again.'
},
smartyIntlValidationStrings: {
intlVerifiedTitle: 'Verified',
intlVerifiedMessage: 'This address is verified.',
intlVerifiedSubBuildingChangedTitle: 'Verified with unit changes',
intlVerifiedSubBuildingChangedMessage: 'This address is verified, but the apartment or suite information was changed.',
intlPartialTitle: 'Partially verified',
intlPartialMessage: 'This address could only be partially verified.',
intlAmbiguousTitle: 'Multiple matches found',
intlAmbiguousMessage: 'Multiple possible matches were found for this address.',
intlNotVerifiedTitle: 'Not verified',
intlNotVerifiedMessage: 'This address could not be verified.',
intlNoResults: 'We couldn’t verify this address. Check the address and try again.'
}
}Those grouped validation strings are normalized internally into the flat label shape used by the component.
For Contentstack-backed root fields, note that switch_to_manuel_entry is normalized to switchToManualEntry in the component label object.
formDefinition shape
If passed, this is used instead of loading a form definition inside AddressLookup.
{
addressElements: [
{
field: 'address1',
width: 'full',
editable: true,
maxLength: 40,
required: true,
apiMappings: {
autocomplete: 'street',
verification: 'formatted.address1'
}
}
],
lookupField: 'address1',
requiredFields: ['address1'],
postalCodeRegex: /^\d{5}$/,
verifyAfterAutocomplete: false,
wrapAddress1ToAddress2: false
}Notes:
apiMappingsis currently used on international forms- valid verification mapping targets include direct Smarty paths like
components.localityand derived targets likeformatted.address1/formatted.address2 - US field mapping remains code-driven
If the parent is already loading form definitions from Contentstack, it can normalize them once and pass them through:
import { normalizeAddressForm } from '@nuskin/address-lookup'Incoming address verification
The optional address prop can include verification metadata:
{
address1: '123 Main St',
city: 'Provo',
region: 'UT',
postalCode: '84604',
verification: {
status: 'verified' | 'autocomplete' | 'unverified',
source: 'smarty-us' | 'smarty-intl',
verifiedAt: '2026-05-01T15:01:07.905Z',
manuallyEdited: true,
editedFields: ['address2']
}
}Behavior:
- incoming
verifiedaddresses are accepted as-is - incoming
autocompleteaddresses are accepted as-is - incoming
unverifiedaddresses are auto-verified once whenrequireValidation=trueand enough address data is present - if that auto-verification returns exactly one strong result, the component applies it automatically without user interaction
- after verification/autocomplete, minor editable-field changes preserve the current verification status and mark the address as manually edited
- major editable-field changes downgrade the address back to
unverified
Major-field rule:
lookupFieldis always majoraddress1,city,region, andpostalCodeare major only when they are required in the resolved form definition- fields like
address2anddistrictremain minor by default
Callback payloads
onAddressSelect(payload) emits different type values:
autocomplete- emitted when a suggestion is selected and mapped into form fields
field-change- emitted on debounced manual edits
validation- emitted when user selects a verification candidate
validation-request- emitted when parent increments
validationRequestId - reports current status only; does not trigger verification API calls
- emitted when parent increments
Common payload shape:
{
type: 'autocomplete' | 'field-change' | 'validation' | 'validation-request',
address: {
...,
verification?: {
status: 'verified' | 'autocomplete' | 'unverified',
source?: string,
verifiedAt?: string,
manuallyEdited?: boolean,
editedFields?: string[]
}
},
field?: string,
value?: string,
verification?: object,
isDeliverable?: boolean,
isValidEmail?: boolean,
status: {
formAvailable: boolean,
verification: 'unverified' | 'autocomplete' | 'verified',
manualChanges?: {
manuallyEdited: true,
editedFields: string[]
},
needsValidation: boolean,
completeness: 'complete' | 'incomplete',
missingFields: string[],
mode: 'autocomplete' | 'manual',
postalCode: {
applicable: boolean,
required: boolean,
value: string,
isValid: boolean | null
},
email: {
applicable: boolean,
required: boolean,
value: string,
isValid: boolean | null
}
}
}For verified US and international validation results, address may also include:
{
latitude: 48.8566,
longitude: 2.3522
}These are returned from Smarty verify response metadata and are included when a validation candidate is selected.
When Google Maps is loaded by the parent app, the component can use these coordinates to show an in-app map modal from the address action row.
Validation behavior
- Required fields come from the resolved form definition
status.formAvailableisfalsewhen no form definition is available for the selected countrystatus.needsValidationistruewhen:requireValidation=true- mode is
manual - address has not been verified yet
- Required errors are shown when:
- field loses focus, or
- parent triggers validation via
validationRequestId
- UI for invalid required/email fields:
- red input border
aria-invalid="true"
- Email format is validated when email has a non-empty value
status.completenessisincompletewhen email format is invalid- Validation messages use localized strings when provided:
fieldRequiredinvalidPostalCodeinvalidEmailrateLimited
- Safe defaults are used when those validation strings are missing
- verification results are cached briefly by exact request key:
- US validation cache: 60 seconds
- intl validation cache: 60 seconds
- failed validation requests are not cached
Autocomplete and manual mode
- Lookup field comes from the resolved form definition
- example: JP can use
postalCode
- example: JP can use
- intl verification and map-triggered verification also use the configured
lookupField, not alwaysaddress1 - Typing in lookup creates a draft
- Clicking outside while suggestions are open cancels draft and restores prior committed values
- Manual mode:
- with
requireValidation=true, user can run verification explicitly - with
requireValidation=false, no verification API call is made - parent
validationRequestIdreturns current status but does not perform verification - optional
showManualValidateButtonrenders a compactValidate Addressbutton next to the lookup field actions
- with
Localization
When strings is not passed, labels load from Contentstack via @nuskin/contentstack-lib and are cached by locale key:
- cache key format:
${language}_${country}
When formDefinition is not passed, the address form definition also loads from Contentstack.
If no form definition is available after loading, the component shows:
No form available for this country: USNo form available for this country: {countryCode}
Styling and Overrides
The component now ships with a shared stylesheet:
Parent applications can skin the component in two ways:
- Override exported class names in parent CSS
- Override CSS variables on the root
.ns-address-lookupelement
Common class names:
.ns-address-lookup.ns-address-lookup__panel.ns-address-lookup__form.ns-address-lookup__field.ns-address-lookup__field--with-error.ns-address-lookup__label-row.ns-address-lookup__label.ns-address-lookup__actions.ns-address-lookup__toggle.ns-address-lookup__input.ns-address-lookup__input--readonly.ns-address-lookup__input--invalid.ns-address-lookup__input--loading.ns-address-lookup__error-text.ns-address-lookup__message.ns-address-lookup__spinner.ns-address-lookup__validation.ns-address-lookup__button.ns-address-lookup__button--compact.ns-address-lookup__list.ns-address-lookup__list--suggestions.ns-address-lookup__list--validation.ns-address-lookup__list--validation-inline.ns-address-lookup__list-item.ns-address-lookup__list-item--divided.ns-address-lookup__list-button.ns-address-lookup__list-title.ns-address-lookup__list-subtitle
Supported CSS variables on .ns-address-lookup:
--nsal-text-color--nsal-muted-text--nsal-link-color--nsal-border-color--nsal-error-color--nsal-surface-color--nsal-disabled-surface--nsal-validation-surface--nsal-validation-border--nsal-shadow--nsal-radius--nsal-field-gap--nsal-field-gutter--nsal-error-gap--nsal-input-padding-y--nsal-input-padding-x
Example:
.checkout-address .ns-address-lookup {
--nsal-border-color: #cbd5e1;
--nsal-error-color: #b91c1c;
--nsal-radius: 8px;
--nsal-field-gutter: 16px;
}
.checkout-address .ns-address-lookup__button {
background: #111827;
}Development
Run local test app:
npm run devRun checks:
npm run lint
npm test -- --runInBand
npm run build