@ikeboy003/address 0.1.1
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 +42 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/loader.d.ts +11 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +77 -0
- package/dist/loader.js.map +1 -0
- package/dist/parse.d.ts +8 -0
- package/dist/parse.d.ts.map +1 -0
- package/dist/parse.js +40 -0
- package/dist/parse.js.map +1 -0
- package/dist/react.d.ts +45 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +87 -0
- package/dist/react.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/package.json +56 -0
- package/src/index.ts +17 -0
- package/src/loader.ts +79 -0
- package/src/parse.ts +47 -0
- package/src/react.tsx +151 -0
- package/src/types.ts +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @ikeboy003/address
|
|
2
|
+
|
|
3
|
+
Headless Google Places address autocomplete. One public, referrer-restricted browser
|
|
4
|
+
key serves every site — no backend, no D1 cache (the JS widget is session-billed in
|
|
5
|
+
the browser and Google's ToS forbids storing predictions).
|
|
6
|
+
|
|
7
|
+
Over a raw autocomplete it returns **structured, ShipTo-shaped parts** so a checkout
|
|
8
|
+
form auto-fills street/city/state/zip instead of just a display string.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
Set `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY` (or `configureAddress({ apiKey })`). Restrict the
|
|
13
|
+
key to your domains + the Maps JavaScript API + Places library in the Google console.
|
|
14
|
+
|
|
15
|
+
## Hook (headless — you own the input)
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { useAddressAutocomplete } from "@ikeboy003/address/react";
|
|
19
|
+
|
|
20
|
+
const { inputRef, error } = useAddressAutocomplete({
|
|
21
|
+
onSelect: (r) => setShip((s) => ({ ...s, ...r.parts })), // parts ⊂ ShipTo
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
<input ref={inputRef} placeholder="Start typing your address…" className={inputCls} />
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Component
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { AddressAutocomplete } from "@ikeboy003/address/react";
|
|
31
|
+
|
|
32
|
+
<AddressAutocomplete
|
|
33
|
+
className={inputCls}
|
|
34
|
+
placeholder="Street address"
|
|
35
|
+
onSelect={(r) => setShip((s) => ({ ...s, ...r.parts }))}
|
|
36
|
+
onRawChange={(v) => setStreet1(v)}
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`AddressResult`: `{ formatted, parts: { street1, street2, city, state, postal, country }, placeId }`.
|
|
41
|
+
|
|
42
|
+
`placeId` is the only Google field allowed to be persisted long-term.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,YAAY,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC1E,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @ikeboy003/address — headless Google Places address autocomplete.
|
|
2
|
+
//
|
|
3
|
+
// Core entry (framework-agnostic): the script loader, the place parser, and
|
|
4
|
+
// import-time config. The React binding (`useAddressAutocomplete`, `<AddressAutocomplete>`)
|
|
5
|
+
// lives in `@ikeboy003/address/react`.
|
|
6
|
+
//
|
|
7
|
+
// import { configureAddress } from "@ikeboy003/address"; // optional
|
|
8
|
+
// import { useAddressAutocomplete } from "@ikeboy003/address/react";
|
|
9
|
+
//
|
|
10
|
+
// One PUBLIC, referrer-restricted browser key serves every site — restrict it to
|
|
11
|
+
// your domains in the Google console. No backend, no cache: the JS widget is
|
|
12
|
+
// session-billed in the browser and Google's ToS forbids storing predictions.
|
|
13
|
+
export { configureAddress, loadPlaces, addressCountry } from "./loader";
|
|
14
|
+
export { parsePlace } from "./parse";
|
|
15
|
+
export { EMPTY_PARTS } from "./types";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,EAAE;AACF,4EAA4E;AAC5E,4FAA4F;AAC5F,uCAAuC;AACvC,EAAE;AACF,2EAA2E;AAC3E,uEAAuE;AACvE,EAAE;AACF,iFAAiF;AACjF,6EAA6E;AAC7E,8EAA8E;AAE9E,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAErC,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC"}
|
package/dist/loader.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AddressConfig } from "./types";
|
|
2
|
+
/** Override config at import time. Unset fields keep their defaults / env fallback. */
|
|
3
|
+
export declare function configureAddress(next: AddressConfig): void;
|
|
4
|
+
export declare function addressCountry(): string | string[] | null;
|
|
5
|
+
/**
|
|
6
|
+
* Load `google.maps.places` once. Resolves with `google.maps`. Rejects if no key
|
|
7
|
+
* is configured or the script fails. Safe to call repeatedly — one script, one
|
|
8
|
+
* promise. Pass `apiKey` to override the configured/env key for this page.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loadPlaces(apiKey?: string): Promise<typeof google.maps>;
|
|
11
|
+
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAQ7C,uFAAuF;AACvF,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAM1D;AAYD,wBAAgB,cAAc,IAAI,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAEzD;AAID;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,MAAM,CAAC,IAAI,CAAC,CAmCvE"}
|
package/dist/loader.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Framework-agnostic loader for the Google Maps JS API (places library). Loads the
|
|
2
|
+
// script exactly once per page and de-dupes concurrent callers behind one promise —
|
|
3
|
+
// three checkout forms mounting at once still yield a single <script>. Browser-only;
|
|
4
|
+
// on the server `load()` returns a never-resolving promise (guarded by callers).
|
|
5
|
+
let config = {
|
|
6
|
+
apiKey: "",
|
|
7
|
+
country: "us",
|
|
8
|
+
version: "weekly",
|
|
9
|
+
};
|
|
10
|
+
/** Override config at import time. Unset fields keep their defaults / env fallback. */
|
|
11
|
+
export function configureAddress(next) {
|
|
12
|
+
config = {
|
|
13
|
+
apiKey: next.apiKey ?? config.apiKey,
|
|
14
|
+
country: next.country === undefined ? config.country : next.country,
|
|
15
|
+
version: next.version ?? config.version,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function resolvedKey(override) {
|
|
19
|
+
if (override)
|
|
20
|
+
return override;
|
|
21
|
+
if (config.apiKey)
|
|
22
|
+
return config.apiKey;
|
|
23
|
+
// Env fallback. MUST be the static `process.env.NEXT_PUBLIC_*` member expression —
|
|
24
|
+
// Next/Vite DefinePlugin replaces exactly this token with the string literal at
|
|
25
|
+
// build (a dynamic `env[key]` access is NOT inlined and resolves to "" in the
|
|
26
|
+
// browser). No `typeof process` guard: after inlining, no `process` ref remains.
|
|
27
|
+
return process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? "";
|
|
28
|
+
}
|
|
29
|
+
export function addressCountry() {
|
|
30
|
+
return config.country ?? null;
|
|
31
|
+
}
|
|
32
|
+
let loadPromise = null;
|
|
33
|
+
/**
|
|
34
|
+
* Load `google.maps.places` once. Resolves with `google.maps`. Rejects if no key
|
|
35
|
+
* is configured or the script fails. Safe to call repeatedly — one script, one
|
|
36
|
+
* promise. Pass `apiKey` to override the configured/env key for this page.
|
|
37
|
+
*/
|
|
38
|
+
export function loadPlaces(apiKey) {
|
|
39
|
+
if (loadPromise)
|
|
40
|
+
return loadPromise;
|
|
41
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
42
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
43
|
+
// SSR — never resolves; the React hook only calls this in an effect.
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
// Already present (e.g. another lib loaded it).
|
|
47
|
+
const existing = window.google;
|
|
48
|
+
if (existing?.maps?.places) {
|
|
49
|
+
resolve(existing.maps);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const key = resolvedKey(apiKey);
|
|
53
|
+
if (!key) {
|
|
54
|
+
reject(new Error("@ikeboy003/address: no Maps API key (set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY or configureAddress({ apiKey }))"));
|
|
55
|
+
loadPromise = null;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const params = new URLSearchParams({ key, libraries: "places", v: config.version });
|
|
59
|
+
const script = document.createElement("script");
|
|
60
|
+
script.src = `https://maps.googleapis.com/maps/api/js?${params.toString()}`;
|
|
61
|
+
script.async = true;
|
|
62
|
+
script.onload = () => {
|
|
63
|
+
const g = window.google;
|
|
64
|
+
if (g?.maps?.places)
|
|
65
|
+
resolve(g.maps);
|
|
66
|
+
else
|
|
67
|
+
reject(new Error("@ikeboy003/address: Maps JS loaded but places library missing"));
|
|
68
|
+
};
|
|
69
|
+
script.onerror = () => {
|
|
70
|
+
loadPromise = null;
|
|
71
|
+
reject(new Error("@ikeboy003/address: failed to load Google Maps JS"));
|
|
72
|
+
};
|
|
73
|
+
document.head.appendChild(script);
|
|
74
|
+
});
|
|
75
|
+
return loadPromise;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../src/loader.ts"],"names":[],"mappings":"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"}
|
package/dist/parse.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AddressResult } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Parse a selected place into `{ formatted, parts, placeId }`. Requires the place
|
|
4
|
+
* to have been fetched with `fields` including `address_components`,
|
|
5
|
+
* `formatted_address`, and `place_id` (the hook requests these).
|
|
6
|
+
*/
|
|
7
|
+
export declare function parsePlace(place: google.maps.places.PlaceResult): AddressResult;
|
|
8
|
+
//# sourceMappingURL=parse.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.d.ts","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAgB,MAAM,SAAS,CAAC;AAY3D;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,GAAG,aAAa,CAyB/E"}
|
package/dist/parse.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Turn a Google PlaceResult into our structured AddressResult. This is the real
|
|
2
|
+
// value over a raw autocomplete: the site gets ShipTo-shaped fields, not just a
|
|
3
|
+
// display string, so the checkout form auto-fills street/city/state/zip.
|
|
4
|
+
import { EMPTY_PARTS } from "./types";
|
|
5
|
+
/** First component whose `types` include one of `wanted`. */
|
|
6
|
+
function pick(components, wanted, short = false) {
|
|
7
|
+
const c = components.find((comp) => comp.types.some((t) => wanted.includes(t)));
|
|
8
|
+
if (!c)
|
|
9
|
+
return "";
|
|
10
|
+
return short ? c.short_name : c.long_name;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse a selected place into `{ formatted, parts, placeId }`. Requires the place
|
|
14
|
+
* to have been fetched with `fields` including `address_components`,
|
|
15
|
+
* `formatted_address`, and `place_id` (the hook requests these).
|
|
16
|
+
*/
|
|
17
|
+
export function parsePlace(place) {
|
|
18
|
+
const components = place.address_components ?? [];
|
|
19
|
+
const streetNumber = pick(components, ["street_number"]);
|
|
20
|
+
const route = pick(components, ["route"]);
|
|
21
|
+
const parts = {
|
|
22
|
+
...EMPTY_PARTS,
|
|
23
|
+
street1: [streetNumber, route].filter(Boolean).join(" "),
|
|
24
|
+
street2: pick(components, ["subpremise"]),
|
|
25
|
+
// locality is the usual city; fall back to postal_town (UK) / sublocality (some
|
|
26
|
+
// metros put the city there).
|
|
27
|
+
city: pick(components, ["locality"]) ||
|
|
28
|
+
pick(components, ["postal_town"]) ||
|
|
29
|
+
pick(components, ["sublocality", "sublocality_level_1"]),
|
|
30
|
+
state: pick(components, ["administrative_area_level_1"], true),
|
|
31
|
+
postal: pick(components, ["postal_code"]),
|
|
32
|
+
country: pick(components, ["country"], true) || "US",
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
formatted: place.formatted_address ?? "",
|
|
36
|
+
parts,
|
|
37
|
+
placeId: place.place_id ?? "",
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=parse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse.js","sourceRoot":"","sources":["../src/parse.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,gFAAgF;AAChF,yEAAyE;AAGzE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAItC,6DAA6D;AAC7D,SAAS,IAAI,CAAC,UAAuB,EAAE,MAAgB,EAAE,KAAK,GAAG,KAAK;IACpE,MAAM,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAChF,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IAClB,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,KAAqC;IAC9D,MAAM,UAAU,GAAG,KAAK,CAAC,kBAAkB,IAAI,EAAE,CAAC;IAElD,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,eAAe,CAAC,CAAC,CAAC;IACzD,MAAM,KAAK,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAiB;QAC1B,GAAG,WAAW;QACd,OAAO,EAAE,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACxD,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,YAAY,CAAC,CAAC;QACzC,gFAAgF;QAChF,8BAA8B;QAC9B,IAAI,EACF,IAAI,CAAC,UAAU,EAAE,CAAC,UAAU,CAAC,CAAC;YAC9B,IAAI,CAAC,UAAU,EAAE,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,UAAU,EAAE,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC;QAC1D,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,6BAA6B,CAAC,EAAE,IAAI,CAAC;QAC9D,MAAM,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,aAAa,CAAC,CAAC;QACzC,OAAO,EAAE,IAAI,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI;KACrD,CAAC;IAEF,OAAO;QACL,SAAS,EAAE,KAAK,CAAC,iBAAiB,IAAI,EAAE;QACxC,KAAK;QACL,OAAO,EAAE,KAAK,CAAC,QAAQ,IAAI,EAAE;KAC9B,CAAC;AACJ,CAAC"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type InputHTMLAttributes, type RefObject } from "react";
|
|
2
|
+
import type { AddressResult } from "./types";
|
|
3
|
+
export interface UseAddressAutocompleteInput {
|
|
4
|
+
/** Called with the parsed address when the user picks a prediction. */
|
|
5
|
+
onSelect: (result: AddressResult) => void;
|
|
6
|
+
/**
|
|
7
|
+
* Country restriction. Defaults to the configured value (`"us"`). `null` = no
|
|
8
|
+
* restriction. Passed to Google's `componentRestrictions`.
|
|
9
|
+
*/
|
|
10
|
+
country?: string | string[] | null;
|
|
11
|
+
/** Override the configured/env Maps key for this input. */
|
|
12
|
+
apiKey?: string;
|
|
13
|
+
/** Gate attaching (e.g. only when the ship-to form is shown). Default true. */
|
|
14
|
+
enabled?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface UseAddressAutocomplete {
|
|
17
|
+
/** Attach to your text <input>. */
|
|
18
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
19
|
+
/** True once the Maps script + widget are attached. */
|
|
20
|
+
ready: boolean;
|
|
21
|
+
/** Load/attach error (missing key, script blocked), else null. */
|
|
22
|
+
error: string | null;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Headless Places Autocomplete. You render the <input> and style it however you
|
|
26
|
+
* want; this wires Google to it and calls `onSelect` with parsed fields. Also
|
|
27
|
+
* swallows the Enter keypress while a prediction list is open so picking a
|
|
28
|
+
* suggestion never accidentally submits the surrounding form.
|
|
29
|
+
*/
|
|
30
|
+
export declare function useAddressAutocomplete({ onSelect, country, apiKey, enabled, }: UseAddressAutocompleteInput): UseAddressAutocomplete;
|
|
31
|
+
export interface AddressAutocompleteProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "onSelect"> {
|
|
32
|
+
/** Parsed address on selection — spread `result.parts` onto your ShipTo. */
|
|
33
|
+
onSelect: (result: AddressResult) => void;
|
|
34
|
+
/** Raw text on every keystroke (before/without a selection). */
|
|
35
|
+
onRawChange?: (value: string) => void;
|
|
36
|
+
country?: string | string[] | null;
|
|
37
|
+
apiKey?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Minimal styled-by-you input with Places attached. Pass `className` + any input
|
|
41
|
+
* props; `onSelect` fires on a picked prediction, `onRawChange` on typing. For full
|
|
42
|
+
* control (labels, error slots, a controlled value) use `useAddressAutocomplete`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function AddressAutocomplete({ onSelect, onRawChange, country, apiKey, ...inputProps }: AddressAutocompleteProps): import("react").JSX.Element;
|
|
45
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAE7C,MAAM,WAAW,2BAA2B;IAC1C,uEAAuE;IACvE,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,sBAAsB;IACrC,mCAAmC;IACnC,QAAQ,EAAE,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;IAC7C,uDAAuD;IACvD,KAAK,EAAE,OAAO,CAAC;IACf,kEAAkE;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,OAAO,EACP,MAAM,EACN,OAAc,GACf,EAAE,2BAA2B,GAAG,sBAAsB,CA2DtD;AAID,MAAM,WAAW,wBACf,SAAQ,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,UAAU,CAAC;IAC/D,4EAA4E;IAC5E,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,gEAAgE;IAChE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,WAAW,EACX,OAAO,EACP,MAAM,EACN,GAAG,UAAU,EACd,EAAE,wBAAwB,+BAc1B"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
// React binding: a headless hook (you own the <input>) and a thin component. The
|
|
4
|
+
// hook attaches Google's Places Autocomplete to your input, and on selection hands
|
|
5
|
+
// back a parsed AddressResult — the site spreads `result.parts` onto its ShipTo.
|
|
6
|
+
import { useCallback, useEffect, useRef, useState, } from "react";
|
|
7
|
+
import { loadPlaces, addressCountry } from "./loader";
|
|
8
|
+
import { parsePlace } from "./parse";
|
|
9
|
+
/**
|
|
10
|
+
* Headless Places Autocomplete. You render the <input> and style it however you
|
|
11
|
+
* want; this wires Google to it and calls `onSelect` with parsed fields. Also
|
|
12
|
+
* swallows the Enter keypress while a prediction list is open so picking a
|
|
13
|
+
* suggestion never accidentally submits the surrounding form.
|
|
14
|
+
*/
|
|
15
|
+
export function useAddressAutocomplete({ onSelect, country, apiKey, enabled = true, }) {
|
|
16
|
+
const inputRef = useRef(null);
|
|
17
|
+
const acRef = useRef(null);
|
|
18
|
+
const [ready, setReady] = useState(false);
|
|
19
|
+
const [error, setError] = useState(null);
|
|
20
|
+
// Keep the latest onSelect without re-attaching the widget every render.
|
|
21
|
+
const onSelectRef = useRef(onSelect);
|
|
22
|
+
onSelectRef.current = onSelect;
|
|
23
|
+
const handlePlace = useCallback(() => {
|
|
24
|
+
const place = acRef.current?.getPlace();
|
|
25
|
+
if (place?.address_components)
|
|
26
|
+
onSelectRef.current(parsePlace(place));
|
|
27
|
+
}, []);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (!enabled)
|
|
30
|
+
return;
|
|
31
|
+
let cancelled = false;
|
|
32
|
+
const input = inputRef.current;
|
|
33
|
+
if (!input)
|
|
34
|
+
return;
|
|
35
|
+
// Enter-while-open should pick the suggestion, not submit the form.
|
|
36
|
+
const onKeyDown = (e) => {
|
|
37
|
+
if (e.key !== "Enter")
|
|
38
|
+
return;
|
|
39
|
+
const pac = document.querySelector(".pac-container");
|
|
40
|
+
const open = pac instanceof HTMLElement && pac.style.display !== "none" && pac.offsetHeight > 0;
|
|
41
|
+
if (open)
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
};
|
|
44
|
+
input.addEventListener("keydown", onKeyDown);
|
|
45
|
+
loadPlaces(apiKey)
|
|
46
|
+
.then((maps) => {
|
|
47
|
+
if (cancelled || !inputRef.current || acRef.current)
|
|
48
|
+
return;
|
|
49
|
+
const restrict = country === undefined ? addressCountry() : country;
|
|
50
|
+
const ac = new maps.places.Autocomplete(inputRef.current, {
|
|
51
|
+
fields: ["address_components", "formatted_address", "place_id"],
|
|
52
|
+
types: ["address"],
|
|
53
|
+
...(restrict ? { componentRestrictions: { country: restrict } } : {}),
|
|
54
|
+
});
|
|
55
|
+
ac.addListener("place_changed", handlePlace);
|
|
56
|
+
acRef.current = ac;
|
|
57
|
+
setReady(true);
|
|
58
|
+
})
|
|
59
|
+
.catch((e) => {
|
|
60
|
+
if (!cancelled)
|
|
61
|
+
setError(e instanceof Error ? e.message : "address autocomplete failed");
|
|
62
|
+
});
|
|
63
|
+
return () => {
|
|
64
|
+
cancelled = true;
|
|
65
|
+
input.removeEventListener("keydown", onKeyDown);
|
|
66
|
+
if (acRef.current) {
|
|
67
|
+
google.maps.event.clearInstanceListeners(acRef.current);
|
|
68
|
+
acRef.current = null;
|
|
69
|
+
}
|
|
70
|
+
setReady(false);
|
|
71
|
+
};
|
|
72
|
+
}, [enabled, apiKey, country, handlePlace]);
|
|
73
|
+
return { inputRef, ready, error };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Minimal styled-by-you input with Places attached. Pass `className` + any input
|
|
77
|
+
* props; `onSelect` fires on a picked prediction, `onRawChange` on typing. For full
|
|
78
|
+
* control (labels, error slots, a controlled value) use `useAddressAutocomplete`.
|
|
79
|
+
*/
|
|
80
|
+
export function AddressAutocomplete({ onSelect, onRawChange, country, apiKey, ...inputProps }) {
|
|
81
|
+
const { inputRef } = useAddressAutocomplete({ onSelect, country, apiKey });
|
|
82
|
+
return (_jsx("input", { ref: inputRef, type: "text", autoComplete: "off", ...inputProps, onChange: (e) => {
|
|
83
|
+
onRawChange?.(e.target.value);
|
|
84
|
+
inputProps.onChange?.(e);
|
|
85
|
+
} }));
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Structured, US-postal-shaped address fields parsed from a Google place. */
|
|
2
|
+
export interface AddressParts {
|
|
3
|
+
/** street_number + route, e.g. "1810 N 4th St". */
|
|
4
|
+
street1: string;
|
|
5
|
+
/** subpremise (apt/suite/unit), e.g. "Apt 4". Empty if none. */
|
|
6
|
+
street2: string;
|
|
7
|
+
/** locality / postal_town / sublocality. */
|
|
8
|
+
city: string;
|
|
9
|
+
/** administrative_area_level_1 short code, e.g. "OH". */
|
|
10
|
+
state: string;
|
|
11
|
+
/** postal_code. */
|
|
12
|
+
postal: string;
|
|
13
|
+
/** ISO-3166 alpha-2 short code, e.g. "US". */
|
|
14
|
+
country: string;
|
|
15
|
+
}
|
|
16
|
+
/** A selected address: the human string, the parsed parts, and the stable id. */
|
|
17
|
+
export interface AddressResult {
|
|
18
|
+
/** Google's formatted_address (display string). */
|
|
19
|
+
formatted: string;
|
|
20
|
+
/** Parsed fields — spread onto a ShipTo. */
|
|
21
|
+
parts: AddressParts;
|
|
22
|
+
/** place_id — the ONLY Google field allowed to be cached/stored long-term. */
|
|
23
|
+
placeId: string;
|
|
24
|
+
}
|
|
25
|
+
/** Import-time config. Anything unset falls back to env at first use. */
|
|
26
|
+
export interface AddressConfig {
|
|
27
|
+
/**
|
|
28
|
+
* Browser Maps JS key. Defaults to `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`. This is a
|
|
29
|
+
* PUBLIC, referrer-restricted key (it ships in client JS) — restrict it to your
|
|
30
|
+
* domains in the Google Cloud console, not by secrecy.
|
|
31
|
+
*/
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Country restriction for predictions. Default `"us"`. Pass an array for several,
|
|
35
|
+
* or `null` to allow all.
|
|
36
|
+
*/
|
|
37
|
+
country?: string | string[] | null;
|
|
38
|
+
/** Maps JS API version. Default `"weekly"`. */
|
|
39
|
+
version?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare const EMPTY_PARTS: AddressParts;
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAIA,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,4CAA4C;IAC5C,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,mBAAmB;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iFAAiF;AACjF,MAAM,WAAW,aAAa;IAC5B,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,KAAK,EAAE,YAAY,CAAC;IACpB,8EAA8E;IAC9E,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC;IACnC,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,eAAO,MAAM,WAAW,EAAE,YAOzB,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Public shapes. `AddressParts` is deliberately the subset of a checkout `ShipTo`
|
|
2
|
+
// that Google can fill — so a site does `setShip(s => ({ ...s, ...result.parts }))`
|
|
3
|
+
// and the street/city/state/zip land with no field-by-field mapping.
|
|
4
|
+
export const EMPTY_PARTS = {
|
|
5
|
+
street1: "",
|
|
6
|
+
street2: "",
|
|
7
|
+
city: "",
|
|
8
|
+
state: "",
|
|
9
|
+
postal: "",
|
|
10
|
+
country: "US",
|
|
11
|
+
};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,oFAAoF;AACpF,qEAAqE;AA6CrE,MAAM,CAAC,MAAM,WAAW,GAAiB;IACvC,OAAO,EAAE,EAAE;IACX,OAAO,EAAE,EAAE;IACX,IAAI,EAAE,EAAE;IACR,KAAK,EAAE,EAAE;IACT,MAAM,EAAE,EAAE;IACV,OAAO,EAAE,IAAI;CACd,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ikeboy003/address",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Headless Google Places address autocomplete: configure-at-import from env, framework-agnostic loader + parser, optional React hook/component. Returns the formatted string AND structured parts (street1/city/state/postal/country) that drop straight onto a checkout ShipTo. No brand/UI baked in.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./react": {
|
|
12
|
+
"types": "./dist/react.d.ts",
|
|
13
|
+
"import": "./dist/react.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.json",
|
|
23
|
+
"dev": "tsc -p tsconfig.json --watch"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"address",
|
|
27
|
+
"autocomplete",
|
|
28
|
+
"google-places",
|
|
29
|
+
"checkout",
|
|
30
|
+
"shipping",
|
|
31
|
+
"headless"
|
|
32
|
+
],
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"react": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"peerDependenciesMeta": {
|
|
41
|
+
"react": {
|
|
42
|
+
"optional": true
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/google.maps": "^3.58.1",
|
|
47
|
+
"@types/node": "^20",
|
|
48
|
+
"@types/react": "^19",
|
|
49
|
+
"typescript": "^5.5.0"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/ikeboy003/ikeboy-npm-packages.git",
|
|
54
|
+
"directory": "packages/address"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// @ikeboy003/address — headless Google Places address autocomplete.
|
|
2
|
+
//
|
|
3
|
+
// Core entry (framework-agnostic): the script loader, the place parser, and
|
|
4
|
+
// import-time config. The React binding (`useAddressAutocomplete`, `<AddressAutocomplete>`)
|
|
5
|
+
// lives in `@ikeboy003/address/react`.
|
|
6
|
+
//
|
|
7
|
+
// import { configureAddress } from "@ikeboy003/address"; // optional
|
|
8
|
+
// import { useAddressAutocomplete } from "@ikeboy003/address/react";
|
|
9
|
+
//
|
|
10
|
+
// One PUBLIC, referrer-restricted browser key serves every site — restrict it to
|
|
11
|
+
// your domains in the Google console. No backend, no cache: the JS widget is
|
|
12
|
+
// session-billed in the browser and Google's ToS forbids storing predictions.
|
|
13
|
+
|
|
14
|
+
export { configureAddress, loadPlaces, addressCountry } from "./loader";
|
|
15
|
+
export { parsePlace } from "./parse";
|
|
16
|
+
export type { AddressParts, AddressResult, AddressConfig } from "./types";
|
|
17
|
+
export { EMPTY_PARTS } from "./types";
|
package/src/loader.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Framework-agnostic loader for the Google Maps JS API (places library). Loads the
|
|
2
|
+
// script exactly once per page and de-dupes concurrent callers behind one promise —
|
|
3
|
+
// three checkout forms mounting at once still yield a single <script>. Browser-only;
|
|
4
|
+
// on the server `load()` returns a never-resolving promise (guarded by callers).
|
|
5
|
+
|
|
6
|
+
import type { AddressConfig } from "./types";
|
|
7
|
+
|
|
8
|
+
let config: Required<Omit<AddressConfig, "apiKey">> & { apiKey: string } = {
|
|
9
|
+
apiKey: "",
|
|
10
|
+
country: "us",
|
|
11
|
+
version: "weekly",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Override config at import time. Unset fields keep their defaults / env fallback. */
|
|
15
|
+
export function configureAddress(next: AddressConfig): void {
|
|
16
|
+
config = {
|
|
17
|
+
apiKey: next.apiKey ?? config.apiKey,
|
|
18
|
+
country: next.country === undefined ? config.country : (next.country as string | string[]),
|
|
19
|
+
version: next.version ?? config.version,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolvedKey(override?: string): string {
|
|
24
|
+
if (override) return override;
|
|
25
|
+
if (config.apiKey) return config.apiKey;
|
|
26
|
+
// Env fallback. MUST be the static `process.env.NEXT_PUBLIC_*` member expression —
|
|
27
|
+
// Next/Vite DefinePlugin replaces exactly this token with the string literal at
|
|
28
|
+
// build (a dynamic `env[key]` access is NOT inlined and resolves to "" in the
|
|
29
|
+
// browser). No `typeof process` guard: after inlining, no `process` ref remains.
|
|
30
|
+
return process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY ?? "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addressCountry(): string | string[] | null {
|
|
34
|
+
return config.country ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let loadPromise: Promise<typeof google.maps> | null = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Load `google.maps.places` once. Resolves with `google.maps`. Rejects if no key
|
|
41
|
+
* is configured or the script fails. Safe to call repeatedly — one script, one
|
|
42
|
+
* promise. Pass `apiKey` to override the configured/env key for this page.
|
|
43
|
+
*/
|
|
44
|
+
export function loadPlaces(apiKey?: string): Promise<typeof google.maps> {
|
|
45
|
+
if (loadPromise) return loadPromise;
|
|
46
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
47
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
48
|
+
// SSR — never resolves; the React hook only calls this in an effect.
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Already present (e.g. another lib loaded it).
|
|
52
|
+
const existing = (window as unknown as { google?: { maps?: typeof google.maps } }).google;
|
|
53
|
+
if (existing?.maps?.places) {
|
|
54
|
+
resolve(existing.maps);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const key = resolvedKey(apiKey);
|
|
58
|
+
if (!key) {
|
|
59
|
+
reject(new Error("@ikeboy003/address: no Maps API key (set NEXT_PUBLIC_GOOGLE_MAPS_API_KEY or configureAddress({ apiKey }))"));
|
|
60
|
+
loadPromise = null;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const params = new URLSearchParams({ key, libraries: "places", v: config.version });
|
|
64
|
+
const script = document.createElement("script");
|
|
65
|
+
script.src = `https://maps.googleapis.com/maps/api/js?${params.toString()}`;
|
|
66
|
+
script.async = true;
|
|
67
|
+
script.onload = () => {
|
|
68
|
+
const g = (window as unknown as { google?: { maps?: typeof google.maps } }).google;
|
|
69
|
+
if (g?.maps?.places) resolve(g.maps);
|
|
70
|
+
else reject(new Error("@ikeboy003/address: Maps JS loaded but places library missing"));
|
|
71
|
+
};
|
|
72
|
+
script.onerror = () => {
|
|
73
|
+
loadPromise = null;
|
|
74
|
+
reject(new Error("@ikeboy003/address: failed to load Google Maps JS"));
|
|
75
|
+
};
|
|
76
|
+
document.head.appendChild(script);
|
|
77
|
+
});
|
|
78
|
+
return loadPromise;
|
|
79
|
+
}
|
package/src/parse.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Turn a Google PlaceResult into our structured AddressResult. This is the real
|
|
2
|
+
// value over a raw autocomplete: the site gets ShipTo-shaped fields, not just a
|
|
3
|
+
// display string, so the checkout form auto-fills street/city/state/zip.
|
|
4
|
+
|
|
5
|
+
import type { AddressResult, AddressParts } from "./types";
|
|
6
|
+
import { EMPTY_PARTS } from "./types";
|
|
7
|
+
|
|
8
|
+
type Component = google.maps.GeocoderAddressComponent;
|
|
9
|
+
|
|
10
|
+
/** First component whose `types` include one of `wanted`. */
|
|
11
|
+
function pick(components: Component[], wanted: string[], short = false): string {
|
|
12
|
+
const c = components.find((comp) => comp.types.some((t) => wanted.includes(t)));
|
|
13
|
+
if (!c) return "";
|
|
14
|
+
return short ? c.short_name : c.long_name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse a selected place into `{ formatted, parts, placeId }`. Requires the place
|
|
19
|
+
* to have been fetched with `fields` including `address_components`,
|
|
20
|
+
* `formatted_address`, and `place_id` (the hook requests these).
|
|
21
|
+
*/
|
|
22
|
+
export function parsePlace(place: google.maps.places.PlaceResult): AddressResult {
|
|
23
|
+
const components = place.address_components ?? [];
|
|
24
|
+
|
|
25
|
+
const streetNumber = pick(components, ["street_number"]);
|
|
26
|
+
const route = pick(components, ["route"]);
|
|
27
|
+
const parts: AddressParts = {
|
|
28
|
+
...EMPTY_PARTS,
|
|
29
|
+
street1: [streetNumber, route].filter(Boolean).join(" "),
|
|
30
|
+
street2: pick(components, ["subpremise"]),
|
|
31
|
+
// locality is the usual city; fall back to postal_town (UK) / sublocality (some
|
|
32
|
+
// metros put the city there).
|
|
33
|
+
city:
|
|
34
|
+
pick(components, ["locality"]) ||
|
|
35
|
+
pick(components, ["postal_town"]) ||
|
|
36
|
+
pick(components, ["sublocality", "sublocality_level_1"]),
|
|
37
|
+
state: pick(components, ["administrative_area_level_1"], true),
|
|
38
|
+
postal: pick(components, ["postal_code"]),
|
|
39
|
+
country: pick(components, ["country"], true) || "US",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
formatted: place.formatted_address ?? "",
|
|
44
|
+
parts,
|
|
45
|
+
placeId: place.place_id ?? "",
|
|
46
|
+
};
|
|
47
|
+
}
|
package/src/react.tsx
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// React binding: a headless hook (you own the <input>) and a thin component. The
|
|
4
|
+
// hook attaches Google's Places Autocomplete to your input, and on selection hands
|
|
5
|
+
// back a parsed AddressResult — the site spreads `result.parts` onto its ShipTo.
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
useCallback,
|
|
9
|
+
useEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
type InputHTMLAttributes,
|
|
13
|
+
type RefObject,
|
|
14
|
+
} from "react";
|
|
15
|
+
import { loadPlaces, addressCountry } from "./loader";
|
|
16
|
+
import { parsePlace } from "./parse";
|
|
17
|
+
import type { AddressResult } from "./types";
|
|
18
|
+
|
|
19
|
+
export interface UseAddressAutocompleteInput {
|
|
20
|
+
/** Called with the parsed address when the user picks a prediction. */
|
|
21
|
+
onSelect: (result: AddressResult) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Country restriction. Defaults to the configured value (`"us"`). `null` = no
|
|
24
|
+
* restriction. Passed to Google's `componentRestrictions`.
|
|
25
|
+
*/
|
|
26
|
+
country?: string | string[] | null;
|
|
27
|
+
/** Override the configured/env Maps key for this input. */
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
/** Gate attaching (e.g. only when the ship-to form is shown). Default true. */
|
|
30
|
+
enabled?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UseAddressAutocomplete {
|
|
34
|
+
/** Attach to your text <input>. */
|
|
35
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
36
|
+
/** True once the Maps script + widget are attached. */
|
|
37
|
+
ready: boolean;
|
|
38
|
+
/** Load/attach error (missing key, script blocked), else null. */
|
|
39
|
+
error: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Headless Places Autocomplete. You render the <input> and style it however you
|
|
44
|
+
* want; this wires Google to it and calls `onSelect` with parsed fields. Also
|
|
45
|
+
* swallows the Enter keypress while a prediction list is open so picking a
|
|
46
|
+
* suggestion never accidentally submits the surrounding form.
|
|
47
|
+
*/
|
|
48
|
+
export function useAddressAutocomplete({
|
|
49
|
+
onSelect,
|
|
50
|
+
country,
|
|
51
|
+
apiKey,
|
|
52
|
+
enabled = true,
|
|
53
|
+
}: UseAddressAutocompleteInput): UseAddressAutocomplete {
|
|
54
|
+
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
55
|
+
const acRef = useRef<google.maps.places.Autocomplete | null>(null);
|
|
56
|
+
const [ready, setReady] = useState(false);
|
|
57
|
+
const [error, setError] = useState<string | null>(null);
|
|
58
|
+
|
|
59
|
+
// Keep the latest onSelect without re-attaching the widget every render.
|
|
60
|
+
const onSelectRef = useRef(onSelect);
|
|
61
|
+
onSelectRef.current = onSelect;
|
|
62
|
+
|
|
63
|
+
const handlePlace = useCallback(() => {
|
|
64
|
+
const place = acRef.current?.getPlace();
|
|
65
|
+
if (place?.address_components) onSelectRef.current(parsePlace(place));
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
if (!enabled) return;
|
|
70
|
+
let cancelled = false;
|
|
71
|
+
const input = inputRef.current;
|
|
72
|
+
if (!input) return;
|
|
73
|
+
|
|
74
|
+
// Enter-while-open should pick the suggestion, not submit the form.
|
|
75
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
76
|
+
if (e.key !== "Enter") return;
|
|
77
|
+
const pac = document.querySelector(".pac-container");
|
|
78
|
+
const open = pac instanceof HTMLElement && pac.style.display !== "none" && pac.offsetHeight > 0;
|
|
79
|
+
if (open) e.preventDefault();
|
|
80
|
+
};
|
|
81
|
+
input.addEventListener("keydown", onKeyDown);
|
|
82
|
+
|
|
83
|
+
loadPlaces(apiKey)
|
|
84
|
+
.then((maps) => {
|
|
85
|
+
if (cancelled || !inputRef.current || acRef.current) return;
|
|
86
|
+
const restrict = country === undefined ? addressCountry() : country;
|
|
87
|
+
const ac = new maps.places.Autocomplete(inputRef.current, {
|
|
88
|
+
fields: ["address_components", "formatted_address", "place_id"],
|
|
89
|
+
types: ["address"],
|
|
90
|
+
...(restrict ? { componentRestrictions: { country: restrict } } : {}),
|
|
91
|
+
});
|
|
92
|
+
ac.addListener("place_changed", handlePlace);
|
|
93
|
+
acRef.current = ac;
|
|
94
|
+
setReady(true);
|
|
95
|
+
})
|
|
96
|
+
.catch((e) => {
|
|
97
|
+
if (!cancelled) setError(e instanceof Error ? e.message : "address autocomplete failed");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
input.removeEventListener("keydown", onKeyDown);
|
|
103
|
+
if (acRef.current) {
|
|
104
|
+
google.maps.event.clearInstanceListeners(acRef.current);
|
|
105
|
+
acRef.current = null;
|
|
106
|
+
}
|
|
107
|
+
setReady(false);
|
|
108
|
+
};
|
|
109
|
+
}, [enabled, apiKey, country, handlePlace]);
|
|
110
|
+
|
|
111
|
+
return { inputRef, ready, error };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Convenience component ────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export interface AddressAutocompleteProps
|
|
117
|
+
extends Omit<InputHTMLAttributes<HTMLInputElement>, "onSelect"> {
|
|
118
|
+
/** Parsed address on selection — spread `result.parts` onto your ShipTo. */
|
|
119
|
+
onSelect: (result: AddressResult) => void;
|
|
120
|
+
/** Raw text on every keystroke (before/without a selection). */
|
|
121
|
+
onRawChange?: (value: string) => void;
|
|
122
|
+
country?: string | string[] | null;
|
|
123
|
+
apiKey?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Minimal styled-by-you input with Places attached. Pass `className` + any input
|
|
128
|
+
* props; `onSelect` fires on a picked prediction, `onRawChange` on typing. For full
|
|
129
|
+
* control (labels, error slots, a controlled value) use `useAddressAutocomplete`.
|
|
130
|
+
*/
|
|
131
|
+
export function AddressAutocomplete({
|
|
132
|
+
onSelect,
|
|
133
|
+
onRawChange,
|
|
134
|
+
country,
|
|
135
|
+
apiKey,
|
|
136
|
+
...inputProps
|
|
137
|
+
}: AddressAutocompleteProps) {
|
|
138
|
+
const { inputRef } = useAddressAutocomplete({ onSelect, country, apiKey });
|
|
139
|
+
return (
|
|
140
|
+
<input
|
|
141
|
+
ref={inputRef}
|
|
142
|
+
type="text"
|
|
143
|
+
autoComplete="off"
|
|
144
|
+
{...inputProps}
|
|
145
|
+
onChange={(e) => {
|
|
146
|
+
onRawChange?.(e.target.value);
|
|
147
|
+
inputProps.onChange?.(e);
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Public shapes. `AddressParts` is deliberately the subset of a checkout `ShipTo`
|
|
2
|
+
// that Google can fill — so a site does `setShip(s => ({ ...s, ...result.parts }))`
|
|
3
|
+
// and the street/city/state/zip land with no field-by-field mapping.
|
|
4
|
+
|
|
5
|
+
/** Structured, US-postal-shaped address fields parsed from a Google place. */
|
|
6
|
+
export interface AddressParts {
|
|
7
|
+
/** street_number + route, e.g. "1810 N 4th St". */
|
|
8
|
+
street1: string;
|
|
9
|
+
/** subpremise (apt/suite/unit), e.g. "Apt 4". Empty if none. */
|
|
10
|
+
street2: string;
|
|
11
|
+
/** locality / postal_town / sublocality. */
|
|
12
|
+
city: string;
|
|
13
|
+
/** administrative_area_level_1 short code, e.g. "OH". */
|
|
14
|
+
state: string;
|
|
15
|
+
/** postal_code. */
|
|
16
|
+
postal: string;
|
|
17
|
+
/** ISO-3166 alpha-2 short code, e.g. "US". */
|
|
18
|
+
country: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A selected address: the human string, the parsed parts, and the stable id. */
|
|
22
|
+
export interface AddressResult {
|
|
23
|
+
/** Google's formatted_address (display string). */
|
|
24
|
+
formatted: string;
|
|
25
|
+
/** Parsed fields — spread onto a ShipTo. */
|
|
26
|
+
parts: AddressParts;
|
|
27
|
+
/** place_id — the ONLY Google field allowed to be cached/stored long-term. */
|
|
28
|
+
placeId: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Import-time config. Anything unset falls back to env at first use. */
|
|
32
|
+
export interface AddressConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Browser Maps JS key. Defaults to `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`. This is a
|
|
35
|
+
* PUBLIC, referrer-restricted key (it ships in client JS) — restrict it to your
|
|
36
|
+
* domains in the Google Cloud console, not by secrecy.
|
|
37
|
+
*/
|
|
38
|
+
apiKey?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Country restriction for predictions. Default `"us"`. Pass an array for several,
|
|
41
|
+
* or `null` to allow all.
|
|
42
|
+
*/
|
|
43
|
+
country?: string | string[] | null;
|
|
44
|
+
/** Maps JS API version. Default `"weekly"`. */
|
|
45
|
+
version?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const EMPTY_PARTS: AddressParts = {
|
|
49
|
+
street1: "",
|
|
50
|
+
street2: "",
|
|
51
|
+
city: "",
|
|
52
|
+
state: "",
|
|
53
|
+
postal: "",
|
|
54
|
+
country: "US",
|
|
55
|
+
};
|