@zappie/sdk 0.1.16 → 0.1.18
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/CHANGELOG.md +25 -0
- package/README.md +41 -15
- package/dist/checkout/embed.d.ts +148 -0
- package/dist/checkout/embed.js +240 -0
- package/dist/checkout/embed.js.map +7 -0
- package/dist/checkout/react.d.ts +150 -0
- package/dist/checkout/react.js +39 -0
- package/dist/checkout/react.js.map +7 -0
- package/dist/debug.js +1 -1
- package/dist/index.js +1 -1
- package/dist/server/auth/crypto.js +106 -0
- package/dist/server/auth/crypto.js.map +7 -0
- package/dist/server/auth/jwt.js +204 -0
- package/dist/server/auth/jwt.js.map +7 -0
- package/dist/server/auth/session-store.js +46 -0
- package/dist/server/auth/session-store.js.map +7 -0
- package/dist/server/edge-snapshot.js +200 -0
- package/dist/server/edge-snapshot.js.map +7 -0
- package/dist/server/index.d.ts +324 -2
- package/dist/server/index.js +40 -0
- package/dist/server/index.js.map +2 -2
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @zappie/sdk
|
|
2
2
|
|
|
3
|
+
## 0.1.18
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- f0c08ed: Open checkout over the page. `embedCheckout()` from `@zappie/sdk/checkout/embed` frames a checkout session as a modal on desktop or a bottom sheet on mobile, and redirects in in-app browsers and framed pages. Back and Escape ask the checkout to step back or close, and once a payment has started the checkout asks the shopper before it closes. A completed result waits for the server to confirm the session, and a completed session can't be reopened. `resumeCheckout()` drops a stale checkout entry from the address without reopening it. `useCheckout()` from `@zappie/sdk/checkout/react` creates the session and opens it.
|
|
8
|
+
|
|
9
|
+
## 0.1.17
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a151b0b: `@zappie/sdk/server` adds the menu snapshot types and helpers that edge menu reads are built on. `MenuSnapshot` wraps the SDK `Menu` that `menu.get()` returns, with the store, the branch, the API's menu version and a `contentHash`. `StoreSnapshot` wraps the SDK `Store`, and `AvailabilityPatch` lists the items, variants and choices that sold out since the menu was built.
|
|
14
|
+
|
|
15
|
+
- `canonicalJson()` writes a value with sorted keys, so equal values give the same text. `snapshotHash()` returns the first 16 hex digits of its SHA-256.
|
|
16
|
+
- `createMenuSnapshot()` builds a snapshot from a complete menu and refuses a partial one. `menuSnapshotFromPayload()` maps the API's menu payload with the same mapper `menu.get()` uses.
|
|
17
|
+
- `createStoreSnapshot()` and `createAvailabilityPatch()` build the other two. A patch's ids are sorted and unique, so equal patches hash the same.
|
|
18
|
+
- `applyAvailability()` returns the snapshot's menu with a patch applied, and ignores a patch older than the snapshot.
|
|
19
|
+
- `isMenuSnapshot()` checks a value read back from storage, and `snapshotKeys` names the storage keys.
|
|
20
|
+
|
|
21
|
+
- 195a5b8: `@zappie/sdk/server` adds the building blocks for signing customers in on a site you host yourself. They use Web Crypto only, so they run in Workers and Node 20 or later.
|
|
22
|
+
|
|
23
|
+
- `createPkce()` returns a PKCE verifier and its S256 challenge. `randomToken()` returns a random URL-safe value for state, nonces and ids.
|
|
24
|
+
- `sealValue()` and `openSealedValue()` keep a JSON value in an AES-GCM sealed string. Pass several secrets to rotate: the first seals and any of them opens. A `purpose` binds the value to one use. Without one, the format matches Zappie's hosted sign-in.
|
|
25
|
+
- `createTokenVerifier({ issuer, audience })` verifies RS256 and ES256 tokens against the issuer's keys at `<issuer>/.well-known/jwks.json`, or a `jwksUrl` you set. It caches the keys for an hour, reads them again once when a token names a new key, and keeps the last good keys while the issuer is down. While the issuer is down and no keys are cached, it reads them at most every 30 seconds, and a set with no usable keys never replaces good ones. It checks the issuer (a trailing slash on either side is ignored), the audience, the expiry with 60 seconds of leeway, `nbf`, `iat`, the nonce you pass and a required `cus_` subject. `verifyIdToken(token, { nonce })` needs the nonce and a subject, and refuses a token whose `typ` names another kind, such as an access token. A refused token throws `ZappieTokenError` with a `reason`.
|
|
26
|
+
- `SessionStore` is the interface every session store implements. `cookieSessionStore({ secret })` keeps the whole session sealed in the cookie for up to 30 days. It can't end sessions from the server, so its `deleteBySub` rejects, and signing out doesn't invalidate a copied cookie before it expires.
|
|
27
|
+
|
|
3
28
|
## 0.1.16
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -50,24 +50,50 @@ Prices always come from Zappie. The cart shows a local estimate while it waits,
|
|
|
50
50
|
|
|
51
51
|
## Entry points
|
|
52
52
|
|
|
53
|
-
| Import | For
|
|
54
|
-
| -------------------------------------------- |
|
|
55
|
-
| `@zappie/sdk` | The client, the cart, money formatting and errors
|
|
56
|
-
| `@zappie/sdk/react` | `ZappieProvider` and hooks such as `useStore`, `useMenu` and `useCart`
|
|
57
|
-
| `@zappie/sdk/checkout` | Opening Zappie's hosted checkout and checking it completed
|
|
58
|
-
| `@zappie/sdk/checkout-defaults` | The phone, address, region and time a signed-in checkout starts from
|
|
59
|
-
| `@zappie/sdk/customer` and `/customer/react` | Signed-in customers, their orders and saved addresses
|
|
60
|
-
| `@zappie/sdk/integrations` | Public integration IDs and browser keys
|
|
61
|
-
| `@zappie/sdk/places` | Address search and delivery-zone checks
|
|
62
|
-
| `@zappie/sdk/pricing` | An item's starting price and whether to show it as "From"
|
|
63
|
-
| `@zappie/sdk/server` | Server-side reads, storefront URLs
|
|
64
|
-
| `@zappie/sdk/template` and `/template/react` | Template settings and responsive images for template authors
|
|
65
|
-
| `@zappie/sdk/testing` | A mock client for your tests
|
|
66
|
-
| `@zappie/sdk/url` | Storefront URL parsing and canonical redirects
|
|
67
|
-
| `@zappie/sdk/redirect` | Same-origin validation for untrusted redirect targets
|
|
53
|
+
| Import | For |
|
|
54
|
+
| -------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
55
|
+
| `@zappie/sdk` | The client, the cart, money formatting and errors |
|
|
56
|
+
| `@zappie/sdk/react` | `ZappieProvider` and hooks such as `useStore`, `useMenu` and `useCart` |
|
|
57
|
+
| `@zappie/sdk/checkout` | Opening Zappie's hosted checkout and checking it completed |
|
|
58
|
+
| `@zappie/sdk/checkout-defaults` | The phone, address, region and time a signed-in checkout starts from |
|
|
59
|
+
| `@zappie/sdk/customer` and `/customer/react` | Signed-in customers, their orders and saved addresses |
|
|
60
|
+
| `@zappie/sdk/integrations` | Public integration IDs and browser keys |
|
|
61
|
+
| `@zappie/sdk/places` | Address search and delivery-zone checks |
|
|
62
|
+
| `@zappie/sdk/pricing` | An item's starting price and whether to show it as "From" |
|
|
63
|
+
| `@zappie/sdk/server` | Server-side reads, storefront URLs, `verifyWebhook()`, menu snapshots and sign-in crypto |
|
|
64
|
+
| `@zappie/sdk/template` and `/template/react` | Template settings and responsive images for template authors |
|
|
65
|
+
| `@zappie/sdk/testing` | A mock client for your tests |
|
|
66
|
+
| `@zappie/sdk/url` | Storefront URL parsing and canonical redirects |
|
|
67
|
+
| `@zappie/sdk/redirect` | Same-origin validation for untrusted redirect targets |
|
|
68
68
|
|
|
69
69
|
Customer sign-in runs through Zappie's site host, so `@zappie/sdk/customer` works on sites Zappie hosts. Sign-in for a site you host yourself isn't available yet.
|
|
70
70
|
|
|
71
|
+
A menu snapshot is the SDK `Menu` with the facts an edge cache needs: the
|
|
72
|
+
store, the branch, the API's menu version and a content hash. Build one with
|
|
73
|
+
`createMenuSnapshot()` or `menuSnapshotFromPayload()` from `@zappie/sdk/server`,
|
|
74
|
+
and apply a sold-out patch to it with `applyAvailability()`.
|
|
75
|
+
|
|
76
|
+
`@zappie/sdk/server` has the pieces that sign-in will be built from. They use Web Crypto only, so they run in Workers and Node 20 or later:
|
|
77
|
+
|
|
78
|
+
- `createPkce()` and `randomToken()` for the code challenge, state and nonce;
|
|
79
|
+
- `sealValue()` and `openSealedValue()` to keep a value in an AES-GCM cookie, with secret rotation;
|
|
80
|
+
- `createTokenVerifier()` to check a token's signature against the issuer's published keys, and its issuer, audience, expiry and subject. `verifyIdToken()` also needs the sign-in's nonce and refuses an access token;
|
|
81
|
+
- `SessionStore` and `cookieSessionStore()` to keep a signed-in session in a sealed cookie.
|
|
82
|
+
|
|
83
|
+
Make each sealing secret at least 32 random bytes, such as a value from `randomToken()`. Don't use a password or a phrase. The secret is hashed once with no stretching, so anyone holding one sealed value can test guesses offline.
|
|
84
|
+
|
|
85
|
+
`cookieSessionStore()` keeps the session in the cookie, not on your server. Signing out clears the cookie in that browser only. It doesn't invalidate a copied cookie, which still opens the session until it expires 30 days after sign-in. To end every session at once, rotate the secret. To end one session from the server, use a store that keeps sessions there.
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { createTokenVerifier } from "@zappie/sdk/server";
|
|
89
|
+
|
|
90
|
+
const verifier = createTokenVerifier({
|
|
91
|
+
issuer: "https://accounts.zappie.ai",
|
|
92
|
+
audience: clientId,
|
|
93
|
+
});
|
|
94
|
+
const claims = await verifier.verifyIdToken(idToken, { nonce });
|
|
95
|
+
```
|
|
96
|
+
|
|
71
97
|
Pass any redirect target read from a query parameter through `safeRedirect`.
|
|
72
98
|
It keeps same-origin relative paths and returns your trusted fallback for an
|
|
73
99
|
external, malformed or unsupported target.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { WireLine, WireMoney } from '@zappie/sdk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Address fields you can send with a delivery checkout request.
|
|
5
|
+
*/
|
|
6
|
+
type CheckoutAddress = {
|
|
7
|
+
line1?: string;
|
|
8
|
+
city?: string;
|
|
9
|
+
place_id?: string;
|
|
10
|
+
lat?: number;
|
|
11
|
+
lng?: number;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Pickup or delivery details for a checkout session, including an optional schedule.
|
|
15
|
+
*/
|
|
16
|
+
type CheckoutFulfilment = {
|
|
17
|
+
/** Dine-in orders are not checked out through a session. */
|
|
18
|
+
type: "pickup" | "delivery";
|
|
19
|
+
address?: CheckoutAddress;
|
|
20
|
+
scheduled_for?: string | null;
|
|
21
|
+
delivery_region_id?: string;
|
|
22
|
+
instructions?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Cart lines, branch, fulfilment and return URLs you send to open checkout.
|
|
26
|
+
*/
|
|
27
|
+
type CheckoutRequest = {
|
|
28
|
+
branch_id: string;
|
|
29
|
+
menu_version: string;
|
|
30
|
+
lines: WireLine[];
|
|
31
|
+
fulfilment: CheckoutFulfilment;
|
|
32
|
+
contact_phone?: string;
|
|
33
|
+
promo_code?: string;
|
|
34
|
+
return_url: string;
|
|
35
|
+
cancel_url?: string;
|
|
36
|
+
source?: string;
|
|
37
|
+
campaign?: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* A checkout session returned by the service, with its amount, expiry and order status.
|
|
41
|
+
*/
|
|
42
|
+
type CheckoutSession = {
|
|
43
|
+
id: string;
|
|
44
|
+
status: "open" | "completed" | "expired" | "cancelled";
|
|
45
|
+
url?: string;
|
|
46
|
+
amount: WireMoney;
|
|
47
|
+
order_id: string | null;
|
|
48
|
+
return_url: string;
|
|
49
|
+
cancel_url?: string;
|
|
50
|
+
expires_at: string;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* A checkout session with a payment URL you can use to send the shopper to checkout.
|
|
54
|
+
*/
|
|
55
|
+
type OpenCheckoutSession = CheckoutSession & {
|
|
56
|
+
url: string;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* A terminal checkout result: a completed order, an expired session or a cancelled session.
|
|
60
|
+
*/
|
|
61
|
+
type CheckoutCompletion = (CheckoutSession & {
|
|
62
|
+
status: "completed";
|
|
63
|
+
order_id: string;
|
|
64
|
+
}) | (CheckoutSession & {
|
|
65
|
+
status: "expired" | "cancelled";
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Optional customer API base URL and fetch implementation for the checkout client.
|
|
69
|
+
*/
|
|
70
|
+
type CheckoutClientOptions = {
|
|
71
|
+
customerBase?: string;
|
|
72
|
+
fetch?: typeof fetch;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Cancellation signal and polling interval in milliseconds for awaiting checkout completion.
|
|
76
|
+
*/
|
|
77
|
+
type AwaitCompletionOptions = {
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
intervalMs?: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Create a client that opens checkout sessions, reads them and polls for completion.
|
|
84
|
+
*
|
|
85
|
+
* @param options - Override the default `/_zappie/api` base or fetch implementation.
|
|
86
|
+
* @returns A checkout client. Retain each idempotency key when retrying the same cart.
|
|
87
|
+
*/
|
|
88
|
+
declare function createCheckout(options?: CheckoutClientOptions): {
|
|
89
|
+
/** The caller retains this key and reuses it when retrying the same cart. */
|
|
90
|
+
open(body: CheckoutRequest, idempotencyKey: string): Promise<OpenCheckoutSession>;
|
|
91
|
+
session(id: string): Promise<CheckoutSession>;
|
|
92
|
+
awaitCompletion(id: string, options?: AwaitCompletionOptions): Promise<CheckoutCompletion>;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
95
|
+
* The session creation, lookup and completion methods returned by createCheckout.
|
|
96
|
+
*/
|
|
97
|
+
type CheckoutClient = ReturnType<typeof createCheckout>;
|
|
98
|
+
|
|
99
|
+
/** How checkout opens. Auto redirects in in-app browsers and framed pages, else a modal on desktop and a sheet on mobile. */
|
|
100
|
+
type CheckoutPresentation = "auto" | "modal" | "sheet" | "fullscreen" | "redirect";
|
|
101
|
+
/** The checkout look from toCheckoutTheme in @zappie/tokens/checkout. The checkout validates it before use. */
|
|
102
|
+
type CheckoutTheme = {
|
|
103
|
+
version: 1;
|
|
104
|
+
tokens: Partial<Record<`--z-${string}`, string>>;
|
|
105
|
+
logo?: {
|
|
106
|
+
url: string;
|
|
107
|
+
alt: string;
|
|
108
|
+
};
|
|
109
|
+
fonts?: {
|
|
110
|
+
family: string;
|
|
111
|
+
role: "body" | "display";
|
|
112
|
+
weights?: number[];
|
|
113
|
+
}[];
|
|
114
|
+
copy?: {
|
|
115
|
+
payButton?: string;
|
|
116
|
+
successTitle?: string;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
/** Presentation, theme and step reporting for an embedded checkout. */
|
|
120
|
+
type EmbedOptions = {
|
|
121
|
+
presentation?: CheckoutPresentation;
|
|
122
|
+
theme?: CheckoutTheme;
|
|
123
|
+
/** Called with each checkout step, and whether a payment has started. */
|
|
124
|
+
onStep?: (step: string, paying: boolean) => void;
|
|
125
|
+
};
|
|
126
|
+
/** How an embedded checkout ended. A completed result is confirmed by the server first. */
|
|
127
|
+
type CheckoutResult = {
|
|
128
|
+
status: "completed";
|
|
129
|
+
orderId: string;
|
|
130
|
+
} | {
|
|
131
|
+
status: "cancelled";
|
|
132
|
+
reason: string;
|
|
133
|
+
} | {
|
|
134
|
+
status: "redirected";
|
|
135
|
+
};
|
|
136
|
+
/** Resolve auto to the presentation this browser gets. */
|
|
137
|
+
declare function checkoutPresentation(presentation?: CheckoutPresentation): Exclude<CheckoutPresentation, "auto">;
|
|
138
|
+
/** Ask the open checkout to close. Mid-payment it asks the shopper first. */
|
|
139
|
+
declare function closeCheckout(): void;
|
|
140
|
+
/** On load, drop a stale checkout entry from the address and read its session. It never reopens checkout. */
|
|
141
|
+
declare function resumeCheckout(checkout: CheckoutClient): Promise<CheckoutResult | null>;
|
|
142
|
+
/**
|
|
143
|
+
* Open a checkout session over the page and settle when it ends. Only Zappie's checkout, or one on this machine, is framed. Back and Escape ask the checkout to step back or
|
|
144
|
+
* close. A completed result waits for the server to confirm the session. A completed session can't be reopened.
|
|
145
|
+
*/
|
|
146
|
+
declare function embedCheckout(checkout: CheckoutClient, session: OpenCheckoutSession, options?: EmbedOptions): Promise<CheckoutResult>;
|
|
147
|
+
|
|
148
|
+
export { type CheckoutPresentation, type CheckoutResult, type CheckoutTheme, type EmbedOptions, checkoutPresentation, closeCheckout, embedCheckout, resumeCheckout };
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// src/checkout/embed.ts
|
|
2
|
+
import { ZappieError } from "./sdk.js";
|
|
3
|
+
var PARAM = "zappie_checkout";
|
|
4
|
+
var finished = /* @__PURE__ */ new Set();
|
|
5
|
+
var request;
|
|
6
|
+
var media = (query) => matchMedia(query).matches;
|
|
7
|
+
function checkoutPresentation(presentation = "auto") {
|
|
8
|
+
if (presentation !== "auto") {
|
|
9
|
+
return presentation;
|
|
10
|
+
}
|
|
11
|
+
if (window.top !== window || /FBA[NV]|Instagram|TikTok|musical_ly|Bytedance|Line\//.test(
|
|
12
|
+
navigator.userAgent
|
|
13
|
+
)) {
|
|
14
|
+
return "redirect";
|
|
15
|
+
}
|
|
16
|
+
return media("(min-width:640px) and (pointer:fine),(min-width:1024px)") ? "modal" : "sheet";
|
|
17
|
+
}
|
|
18
|
+
function closeCheckout() {
|
|
19
|
+
request?.();
|
|
20
|
+
}
|
|
21
|
+
async function resumeCheckout(checkout) {
|
|
22
|
+
const url = new URL(location.href);
|
|
23
|
+
const id = url.searchParams.get(PARAM);
|
|
24
|
+
if (!id) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
url.searchParams.delete(PARAM);
|
|
28
|
+
history.replaceState(history.state, "", url);
|
|
29
|
+
const session = await checkout.session(id);
|
|
30
|
+
return session.status === "completed" && session.order_id ? { status: "completed", orderId: session.order_id } : null;
|
|
31
|
+
}
|
|
32
|
+
function embedCheckout(checkout, session, options = {}) {
|
|
33
|
+
const url = new URL(session.url);
|
|
34
|
+
if (request || session.status !== "open" || finished.has(session.id) || !(/^https:\/\/checkout\.(stg\.)?zappie\.ai$/.test(url.origin) || /^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(url.origin))) {
|
|
35
|
+
return Promise.reject(
|
|
36
|
+
new ZappieError(
|
|
37
|
+
"invalid_request",
|
|
38
|
+
"This checkout session can't be opened"
|
|
39
|
+
)
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
const shown = checkoutPresentation(options.presentation);
|
|
43
|
+
if (shown === "redirect") {
|
|
44
|
+
location.assign(session.url);
|
|
45
|
+
return Promise.resolve({ status: "redirected" });
|
|
46
|
+
}
|
|
47
|
+
url.pathname = `/embed/${encodeURIComponent(session.id)}`;
|
|
48
|
+
const doc = document;
|
|
49
|
+
const body = doc.body;
|
|
50
|
+
const root = doc.createElement("div");
|
|
51
|
+
const panel = doc.createElement("div");
|
|
52
|
+
const handle = doc.createElement("div");
|
|
53
|
+
const frame = doc.createElement("iframe");
|
|
54
|
+
const sheet = shown === "sheet";
|
|
55
|
+
const still = media("(prefers-reduced-motion:reduce)");
|
|
56
|
+
const opener = doc.activeElement;
|
|
57
|
+
const y = scrollY;
|
|
58
|
+
const bodyStyle = body.style.cssText;
|
|
59
|
+
const restoration = history.scrollRestoration;
|
|
60
|
+
const inert = [...body.children].filter(
|
|
61
|
+
(el) => !el.inert
|
|
62
|
+
);
|
|
63
|
+
const abort = new AbortController();
|
|
64
|
+
const vv = window.visualViewport;
|
|
65
|
+
let ready = false;
|
|
66
|
+
let paying = false;
|
|
67
|
+
let height = 640;
|
|
68
|
+
let settle = () => {
|
|
69
|
+
};
|
|
70
|
+
root.setAttribute("data-zappie-checkout", shown);
|
|
71
|
+
root.setAttribute("role", "dialog");
|
|
72
|
+
root.setAttribute("aria-modal", "true");
|
|
73
|
+
root.setAttribute("aria-label", "Checkout");
|
|
74
|
+
root.style.cssText = "position:fixed;inset:0;z-index:2147483647;background:var(--z-checkout-backdrop,rgb(0 0 0/.5))";
|
|
75
|
+
panel.style.cssText = `position:absolute;overflow:hidden;display:flex;flex-direction:column;background:var(--z-surface,#fff);${shown === "fullscreen" ? "inset:0" : sheet ? "left:0;right:0;border-radius:var(--z-checkout-radius,16px) var(--z-checkout-radius,16px) 0 0" : "inset:0;margin:auto;width:min(480px,100vw - 32px);border-radius:var(--z-checkout-radius,16px)"}`;
|
|
76
|
+
handle.style.cssText = "flex:none;height:24px;touch-action:none;cursor:grab;background:radial-gradient(closest-side,var(--z-border,#ccc) 99%,#0000) 50%/40px 5px no-repeat";
|
|
77
|
+
frame.title = "Checkout";
|
|
78
|
+
frame.src = url.href;
|
|
79
|
+
frame.allow = "payment 'src'; publickey-credentials-get 'src'";
|
|
80
|
+
frame.referrerPolicy = "no-referrer";
|
|
81
|
+
frame.style.cssText = "flex:1;width:100%;border:0;display:block";
|
|
82
|
+
if (sheet) {
|
|
83
|
+
panel.append(handle);
|
|
84
|
+
}
|
|
85
|
+
panel.append(frame);
|
|
86
|
+
root.append(panel);
|
|
87
|
+
const layout = () => {
|
|
88
|
+
const view = vv ?? { height: innerHeight, offsetTop: 0 };
|
|
89
|
+
if (sheet) {
|
|
90
|
+
panel.style.height = `${Math.round(view.height * 0.92)}px`;
|
|
91
|
+
panel.style.bottom = `${Math.max(0, innerHeight - view.offsetTop - view.height)}px`;
|
|
92
|
+
} else if (shown === "modal") {
|
|
93
|
+
panel.style.height = `${Math.min(height, view.height - 32)}px`;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const post = (type, payload = {}) => frame.contentWindow?.postMessage(
|
|
97
|
+
{ source: "zappie-checkout", v: 1, session: session.id, type, payload },
|
|
98
|
+
url.origin
|
|
99
|
+
);
|
|
100
|
+
const finish = (result, popped) => {
|
|
101
|
+
abort.abort();
|
|
102
|
+
request = void 0;
|
|
103
|
+
removeEventListener("message", onMessage);
|
|
104
|
+
removeEventListener("popstate", onPop);
|
|
105
|
+
removeEventListener("keydown", onKey);
|
|
106
|
+
vv?.removeEventListener("resize", layout);
|
|
107
|
+
vv?.removeEventListener("scroll", layout);
|
|
108
|
+
root.remove();
|
|
109
|
+
inert.forEach((el) => el.inert = false);
|
|
110
|
+
body.style.cssText = bodyStyle;
|
|
111
|
+
const restore = () => {
|
|
112
|
+
scrollTo(0, y);
|
|
113
|
+
setTimeout(() => {
|
|
114
|
+
scrollTo(0, y);
|
|
115
|
+
history.scrollRestoration = restoration;
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
restore();
|
|
119
|
+
opener?.focus({ preventScroll: true });
|
|
120
|
+
if (!popped && history.state?.[PARAM] === session.id) {
|
|
121
|
+
history.scrollRestoration = "manual";
|
|
122
|
+
addEventListener("popstate", restore, { once: true });
|
|
123
|
+
history.back();
|
|
124
|
+
}
|
|
125
|
+
settle(result);
|
|
126
|
+
};
|
|
127
|
+
const close = () => ready ? post("close_request") : finish({ status: "cancelled", reason: "user" });
|
|
128
|
+
const onKey = (event) => {
|
|
129
|
+
if (event.key === "Escape") {
|
|
130
|
+
close();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
const onPop = () => {
|
|
134
|
+
if (!ready) {
|
|
135
|
+
return finish({ status: "cancelled", reason: "user" }, true);
|
|
136
|
+
}
|
|
137
|
+
history.pushState(
|
|
138
|
+
{ ...history.state, [PARAM]: session.id },
|
|
139
|
+
"",
|
|
140
|
+
location.href
|
|
141
|
+
);
|
|
142
|
+
post("close_request");
|
|
143
|
+
};
|
|
144
|
+
const onMessage = (event) => {
|
|
145
|
+
const data = event.data;
|
|
146
|
+
if (event.origin !== url.origin || event.source !== frame.contentWindow || data?.source !== "zappie-checkout" || data.v !== 1 || data.session !== session.id) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const payload = Object(data.payload);
|
|
150
|
+
if (data.type === "ready" || data.type === "step") {
|
|
151
|
+
if (data.type === "ready") {
|
|
152
|
+
ready = true;
|
|
153
|
+
frame.focus();
|
|
154
|
+
if (options.theme) {
|
|
155
|
+
post("theme", options.theme);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
paying = payload.paying === true;
|
|
159
|
+
if (typeof payload.step === "string") {
|
|
160
|
+
options.onStep?.(payload.step, paying);
|
|
161
|
+
}
|
|
162
|
+
} else if (data.type === "resize" && Number.isFinite(payload.height)) {
|
|
163
|
+
height = payload.height;
|
|
164
|
+
layout();
|
|
165
|
+
} else if (data.type === "completed") {
|
|
166
|
+
checkout.awaitCompletion(session.id, { signal: abort.signal, intervalMs: 1500 }).then(
|
|
167
|
+
(done) => {
|
|
168
|
+
if (done.status === "completed") {
|
|
169
|
+
finished.add(session.id);
|
|
170
|
+
finish({ status: "completed", orderId: done.order_id });
|
|
171
|
+
} else {
|
|
172
|
+
finish({ status: "cancelled", reason: done.status });
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
() => {
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
} else if (data.type === "cancelled" || data.type === "error") {
|
|
179
|
+
finish({
|
|
180
|
+
status: "cancelled",
|
|
181
|
+
reason: String(payload.reason ?? payload.code ?? "user")
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
let start = 0;
|
|
186
|
+
handle.onpointerdown = (event) => {
|
|
187
|
+
if (paying) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
start = event.clientY;
|
|
191
|
+
handle.setPointerCapture?.(event.pointerId);
|
|
192
|
+
};
|
|
193
|
+
handle.onpointermove = (event) => {
|
|
194
|
+
if (start) {
|
|
195
|
+
panel.style.transform = `translateY(${Math.max(0, event.clientY - start)}px)`;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
handle.onpointerup = handle.onpointercancel = (event) => {
|
|
199
|
+
const moved = start ? event.clientY - start : 0;
|
|
200
|
+
start = 0;
|
|
201
|
+
panel.style.transform = "";
|
|
202
|
+
if (!paying && moved > panel.offsetHeight / 4) {
|
|
203
|
+
finish({ status: "cancelled", reason: "user" });
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
history.pushState(
|
|
207
|
+
{ ...history.state, [PARAM]: session.id },
|
|
208
|
+
"",
|
|
209
|
+
(() => {
|
|
210
|
+
const here = new URL(location.href);
|
|
211
|
+
here.searchParams.set(PARAM, session.id);
|
|
212
|
+
return here;
|
|
213
|
+
})()
|
|
214
|
+
);
|
|
215
|
+
addEventListener("message", onMessage);
|
|
216
|
+
addEventListener("popstate", onPop);
|
|
217
|
+
addEventListener("keydown", onKey);
|
|
218
|
+
vv?.addEventListener("resize", layout);
|
|
219
|
+
vv?.addEventListener("scroll", layout);
|
|
220
|
+
inert.forEach((el) => el.inert = true);
|
|
221
|
+
history.scrollRestoration = "manual";
|
|
222
|
+
body.style.cssText += `;position:fixed;top:${-y}px;left:0;right:0;overflow:hidden`;
|
|
223
|
+
body.append(root);
|
|
224
|
+
layout();
|
|
225
|
+
if (!still) {
|
|
226
|
+
panel.animate?.(
|
|
227
|
+
sheet ? [{ transform: "translateY(100%)" }, { transform: "none" }] : [{ opacity: 0 }, { opacity: 1 }],
|
|
228
|
+
{ duration: sheet ? 280 : 160, easing: "cubic-bezier(.2,.8,.2,1)" }
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
request = close;
|
|
232
|
+
return new Promise((resolve) => settle = resolve);
|
|
233
|
+
}
|
|
234
|
+
export {
|
|
235
|
+
checkoutPresentation,
|
|
236
|
+
closeCheckout,
|
|
237
|
+
embedCheckout,
|
|
238
|
+
resumeCheckout
|
|
239
|
+
};
|
|
240
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/checkout/embed.ts"],
|
|
4
|
+
"sourcesContent": ["import type { CheckoutClient } from \"./client\";\nimport { ZappieError } from \"./sdk\";\nimport type { OpenCheckoutSession } from \"./types\";\n\n/** How checkout opens. Auto redirects in in-app browsers and framed pages, else a modal on desktop and a sheet on mobile. */\nexport type CheckoutPresentation =\n \"auto\" | \"modal\" | \"sheet\" | \"fullscreen\" | \"redirect\";\n\n/** The checkout look from toCheckoutTheme in @zappie/tokens/checkout. The checkout validates it before use. */\nexport type CheckoutTheme = {\n version: 1;\n tokens: Partial<Record<`--z-${string}`, string>>;\n logo?: { url: string; alt: string };\n fonts?: { family: string; role: \"body\" | \"display\"; weights?: number[] }[];\n copy?: { payButton?: string; successTitle?: string };\n};\n\n/** Presentation, theme and step reporting for an embedded checkout. */\nexport type EmbedOptions = {\n presentation?: CheckoutPresentation;\n theme?: CheckoutTheme;\n /** Called with each checkout step, and whether a payment has started. */\n onStep?: (step: string, paying: boolean) => void;\n};\n\n/** How an embedded checkout ended. A completed result is confirmed by the server first. */\nexport type CheckoutResult =\n | { status: \"completed\"; orderId: string }\n | { status: \"cancelled\"; reason: string }\n | { status: \"redirected\" };\n\nconst PARAM = \"zappie_checkout\";\nconst finished = new Set<string>();\nlet request: (() => void) | undefined;\nconst media = (query: string) => matchMedia(query).matches;\n\n/** Resolve auto to the presentation this browser gets. */\nexport function checkoutPresentation(\n presentation: CheckoutPresentation = \"auto\",\n): Exclude<CheckoutPresentation, \"auto\"> {\n if (presentation !== \"auto\") {\n return presentation;\n }\n if (\n window.top !== window ||\n /FBA[NV]|Instagram|TikTok|musical_ly|Bytedance|Line\\//.test(\n navigator.userAgent,\n )\n ) {\n return \"redirect\";\n }\n return media(\"(min-width:640px) and (pointer:fine),(min-width:1024px)\")\n ? \"modal\"\n : \"sheet\";\n}\n\n/** Ask the open checkout to close. Mid-payment it asks the shopper first. */\nexport function closeCheckout(): void {\n request?.();\n}\n\n/** On load, drop a stale checkout entry from the address and read its session. It never reopens checkout. */\nexport async function resumeCheckout(\n checkout: CheckoutClient,\n): Promise<CheckoutResult | null> {\n const url = new URL(location.href);\n const id = url.searchParams.get(PARAM);\n if (!id) {\n return null;\n }\n url.searchParams.delete(PARAM);\n history.replaceState(history.state, \"\", url);\n const session = await checkout.session(id);\n return session.status === \"completed\" && session.order_id\n ? { status: \"completed\", orderId: session.order_id }\n : null;\n}\n\n/**\n * Open a checkout session over the page and settle when it ends. Only Zappie's checkout, or one on this machine, is framed. Back and Escape ask the checkout to step back or\n * close. A completed result waits for the server to confirm the session. A completed session can't be reopened.\n */\nexport function embedCheckout(\n checkout: CheckoutClient,\n session: OpenCheckoutSession,\n options: EmbedOptions = {},\n): Promise<CheckoutResult> {\n const url = new URL(session.url);\n if (\n request ||\n session.status !== \"open\" ||\n finished.has(session.id) ||\n !(\n /^https:\\/\\/checkout\\.(stg\\.)?zappie\\.ai$/.test(url.origin) ||\n /^http:\\/\\/(localhost|127\\.0\\.0\\.1)(:\\d+)?$/.test(url.origin)\n )\n ) {\n return Promise.reject(\n new ZappieError(\n \"invalid_request\",\n \"This checkout session can't be opened\",\n ),\n );\n }\n const shown = checkoutPresentation(options.presentation);\n if (shown === \"redirect\") {\n location.assign(session.url);\n return Promise.resolve({ status: \"redirected\" });\n }\n url.pathname = `/embed/${encodeURIComponent(session.id)}`;\n const doc = document;\n const body = doc.body;\n const root = doc.createElement(\"div\");\n const panel = doc.createElement(\"div\");\n const handle = doc.createElement(\"div\");\n const frame = doc.createElement(\"iframe\");\n const sheet = shown === \"sheet\";\n const still = media(\"(prefers-reduced-motion:reduce)\");\n const opener = doc.activeElement as HTMLElement | null;\n const y = scrollY;\n const bodyStyle = body.style.cssText;\n const restoration = history.scrollRestoration;\n const inert = [...body.children].filter(\n (el): el is HTMLElement => !(el as HTMLElement).inert,\n );\n const abort = new AbortController();\n const vv = window.visualViewport;\n let ready = false;\n let paying = false;\n let height = 640;\n let settle: (result: CheckoutResult) => void = () => {};\n\n root.setAttribute(\"data-zappie-checkout\", shown);\n root.setAttribute(\"role\", \"dialog\");\n root.setAttribute(\"aria-modal\", \"true\");\n root.setAttribute(\"aria-label\", \"Checkout\");\n root.style.cssText =\n \"position:fixed;inset:0;z-index:2147483647;background:var(--z-checkout-backdrop,rgb(0 0 0/.5))\";\n panel.style.cssText = `position:absolute;overflow:hidden;display:flex;flex-direction:column;background:var(--z-surface,#fff);${\n shown === \"fullscreen\"\n ? \"inset:0\"\n : sheet\n ? \"left:0;right:0;border-radius:var(--z-checkout-radius,16px) var(--z-checkout-radius,16px) 0 0\"\n : \"inset:0;margin:auto;width:min(480px,100vw - 32px);border-radius:var(--z-checkout-radius,16px)\"\n }`;\n handle.style.cssText =\n \"flex:none;height:24px;touch-action:none;cursor:grab;background:radial-gradient(closest-side,var(--z-border,#ccc) 99%,#0000) 50%/40px 5px no-repeat\";\n frame.title = \"Checkout\";\n frame.src = url.href;\n frame.allow = \"payment 'src'; publickey-credentials-get 'src'\";\n frame.referrerPolicy = \"no-referrer\";\n frame.style.cssText = \"flex:1;width:100%;border:0;display:block\";\n if (sheet) {\n panel.append(handle);\n }\n panel.append(frame);\n root.append(panel);\n\n const layout = () => {\n const view = vv ?? { height: innerHeight, offsetTop: 0 };\n if (sheet) {\n panel.style.height = `${Math.round(view.height * 0.92)}px`;\n panel.style.bottom = `${Math.max(0, innerHeight - view.offsetTop - view.height)}px`;\n } else if (shown === \"modal\") {\n panel.style.height = `${Math.min(height, view.height - 32)}px`;\n }\n };\n const post = (type: string, payload: object = {}) =>\n frame.contentWindow?.postMessage(\n { source: \"zappie-checkout\", v: 1, session: session.id, type, payload },\n url.origin,\n );\n const finish = (result: CheckoutResult, popped?: boolean) => {\n abort.abort();\n request = undefined;\n removeEventListener(\"message\", onMessage);\n removeEventListener(\"popstate\", onPop);\n removeEventListener(\"keydown\", onKey);\n vv?.removeEventListener(\"resize\", layout);\n vv?.removeEventListener(\"scroll\", layout);\n root.remove();\n inert.forEach((el) => (el.inert = false));\n body.style.cssText = bodyStyle;\n // WebKit scrolls the page again after popstate, so the scroll is set once more a task later.\n const restore = () => {\n scrollTo(0, y);\n setTimeout(() => {\n scrollTo(0, y);\n history.scrollRestoration = restoration;\n });\n };\n restore();\n opener?.focus({ preventScroll: true });\n if (!popped && history.state?.[PARAM] === session.id) {\n history.scrollRestoration = \"manual\";\n addEventListener(\"popstate\", restore, { once: true });\n history.back();\n }\n settle(result);\n };\n const close = () =>\n ready\n ? post(\"close_request\")\n : finish({ status: \"cancelled\", reason: \"user\" });\n const onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\") {\n close();\n }\n };\n const onPop = () => {\n if (!ready) {\n return finish({ status: \"cancelled\", reason: \"user\" }, true);\n }\n history.pushState(\n { ...history.state, [PARAM]: session.id },\n \"\",\n location.href,\n );\n post(\"close_request\");\n };\n const onMessage = (event: MessageEvent) => {\n const data = event.data;\n if (\n event.origin !== url.origin ||\n event.source !== frame.contentWindow ||\n data?.source !== \"zappie-checkout\" ||\n data.v !== 1 ||\n data.session !== session.id\n ) {\n return;\n }\n const payload = Object(data.payload);\n if (data.type === \"ready\" || data.type === \"step\") {\n if (data.type === \"ready\") {\n ready = true;\n frame.focus();\n if (options.theme) {\n post(\"theme\", options.theme);\n }\n }\n paying = payload.paying === true;\n if (typeof payload.step === \"string\") {\n options.onStep?.(payload.step, paying);\n }\n } else if (data.type === \"resize\" && Number.isFinite(payload.height)) {\n height = payload.height;\n layout();\n } else if (data.type === \"completed\") {\n checkout\n .awaitCompletion(session.id, { signal: abort.signal, intervalMs: 1500 })\n .then(\n (done) => {\n if (done.status === \"completed\") {\n finished.add(session.id);\n finish({ status: \"completed\", orderId: done.order_id });\n } else {\n finish({ status: \"cancelled\", reason: done.status });\n }\n },\n () => {},\n );\n } else if (data.type === \"cancelled\" || data.type === \"error\") {\n finish({\n status: \"cancelled\",\n reason: String(payload.reason ?? payload.code ?? \"user\"),\n });\n }\n };\n\n let start = 0;\n handle.onpointerdown = (event) => {\n if (paying) {\n return;\n }\n start = event.clientY;\n handle.setPointerCapture?.(event.pointerId);\n };\n handle.onpointermove = (event) => {\n if (start) {\n panel.style.transform = `translateY(${Math.max(0, event.clientY - start)}px)`;\n }\n };\n handle.onpointerup = handle.onpointercancel = (event) => {\n const moved = start ? event.clientY - start : 0;\n start = 0;\n panel.style.transform = \"\";\n if (!paying && moved > panel.offsetHeight / 4) {\n finish({ status: \"cancelled\", reason: \"user\" });\n }\n };\n\n history.pushState(\n { ...history.state, [PARAM]: session.id },\n \"\",\n (() => {\n const here = new URL(location.href);\n here.searchParams.set(PARAM, session.id);\n return here;\n })(),\n );\n addEventListener(\"message\", onMessage);\n addEventListener(\"popstate\", onPop);\n addEventListener(\"keydown\", onKey);\n vv?.addEventListener(\"resize\", layout);\n vv?.addEventListener(\"scroll\", layout);\n inert.forEach((el) => (el.inert = true));\n history.scrollRestoration = \"manual\";\n body.style.cssText += `;position:fixed;top:${-y}px;left:0;right:0;overflow:hidden`;\n body.append(root);\n layout();\n if (!still) {\n panel.animate?.(\n sheet\n ? [{ transform: \"translateY(100%)\" }, { transform: \"none\" }]\n : [{ opacity: 0 }, { opacity: 1 }],\n { duration: sheet ? 280 : 160, easing: \"cubic-bezier(.2,.8,.2,1)\" },\n );\n }\n request = close;\n return new Promise((resolve) => (settle = resolve));\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|