@huckleberry-inc/address 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -0
- package/build/cjs/AddressFormatter.js +98 -0
- package/build/cjs/format.js +53 -0
- package/build/cjs/graphqlQuery.js +76 -0
- package/build/cjs/index.js +23 -0
- package/build/cjs/loader.js +70 -0
- package/build/cjs/node_modules/@shopify/address-consts/build/esm/index.mjs.js +32 -0
- package/build/cjs/packages/address/src/AddressFormatter.js +98 -0
- package/build/cjs/packages/address/src/format.js +53 -0
- package/build/cjs/packages/address/src/graphqlQuery.js +76 -0
- package/build/cjs/packages/address/src/index.js +27 -0
- package/build/cjs/packages/address/src/loader.js +70 -0
- package/build/cjs/packages/address/src/utilities.js +65 -0
- package/build/cjs/utilities.js +65 -0
- package/build/esm/AddressFormatter.mjs +94 -0
- package/build/esm/format.mjs +48 -0
- package/build/esm/graphqlQuery.mjs +72 -0
- package/build/esm/index.mjs +4 -0
- package/build/esm/loader.mjs +64 -0
- package/build/esm/node_modules/@shopify/address-consts/build/esm/index.mjs.mjs +27 -0
- package/build/esm/packages/address/src/AddressFormatter.mjs +94 -0
- package/build/esm/packages/address/src/format.mjs +48 -0
- package/build/esm/packages/address/src/graphqlQuery.mjs +72 -0
- package/build/esm/packages/address/src/index.mjs +4 -0
- package/build/esm/packages/address/src/loader.mjs +64 -0
- package/build/esm/packages/address/src/utilities.mjs +59 -0
- package/build/esm/utilities.mjs +59 -0
- package/build/esnext/AddressFormatter.esnext +94 -0
- package/build/esnext/format.esnext +48 -0
- package/build/esnext/graphqlQuery.esnext +72 -0
- package/build/esnext/index.esnext +4 -0
- package/build/esnext/loader.esnext +64 -0
- package/build/esnext/node_modules/@shopify/address-consts/build/esm/index.mjs.esnext +27 -0
- package/build/esnext/packages/address/src/AddressFormatter.esnext +94 -0
- package/build/esnext/packages/address/src/format.esnext +48 -0
- package/build/esnext/packages/address/src/graphqlQuery.esnext +72 -0
- package/build/esnext/packages/address/src/index.esnext +4 -0
- package/build/esnext/packages/address/src/loader.esnext +64 -0
- package/build/esnext/packages/address/src/utilities.esnext +59 -0
- package/build/esnext/utilities.esnext +59 -0
- package/build/ts/AddressFormatter.d.ts +23 -0
- package/build/ts/AddressFormatter.d.ts.map +1 -0
- package/build/ts/format.d.ts +29 -0
- package/build/ts/format.d.ts.map +1 -0
- package/build/ts/graphqlQuery.d.ts +3 -0
- package/build/ts/graphqlQuery.d.ts.map +1 -0
- package/build/ts/index.d.ts +5 -0
- package/build/ts/index.d.ts.map +1 -0
- package/build/ts/loader.d.ts +11 -0
- package/build/ts/loader.d.ts.map +1 -0
- package/build/ts/utilities.d.ts +8 -0
- package/build/ts/utilities.d.ts.map +1 -0
- package/index.esnext +2 -0
- package/index.js +1 -0
- package/index.mjs +2 -0
- package/package.json +51 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { formatAddress, buildOrderedFields } from './format.mjs';
|
|
2
|
+
import { loadCountry, loadCountries } from './loader.mjs';
|
|
3
|
+
|
|
4
|
+
const ORDERED_COUNTRIES_CACHE = new Map();
|
|
5
|
+
class AddressFormatter {
|
|
6
|
+
/**
|
|
7
|
+
* Useful in tests or any situation where the cache has undesirable
|
|
8
|
+
* side-effects.
|
|
9
|
+
*/
|
|
10
|
+
static resetCache() {
|
|
11
|
+
ORDERED_COUNTRIES_CACHE.clear();
|
|
12
|
+
}
|
|
13
|
+
constructor(locale) {
|
|
14
|
+
this.locale = locale;
|
|
15
|
+
this.locale = locale;
|
|
16
|
+
}
|
|
17
|
+
updateLocale(locale) {
|
|
18
|
+
this.locale = locale;
|
|
19
|
+
}
|
|
20
|
+
async getCountry(countryCode, {
|
|
21
|
+
includeHiddenZones = false
|
|
22
|
+
} = {}) {
|
|
23
|
+
const country = this.loadCountryFromCache(countryCode, includeHiddenZones);
|
|
24
|
+
if (country) return country;
|
|
25
|
+
return loadCountry(this.locale, countryCode, {
|
|
26
|
+
includeHiddenZones
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async getCountries({
|
|
30
|
+
includeHiddenZones = false
|
|
31
|
+
} = {}) {
|
|
32
|
+
const cacheKey = this.cacheKey(this.locale, includeHiddenZones);
|
|
33
|
+
const cachedCountries = ORDERED_COUNTRIES_CACHE.get(cacheKey);
|
|
34
|
+
if (cachedCountries) return cachedCountries;
|
|
35
|
+
const countries = await loadCountries(this.locale, {
|
|
36
|
+
includeHiddenZones
|
|
37
|
+
});
|
|
38
|
+
ORDERED_COUNTRIES_CACHE.set(cacheKey, countries);
|
|
39
|
+
return countries;
|
|
40
|
+
}
|
|
41
|
+
async getZoneName(countryCode, zoneCode) {
|
|
42
|
+
const country = await this.getCountry(countryCode);
|
|
43
|
+
const countryZone = country.zones.find(item => item.code === zoneCode);
|
|
44
|
+
if (!(countryZone !== null && countryZone !== void 0 && countryZone.name)) return undefined;
|
|
45
|
+
return countryZone.name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Returns the address ordered in an array based based on the country code
|
|
49
|
+
* Eg.:
|
|
50
|
+
* [
|
|
51
|
+
* 'Shopify',
|
|
52
|
+
* 'First Name Last Name',
|
|
53
|
+
* 'Address 1',
|
|
54
|
+
* 'address2',
|
|
55
|
+
* 'Montréal',
|
|
56
|
+
* 'Canada Quebec H2J 4B7',
|
|
57
|
+
* '514 444 3333'
|
|
58
|
+
* ]
|
|
59
|
+
*/
|
|
60
|
+
async format(address) {
|
|
61
|
+
const country = await this.getCountry(address.country);
|
|
62
|
+
return formatAddress(address, country);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Returns an array that shows how to order fields based on the country code
|
|
66
|
+
* Eg.:
|
|
67
|
+
* [
|
|
68
|
+
* ['company'],
|
|
69
|
+
* ['firstName', 'lastName'],
|
|
70
|
+
* ['address1'],
|
|
71
|
+
* ['address2'],
|
|
72
|
+
* ['city'],
|
|
73
|
+
* ['country', 'province', 'zip'],
|
|
74
|
+
* ['phone']
|
|
75
|
+
* ]
|
|
76
|
+
*/
|
|
77
|
+
async getOrderedFields(countryCode) {
|
|
78
|
+
const country = await this.getCountry(countryCode);
|
|
79
|
+
return buildOrderedFields(country);
|
|
80
|
+
}
|
|
81
|
+
cacheKey(locale, includeHiddenZones) {
|
|
82
|
+
/* Cache list of countries per locale, both with and without hidden zones included */
|
|
83
|
+
return `${locale}-${includeHiddenZones}`;
|
|
84
|
+
}
|
|
85
|
+
loadCountryFromCache(countryCode, includeHiddenZones) {
|
|
86
|
+
const cachedCountries = ORDERED_COUNTRIES_CACHE.get(this.cacheKey(this.locale, includeHiddenZones));
|
|
87
|
+
if (!cachedCountries) return null;
|
|
88
|
+
return cachedCountries.find(({
|
|
89
|
+
code
|
|
90
|
+
}) => code === countryCode);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { AddressFormatter as default };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { renderLineTemplate, FIELD_REGEXP, FIELDS_MAPPING } from './utilities.mjs';
|
|
2
|
+
|
|
3
|
+
const LINE_DELIMITER = '_';
|
|
4
|
+
const DEFAULT_FORM_LAYOUT = '{firstName}{lastName}_{company}_{address1}_{address2}_{city}_{country}{province}{zip}_{phone}';
|
|
5
|
+
const DEFAULT_SHOW_LAYOUT = '{lastName} {firstName}_{company}_{address1} {address2}_{city} {province} {zip}_{country}_{phone}';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* When it's time to render any address, use this function so that it's properly
|
|
9
|
+
* formatted for the country's locale.
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* ['Shopify', 'Lindenstraße 9-14', '10969 Berlin', 'Germany'];
|
|
13
|
+
* ```
|
|
14
|
+
* @returns all lines of a formatted address as an array of strings.
|
|
15
|
+
*/
|
|
16
|
+
function formatAddress(address, country) {
|
|
17
|
+
const layout = country.formatting.show || DEFAULT_SHOW_LAYOUT;
|
|
18
|
+
return layout.split(LINE_DELIMITER).map(lineTemplate => renderLineTemplate(country, lineTemplate, address).trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In an edit form, this function can be used to properly order all the input
|
|
23
|
+
* fields.
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* [
|
|
27
|
+
* ['firstName', 'lastName'],
|
|
28
|
+
* ['company'],
|
|
29
|
+
* ['address1'],
|
|
30
|
+
* ['address2'],
|
|
31
|
+
* ['city'],
|
|
32
|
+
* ['country', 'province', 'zip'],
|
|
33
|
+
* ['phone'],
|
|
34
|
+
* ];
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function buildOrderedFields(country) {
|
|
38
|
+
const format = country ? country.formatting.edit : DEFAULT_FORM_LAYOUT;
|
|
39
|
+
return format.split(LINE_DELIMITER).map(lineTemplate => {
|
|
40
|
+
const result = lineTemplate.match(FIELD_REGEXP);
|
|
41
|
+
if (!result) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return result.map(field => FIELDS_MAPPING[field]);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { buildOrderedFields, formatAddress };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const query = `
|
|
2
|
+
query getCountries($locale: SupportedLocale!) {
|
|
3
|
+
countries(locale: $locale) {
|
|
4
|
+
name
|
|
5
|
+
code
|
|
6
|
+
continent
|
|
7
|
+
phoneNumberPrefix
|
|
8
|
+
autocompletionField
|
|
9
|
+
provinceKey
|
|
10
|
+
labels {
|
|
11
|
+
address1
|
|
12
|
+
address2
|
|
13
|
+
city
|
|
14
|
+
company
|
|
15
|
+
country
|
|
16
|
+
firstName
|
|
17
|
+
lastName
|
|
18
|
+
phone
|
|
19
|
+
postalCode
|
|
20
|
+
zone
|
|
21
|
+
}
|
|
22
|
+
optionalLabels {
|
|
23
|
+
address2
|
|
24
|
+
}
|
|
25
|
+
formatting {
|
|
26
|
+
edit
|
|
27
|
+
show
|
|
28
|
+
}
|
|
29
|
+
zones {
|
|
30
|
+
name
|
|
31
|
+
code
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
query getCountry($countryCode: SupportedCountry!, $locale: SupportedLocale!) {
|
|
37
|
+
country(countryCode: $countryCode, locale: $locale) {
|
|
38
|
+
name
|
|
39
|
+
code
|
|
40
|
+
continent
|
|
41
|
+
phoneNumberPrefix
|
|
42
|
+
autocompletionField
|
|
43
|
+
provinceKey
|
|
44
|
+
labels {
|
|
45
|
+
address1
|
|
46
|
+
address2
|
|
47
|
+
city
|
|
48
|
+
company
|
|
49
|
+
country
|
|
50
|
+
firstName
|
|
51
|
+
lastName
|
|
52
|
+
phone
|
|
53
|
+
postalCode
|
|
54
|
+
zone
|
|
55
|
+
}
|
|
56
|
+
optionalLabels {
|
|
57
|
+
address2
|
|
58
|
+
}
|
|
59
|
+
formatting {
|
|
60
|
+
edit
|
|
61
|
+
show
|
|
62
|
+
}
|
|
63
|
+
zones {
|
|
64
|
+
name
|
|
65
|
+
code
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
var query$1 = query;
|
|
71
|
+
|
|
72
|
+
export { query$1 as default };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CountryLoaderError, loadCountries, loadCountry } from './loader.mjs';
|
|
2
|
+
export { buildOrderedFields, formatAddress } from './format.mjs';
|
|
3
|
+
export { default } from './AddressFormatter.mjs';
|
|
4
|
+
export { FieldName, GRAPHQL_ENDPOINT, GraphqlOperationName, HEADERS } from '../../../node_modules/@shopify/address-consts/build/esm/index.mjs.mjs';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import query from './graphqlQuery.mjs';
|
|
2
|
+
import { GRAPHQL_ENDPOINT, HEADERS, GraphqlOperationName } from '../../../node_modules/@shopify/address-consts/build/esm/index.mjs.mjs';
|
|
3
|
+
|
|
4
|
+
const loadCountries = memoizeAsync(async (locale, {
|
|
5
|
+
includeHiddenZones = false
|
|
6
|
+
} = {}) => {
|
|
7
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: HEADERS,
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
query,
|
|
12
|
+
operationName: GraphqlOperationName.Countries,
|
|
13
|
+
variables: {
|
|
14
|
+
locale: locale.replace(/-/, '_').toUpperCase(),
|
|
15
|
+
includeHiddenZones
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
const countries = await response.json();
|
|
20
|
+
if (!('data' in countries) && 'errors' in countries) {
|
|
21
|
+
throw new CountryLoaderError(countries);
|
|
22
|
+
}
|
|
23
|
+
return countries.data.countries;
|
|
24
|
+
});
|
|
25
|
+
const loadCountry = memoizeAsync(async (locale, countryCode, {
|
|
26
|
+
includeHiddenZones = false
|
|
27
|
+
} = {}) => {
|
|
28
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: HEADERS,
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
query,
|
|
33
|
+
operationName: GraphqlOperationName.Country,
|
|
34
|
+
variables: {
|
|
35
|
+
countryCode,
|
|
36
|
+
locale: locale.replace(/-/, '_').toUpperCase(),
|
|
37
|
+
includeHiddenZones
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
const country = await response.json();
|
|
42
|
+
if (!('data' in country) && 'errors' in country) {
|
|
43
|
+
throw new CountryLoaderError(country);
|
|
44
|
+
}
|
|
45
|
+
return country.data.country;
|
|
46
|
+
});
|
|
47
|
+
class CountryLoaderError extends Error {
|
|
48
|
+
constructor(errors) {
|
|
49
|
+
const errorMessage = errors.errors.map(error => error.message).join('; ');
|
|
50
|
+
super(errorMessage);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function memoizeAsync(asyncFunction) {
|
|
54
|
+
const cache = {};
|
|
55
|
+
return (...args) => {
|
|
56
|
+
const stringifiedArgs = JSON.stringify(args);
|
|
57
|
+
if (!cache[stringifiedArgs]) {
|
|
58
|
+
cache[stringifiedArgs] = asyncFunction.apply(this, args);
|
|
59
|
+
}
|
|
60
|
+
return cache[stringifiedArgs];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { CountryLoaderError, loadCountries, loadCountry };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FieldName } from '../../../node_modules/@shopify/address-consts/build/esm/index.mjs.mjs';
|
|
2
|
+
|
|
3
|
+
const FIELD_REGEXP = /({\w+})/g;
|
|
4
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
5
|
+
const FIELDS_MAPPING = {
|
|
6
|
+
'{firstName}': FieldName.FirstName,
|
|
7
|
+
'{lastName}': FieldName.LastName,
|
|
8
|
+
'{country}': FieldName.Country,
|
|
9
|
+
'{city}': FieldName.City,
|
|
10
|
+
'{zip}': FieldName.PostalCode,
|
|
11
|
+
'{province}': FieldName.Zone,
|
|
12
|
+
'{address1}': FieldName.Address1,
|
|
13
|
+
'{address2}': FieldName.Address2,
|
|
14
|
+
'{phone}': FieldName.Phone,
|
|
15
|
+
'{company}': FieldName.Company
|
|
16
|
+
};
|
|
17
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Returns empty string if all replacement fields are empty.
|
|
21
|
+
*/
|
|
22
|
+
function renderLineTemplate(country, template, address) {
|
|
23
|
+
const result = template.match(FIELD_REGEXP);
|
|
24
|
+
let line = template;
|
|
25
|
+
if (!result) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
let lineIsEmpty = true;
|
|
29
|
+
result.forEach(key => {
|
|
30
|
+
const addressKey = key.replace('{', '').replace('}', '');
|
|
31
|
+
if (address[addressKey]) {
|
|
32
|
+
lineIsEmpty = false;
|
|
33
|
+
}
|
|
34
|
+
switch (addressKey) {
|
|
35
|
+
case FieldName.Country:
|
|
36
|
+
line = line.replace(`{${FieldName.Country}}`, country.name);
|
|
37
|
+
break;
|
|
38
|
+
case FieldName.Zone:
|
|
39
|
+
line = line.replace(`{${FieldName.Zone}}`, address.province ? getZone(country.zones, address.province).name : '');
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
line = line.replace(key, address[addressKey] || '');
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (lineIsEmpty) {
|
|
47
|
+
return '';
|
|
48
|
+
} else {
|
|
49
|
+
return line.trim().replace(' ', ' ');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getZone(zones, zoneCode) {
|
|
53
|
+
return zones.find(zone => zone.code === zoneCode) || {
|
|
54
|
+
name: '',
|
|
55
|
+
code: ''
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { FIELDS_MAPPING, FIELD_REGEXP, renderLineTemplate };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { FieldName } from '@huckleberry-inc/address-consts';
|
|
2
|
+
|
|
3
|
+
const FIELD_REGEXP = /({\w+})/g;
|
|
4
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
5
|
+
const FIELDS_MAPPING = {
|
|
6
|
+
'{firstName}': FieldName.FirstName,
|
|
7
|
+
'{lastName}': FieldName.LastName,
|
|
8
|
+
'{country}': FieldName.Country,
|
|
9
|
+
'{city}': FieldName.City,
|
|
10
|
+
'{zip}': FieldName.PostalCode,
|
|
11
|
+
'{province}': FieldName.Zone,
|
|
12
|
+
'{address1}': FieldName.Address1,
|
|
13
|
+
'{address2}': FieldName.Address2,
|
|
14
|
+
'{phone}': FieldName.Phone,
|
|
15
|
+
'{company}': FieldName.Company
|
|
16
|
+
};
|
|
17
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Returns empty string if all replacement fields are empty.
|
|
21
|
+
*/
|
|
22
|
+
function renderLineTemplate(country, template, address) {
|
|
23
|
+
const result = template.match(FIELD_REGEXP);
|
|
24
|
+
let line = template;
|
|
25
|
+
if (!result) {
|
|
26
|
+
return '';
|
|
27
|
+
}
|
|
28
|
+
let lineIsEmpty = true;
|
|
29
|
+
result.forEach(key => {
|
|
30
|
+
const addressKey = key.replace('{', '').replace('}', '');
|
|
31
|
+
if (address[addressKey]) {
|
|
32
|
+
lineIsEmpty = false;
|
|
33
|
+
}
|
|
34
|
+
switch (addressKey) {
|
|
35
|
+
case FieldName.Country:
|
|
36
|
+
line = line.replace(`{${FieldName.Country}}`, country.name);
|
|
37
|
+
break;
|
|
38
|
+
case FieldName.Zone:
|
|
39
|
+
line = line.replace(`{${FieldName.Zone}}`, address.province ? getZone(country.zones, address.province).name : '');
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
line = line.replace(key, address[addressKey] || '');
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
if (lineIsEmpty) {
|
|
47
|
+
return '';
|
|
48
|
+
} else {
|
|
49
|
+
return line.trim().replace(' ', ' ');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function getZone(zones, zoneCode) {
|
|
53
|
+
return zones.find(zone => zone.code === zoneCode) || {
|
|
54
|
+
name: '',
|
|
55
|
+
code: ''
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { FIELDS_MAPPING, FIELD_REGEXP, renderLineTemplate };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { formatAddress, buildOrderedFields } from './format.esnext';
|
|
2
|
+
import { loadCountry, loadCountries } from './loader.esnext';
|
|
3
|
+
|
|
4
|
+
const ORDERED_COUNTRIES_CACHE = new Map();
|
|
5
|
+
class AddressFormatter {
|
|
6
|
+
/**
|
|
7
|
+
* Useful in tests or any situation where the cache has undesirable
|
|
8
|
+
* side-effects.
|
|
9
|
+
*/
|
|
10
|
+
static resetCache() {
|
|
11
|
+
ORDERED_COUNTRIES_CACHE.clear();
|
|
12
|
+
}
|
|
13
|
+
constructor(locale) {
|
|
14
|
+
this.locale = locale;
|
|
15
|
+
this.locale = locale;
|
|
16
|
+
}
|
|
17
|
+
updateLocale(locale) {
|
|
18
|
+
this.locale = locale;
|
|
19
|
+
}
|
|
20
|
+
async getCountry(countryCode, {
|
|
21
|
+
includeHiddenZones = false
|
|
22
|
+
} = {}) {
|
|
23
|
+
const country = this.loadCountryFromCache(countryCode, includeHiddenZones);
|
|
24
|
+
if (country) return country;
|
|
25
|
+
return loadCountry(this.locale, countryCode, {
|
|
26
|
+
includeHiddenZones
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async getCountries({
|
|
30
|
+
includeHiddenZones = false
|
|
31
|
+
} = {}) {
|
|
32
|
+
const cacheKey = this.cacheKey(this.locale, includeHiddenZones);
|
|
33
|
+
const cachedCountries = ORDERED_COUNTRIES_CACHE.get(cacheKey);
|
|
34
|
+
if (cachedCountries) return cachedCountries;
|
|
35
|
+
const countries = await loadCountries(this.locale, {
|
|
36
|
+
includeHiddenZones
|
|
37
|
+
});
|
|
38
|
+
ORDERED_COUNTRIES_CACHE.set(cacheKey, countries);
|
|
39
|
+
return countries;
|
|
40
|
+
}
|
|
41
|
+
async getZoneName(countryCode, zoneCode) {
|
|
42
|
+
const country = await this.getCountry(countryCode);
|
|
43
|
+
const countryZone = country.zones.find(item => item.code === zoneCode);
|
|
44
|
+
if (!(countryZone !== null && countryZone !== void 0 && countryZone.name)) return undefined;
|
|
45
|
+
return countryZone.name;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* Returns the address ordered in an array based based on the country code
|
|
49
|
+
* Eg.:
|
|
50
|
+
* [
|
|
51
|
+
* 'Shopify',
|
|
52
|
+
* 'First Name Last Name',
|
|
53
|
+
* 'Address 1',
|
|
54
|
+
* 'address2',
|
|
55
|
+
* 'Montréal',
|
|
56
|
+
* 'Canada Quebec H2J 4B7',
|
|
57
|
+
* '514 444 3333'
|
|
58
|
+
* ]
|
|
59
|
+
*/
|
|
60
|
+
async format(address) {
|
|
61
|
+
const country = await this.getCountry(address.country);
|
|
62
|
+
return formatAddress(address, country);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Returns an array that shows how to order fields based on the country code
|
|
66
|
+
* Eg.:
|
|
67
|
+
* [
|
|
68
|
+
* ['company'],
|
|
69
|
+
* ['firstName', 'lastName'],
|
|
70
|
+
* ['address1'],
|
|
71
|
+
* ['address2'],
|
|
72
|
+
* ['city'],
|
|
73
|
+
* ['country', 'province', 'zip'],
|
|
74
|
+
* ['phone']
|
|
75
|
+
* ]
|
|
76
|
+
*/
|
|
77
|
+
async getOrderedFields(countryCode) {
|
|
78
|
+
const country = await this.getCountry(countryCode);
|
|
79
|
+
return buildOrderedFields(country);
|
|
80
|
+
}
|
|
81
|
+
cacheKey(locale, includeHiddenZones) {
|
|
82
|
+
/* Cache list of countries per locale, both with and without hidden zones included */
|
|
83
|
+
return `${locale}-${includeHiddenZones}`;
|
|
84
|
+
}
|
|
85
|
+
loadCountryFromCache(countryCode, includeHiddenZones) {
|
|
86
|
+
const cachedCountries = ORDERED_COUNTRIES_CACHE.get(this.cacheKey(this.locale, includeHiddenZones));
|
|
87
|
+
if (!cachedCountries) return null;
|
|
88
|
+
return cachedCountries.find(({
|
|
89
|
+
code
|
|
90
|
+
}) => code === countryCode);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { AddressFormatter as default };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { renderLineTemplate, FIELD_REGEXP, FIELDS_MAPPING } from './utilities.esnext';
|
|
2
|
+
|
|
3
|
+
const LINE_DELIMITER = '_';
|
|
4
|
+
const DEFAULT_FORM_LAYOUT = '{firstName}{lastName}_{company}_{address1}_{address2}_{city}_{country}{province}{zip}_{phone}';
|
|
5
|
+
const DEFAULT_SHOW_LAYOUT = '{lastName} {firstName}_{company}_{address1} {address2}_{city} {province} {zip}_{country}_{phone}';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* When it's time to render any address, use this function so that it's properly
|
|
9
|
+
* formatted for the country's locale.
|
|
10
|
+
*
|
|
11
|
+
* ```typescript
|
|
12
|
+
* ['Shopify', 'Lindenstraße 9-14', '10969 Berlin', 'Germany'];
|
|
13
|
+
* ```
|
|
14
|
+
* @returns all lines of a formatted address as an array of strings.
|
|
15
|
+
*/
|
|
16
|
+
function formatAddress(address, country) {
|
|
17
|
+
const layout = country.formatting.show || DEFAULT_SHOW_LAYOUT;
|
|
18
|
+
return layout.split(LINE_DELIMITER).map(lineTemplate => renderLineTemplate(country, lineTemplate, address).trim());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In an edit form, this function can be used to properly order all the input
|
|
23
|
+
* fields.
|
|
24
|
+
*
|
|
25
|
+
* ```typescript
|
|
26
|
+
* [
|
|
27
|
+
* ['firstName', 'lastName'],
|
|
28
|
+
* ['company'],
|
|
29
|
+
* ['address1'],
|
|
30
|
+
* ['address2'],
|
|
31
|
+
* ['city'],
|
|
32
|
+
* ['country', 'province', 'zip'],
|
|
33
|
+
* ['phone'],
|
|
34
|
+
* ];
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function buildOrderedFields(country) {
|
|
38
|
+
const format = country ? country.formatting.edit : DEFAULT_FORM_LAYOUT;
|
|
39
|
+
return format.split(LINE_DELIMITER).map(lineTemplate => {
|
|
40
|
+
const result = lineTemplate.match(FIELD_REGEXP);
|
|
41
|
+
if (!result) {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
return result.map(field => FIELDS_MAPPING[field]);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { buildOrderedFields, formatAddress };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const query = `
|
|
2
|
+
query countries($locale: SupportedLocale!) {
|
|
3
|
+
getCountries(locale: $locale) {
|
|
4
|
+
name
|
|
5
|
+
code
|
|
6
|
+
continent
|
|
7
|
+
phoneNumberPrefix
|
|
8
|
+
autocompletionField
|
|
9
|
+
provinceKey
|
|
10
|
+
labels {
|
|
11
|
+
address1
|
|
12
|
+
address2
|
|
13
|
+
city
|
|
14
|
+
company
|
|
15
|
+
country
|
|
16
|
+
firstName
|
|
17
|
+
lastName
|
|
18
|
+
phone
|
|
19
|
+
postalCode
|
|
20
|
+
zone
|
|
21
|
+
}
|
|
22
|
+
optionalLabels {
|
|
23
|
+
address2
|
|
24
|
+
}
|
|
25
|
+
formatting {
|
|
26
|
+
edit
|
|
27
|
+
show
|
|
28
|
+
}
|
|
29
|
+
zones {
|
|
30
|
+
name
|
|
31
|
+
code
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
query country($countryCode: SupportedCountry!, $locale: SupportedLocale!) {
|
|
37
|
+
getCountry(countryCode: $countryCode, locale: $locale) {
|
|
38
|
+
name
|
|
39
|
+
code
|
|
40
|
+
continent
|
|
41
|
+
phoneNumberPrefix
|
|
42
|
+
autocompletionField
|
|
43
|
+
provinceKey
|
|
44
|
+
labels {
|
|
45
|
+
address1
|
|
46
|
+
address2
|
|
47
|
+
city
|
|
48
|
+
company
|
|
49
|
+
country
|
|
50
|
+
firstName
|
|
51
|
+
lastName
|
|
52
|
+
phone
|
|
53
|
+
postalCode
|
|
54
|
+
zone
|
|
55
|
+
}
|
|
56
|
+
optionalLabels {
|
|
57
|
+
address2
|
|
58
|
+
}
|
|
59
|
+
formatting {
|
|
60
|
+
edit
|
|
61
|
+
show
|
|
62
|
+
}
|
|
63
|
+
zones {
|
|
64
|
+
name
|
|
65
|
+
code
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
`;
|
|
70
|
+
var query$1 = query;
|
|
71
|
+
|
|
72
|
+
export { query$1 as default };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { GRAPHQL_ENDPOINT, HEADERS, GraphqlOperationName } from '@huckleberry-inc/address-consts';
|
|
2
|
+
import query from './graphqlQuery.esnext';
|
|
3
|
+
|
|
4
|
+
const loadCountries = memoizeAsync(async (locale, {
|
|
5
|
+
includeHiddenZones = false
|
|
6
|
+
} = {}) => {
|
|
7
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: HEADERS,
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
query,
|
|
12
|
+
operationName: GraphqlOperationName.Countries,
|
|
13
|
+
variables: {
|
|
14
|
+
locale: locale.replace(/-/, '_').toUpperCase(),
|
|
15
|
+
includeHiddenZones
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
});
|
|
19
|
+
const countries = await response.json();
|
|
20
|
+
if (!('data' in countries) && 'errors' in countries) {
|
|
21
|
+
throw new CountryLoaderError(countries);
|
|
22
|
+
}
|
|
23
|
+
return countries.data.countries;
|
|
24
|
+
});
|
|
25
|
+
const loadCountry = memoizeAsync(async (locale, countryCode, {
|
|
26
|
+
includeHiddenZones = false
|
|
27
|
+
} = {}) => {
|
|
28
|
+
const response = await fetch(GRAPHQL_ENDPOINT, {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: HEADERS,
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
query,
|
|
33
|
+
operationName: GraphqlOperationName.Country,
|
|
34
|
+
variables: {
|
|
35
|
+
countryCode,
|
|
36
|
+
locale: locale.replace(/-/, '_').toUpperCase(),
|
|
37
|
+
includeHiddenZones
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
});
|
|
41
|
+
const country = await response.json();
|
|
42
|
+
if (!('data' in country) && 'errors' in country) {
|
|
43
|
+
throw new CountryLoaderError(country);
|
|
44
|
+
}
|
|
45
|
+
return country.data.country;
|
|
46
|
+
});
|
|
47
|
+
class CountryLoaderError extends Error {
|
|
48
|
+
constructor(errors) {
|
|
49
|
+
const errorMessage = errors.errors.map(error => error.message).join('; ');
|
|
50
|
+
super(errorMessage);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function memoizeAsync(asyncFunction) {
|
|
54
|
+
const cache = {};
|
|
55
|
+
return (...args) => {
|
|
56
|
+
const stringifiedArgs = JSON.stringify(args);
|
|
57
|
+
if (!cache[stringifiedArgs]) {
|
|
58
|
+
cache[stringifiedArgs] = asyncFunction.apply(this, args);
|
|
59
|
+
}
|
|
60
|
+
return cache[stringifiedArgs];
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { CountryLoaderError, loadCountries, loadCountry };
|