+
+ { address.firstName }
+ { address.lastName }
+
+
+ {
+ (address.phone || address.company) &&
+
+ { address.company }
+ { address.phone }
+
+ }
+
+
+
+ { address.address1 }
+ {
+ address.address2 &&
+
+ / { address.address2 }
+
+ }
+
+
+ { address.city },
+ {
+ address.localizedProvince &&
+ { address.localizedProvince },
+ }
+ { address.postalCode } /
+ { address.localizedCountry }
+
+
+
+);
+
+export function mapToStaticAddressProps(
+ context: CheckoutContextProps,
+ { address }: StaticAddressProps
+): WithCheckoutStaticAddressProps | null {
+ const {
+ checkoutState: {
+ data: {
+ getBillingCountries,
+ },
+ },
+ } = context;
+
+ return {
+ localizedAddress: localizeAddress(
+ address,
+ getBillingCountries()
+ ),
+ };
+}
+
+export default withCheckout(mapToStaticAddressProps)(StaticAddress);
diff --git a/src/app/address/staticAddress/__snapshots__/StaticAddress.spec.tsx.snap b/src/app/address/staticAddress/__snapshots__/StaticAddress.spec.tsx.snap
new file mode 100644
index 0000000000..0df03abe98
--- /dev/null
+++ b/src/app/address/staticAddress/__snapshots__/StaticAddress.spec.tsx.snap
@@ -0,0 +1,159 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`StaticAddress Component renders component when props are missing 1`] = `
+
+
+
+ Test
+
+
+
+ Tester
+
+
+
+
+ Bigcommerce
+
+
+
+
+
+
+
+ 12345 Testing Way
+
+
+
+
+
+
+ Some City
+ ,
+
+
+ California
+ ,
+
+
+ 95555
+ /
+
+
+ United States
+
+
+
+
+
+`;
+
+exports[`StaticAddress Component renders component with supplied props 1`] = `
+
+
+
+ Test
+
+
+
+ Tester
+
+
+
+
+ Bigcommerce
+
+
+
+ 555-555-5555
+
+
+
+
+
+ 12345 Testing Way
+
+
+
+
+
+
+ Some City
+ ,
+
+
+ California
+ ,
+
+
+ 95555
+ /
+
+
+ United States
+
+
+
+
+
+`;
diff --git a/src/app/address/util/getFormFieldInputId.ts b/src/app/address/util/getFormFieldInputId.ts
new file mode 100644
index 0000000000..2bedbc09e5
--- /dev/null
+++ b/src/app/address/util/getFormFieldInputId.ts
@@ -0,0 +1,17 @@
+import { AddressKeyMap } from '../DynamicFormField';
+
+export const ADDRESS_FIELD_IDS: AddressKeyMap = {
+ address1: 'addressLine1',
+ address2: 'addressLine2',
+ postalCode: 'postCode',
+ stateOrProvince: 'province',
+ stateOrProvinceCode: 'provinceCode',
+};
+
+export function getFormFieldLegacyName(name: string): string {
+ return `${ADDRESS_FIELD_IDS[name] || name}`;
+}
+
+export function getFormFieldInputId(name: string): string {
+ return `${getFormFieldLegacyName(name)}Input`;
+}
diff --git a/src/app/address/util/isEqualAddress.spec.ts b/src/app/address/util/isEqualAddress.spec.ts
new file mode 100644
index 0000000000..a4bf3447dc
--- /dev/null
+++ b/src/app/address/util/isEqualAddress.spec.ts
@@ -0,0 +1,41 @@
+import { getAddress } from '../address.mock';
+import { getAddressFormFields } from '../formField.mock';
+import mapAddressFromFormValues from '../mapAddressFromFormValues';
+import mapAddressToFormValues from '../mapAddressToFormValues';
+
+import isEqualAddress from './isEqualAddress';
+
+describe('isEqualAddress', () => {
+ it('returns true when ignored values are different', () => {
+ expect(isEqualAddress(getAddress(), {
+ ...getAddress(),
+ stateOrProvinceCode: 'w',
+ id: 'x',
+ email: 'y',
+ type: 'z',
+ })).toBeTruthy();
+ });
+
+ it('returns false when values are different', () => {
+ expect(isEqualAddress(getAddress(), {
+ ...getAddress(),
+ stateOrProvince: 'Foo',
+ })).toBeFalsy();
+ });
+
+ it('returns true for transformed address', () => {
+ const address = getAddress();
+ const transformedAddress = mapAddressFromFormValues(
+ mapAddressToFormValues(getAddressFormFields(), address)
+ );
+
+ expect(isEqualAddress(address, transformedAddress)).toBeTruthy();
+ });
+
+ it('returns true when when custom fields are empty', () => {
+ expect(isEqualAddress(getAddress(), {
+ ...getAddress(),
+ customFields: [{ fieldId: 'foo', fieldValue: '' }],
+ })).toBeTruthy();
+ });
+});
diff --git a/src/app/address/util/isEqualAddress.ts b/src/app/address/util/isEqualAddress.ts
new file mode 100644
index 0000000000..8ba04dff17
--- /dev/null
+++ b/src/app/address/util/isEqualAddress.ts
@@ -0,0 +1,28 @@
+import { Address, AddressRequestBody, BillingAddress, CustomerAddress } from '@bigcommerce/checkout-sdk';
+import { isEqual, omit } from 'lodash';
+
+type ComparableAddress = CustomerAddress | Address | BillingAddress | AddressRequestBody;
+type ComparableAddressFields = keyof CustomerAddress | keyof Address | keyof BillingAddress;
+
+export default function isEqualAddress(address1?: ComparableAddress, address2?: ComparableAddress): boolean {
+ if (!address1 || !address2) {
+ return false;
+ }
+
+ return isEqual(
+ normalizeAddress(address1),
+ normalizeAddress(address2)
+ );
+}
+
+function normalizeAddress(address: ComparableAddress) {
+ const ignoredFields: ComparableAddressFields[] = ['id', 'stateOrProvinceCode', 'type', 'email'];
+
+ return omit(
+ {
+ ...address,
+ customFields: (address.customFields || []).filter(({ fieldValue }) => !!fieldValue),
+ },
+ ignoredFields
+ );
+}
diff --git a/src/app/address/util/isValidAddress.spec.ts b/src/app/address/util/isValidAddress.spec.ts
new file mode 100644
index 0000000000..f13c29ceef
--- /dev/null
+++ b/src/app/address/util/isValidAddress.spec.ts
@@ -0,0 +1,86 @@
+import { getAddress } from '../address.mock';
+import { getFormFields } from '../formField.mock';
+
+import isValidAddress from './isValidAddress';
+
+describe('isValidAddress()', () => {
+ it('returns true if all required fields are defined', () => {
+ expect(isValidAddress(getAddress(), getFormFields()))
+ .toEqual(true);
+ });
+
+ it('returns false if some required fields are not defined', () => {
+ expect(isValidAddress({ ...getAddress(), address1: '' }, getFormFields()))
+ .toEqual(false);
+ });
+
+ describe('when field is dropdown', () => {
+ it('returns false if dropdown is required but not defined', () => {
+ const output = isValidAddress(
+ getAddress(),
+ getFormFields().map(field => (
+ field.fieldType === 'dropdown' ?
+ { ...field, required: true } :
+ field
+ ))
+ );
+
+ expect(output)
+ .toEqual(false);
+ });
+
+ it('returns true if dropdown is required and defined', () => {
+ const output = isValidAddress(
+ {
+ ...getAddress(),
+ customFields: [
+ { fieldId: 'field_27', fieldValue: '0' },
+ ],
+ },
+ getFormFields().map(field => (
+ field.type === 'array' ?
+ { ...field, required: true } :
+ field
+ ))
+ );
+
+ expect(output)
+ .toEqual(true);
+ });
+ });
+
+ describe('when field is number', () => {
+ it('returns true if field is required and defined as 0', () => {
+ const output = isValidAddress(
+ {
+ ...getAddress(),
+ customFields: [
+ { fieldId: 'field_31', fieldValue: 0 },
+ ],
+ },
+ getFormFields().map(field => (
+ field.type === 'integer' ?
+ { ...field, required: true } :
+ field
+ ))
+ );
+
+ expect(output)
+ .toEqual(true);
+ });
+
+ it('returns false if field is required and not defined', () => {
+ const output = isValidAddress(
+ getAddress(),
+ getFormFields().map(field => (
+ (typeof field.type !== undefined && field.type === 'integer') ?
+ { ...field, required: true } :
+ field
+ ))
+ );
+
+ expect(output)
+ .toEqual(false);
+ });
+ });
+});
diff --git a/src/app/address/util/isValidAddress.ts b/src/app/address/util/isValidAddress.ts
new file mode 100644
index 0000000000..2623be0664
--- /dev/null
+++ b/src/app/address/util/isValidAddress.ts
@@ -0,0 +1,10 @@
+import { Address, FormField } from '@bigcommerce/checkout-sdk';
+
+import getAddressValidationSchema from '../getAddressValidationSchema';
+import mapAddressToFormValues from '../mapAddressToFormValues';
+
+export default function isValidAddress(address: Address, formFields: FormField[]): boolean {
+ const addressSchema = getAddressValidationSchema({ formFields });
+
+ return addressSchema.isValidSync(mapAddressToFormValues(formFields, address));
+}
diff --git a/src/app/address/util/isValidCustomerAddress.ts b/src/app/address/util/isValidCustomerAddress.ts
new file mode 100644
index 0000000000..d8ba9a7a30
--- /dev/null
+++ b/src/app/address/util/isValidCustomerAddress.ts
@@ -0,0 +1,17 @@
+import { Address, CustomerAddress, FormField } from '@bigcommerce/checkout-sdk';
+import { some } from 'lodash';
+
+import isEqualAddress from './isEqualAddress';
+import isValidAddress from './isValidAddress';
+
+export default function isValidCustomerAddress(
+ address: Address | undefined,
+ addresses: CustomerAddress[],
+ formFields: FormField[]
+): boolean {
+ if (!address || !isValidAddress(address, formFields)) {
+ return false;
+ }
+
+ return some(addresses, customerAddress => isEqualAddress(customerAddress, address));
+}
diff --git a/src/app/address/util/localizeAddress.spec.ts b/src/app/address/util/localizeAddress.spec.ts
new file mode 100644
index 0000000000..7874abbd25
--- /dev/null
+++ b/src/app/address/util/localizeAddress.spec.ts
@@ -0,0 +1,43 @@
+import { Address } from '@bigcommerce/checkout-sdk';
+
+import { getCountries } from '../../geography/countries.mock';
+import { getAddress } from '../address.mock';
+
+import localizeAddress from './localizeAddress';
+
+describe('localizeAddress', () => {
+ let address: Address;
+
+ beforeEach(() => {
+ address = getAddress();
+ });
+
+ it('localizes address with provided countries', () => {
+ expect(localizeAddress(address, getCountries())).toMatchObject({
+ ...address,
+ localizedCountry: 'United States',
+ localizedProvince: 'California',
+ });
+ });
+
+ it('keeps same value if unable match countryCode', () => {
+ expect(localizeAddress(address, [])).toMatchObject({
+ ...address,
+ localizedCountry: 'United States',
+ localizedProvince: 'California',
+ });
+ });
+
+ it('keeps same value if unable to provinceCode', () => {
+ const countries = getCountries().map(country => ({
+ ...country,
+ subdivisions: [],
+ }));
+
+ expect(localizeAddress(address, countries)).toMatchObject({
+ ...address,
+ localizedCountry: 'United States',
+ localizedProvince: 'California',
+ });
+ });
+});
diff --git a/src/app/address/util/localizeAddress.ts b/src/app/address/util/localizeAddress.ts
new file mode 100644
index 0000000000..53d4953db4
--- /dev/null
+++ b/src/app/address/util/localizeAddress.ts
@@ -0,0 +1,22 @@
+import { Address } from '@bigcommerce/checkout-sdk';
+import { find, isEmpty } from 'lodash';
+
+import { memoize } from '../../common/utility';
+import { Country, LocalizedGeography } from '../../geography';
+
+const localizeAddress =