@zappie/sdk 0.1.17 → 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 +6 -0
- 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/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.1.17
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
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
|
+
/** Ask the open checkout to close. Mid-payment it asks the shopper first. */
|
|
137
|
+
declare function closeCheckout(): void;
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Open checkout for a cart and follow it. `open` creates the session, then embeds or redirects it as
|
|
141
|
+
* `presentation` decides, and resolves with how it ended. Retain the idempotency key when retrying a cart.
|
|
142
|
+
*/
|
|
143
|
+
declare function useCheckout(checkout: CheckoutClient): {
|
|
144
|
+
open: (body: CheckoutRequest, key: string, options?: EmbedOptions) => Promise<CheckoutResult>;
|
|
145
|
+
close: typeof closeCheckout;
|
|
146
|
+
status: "open" | "completed" | "cancelled" | "redirected" | "idle" | "opening";
|
|
147
|
+
step: string | undefined;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export { useCheckout };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/checkout/react.ts
|
|
4
|
+
import { useCallback, useState } from "react";
|
|
5
|
+
import {
|
|
6
|
+
closeCheckout,
|
|
7
|
+
embedCheckout
|
|
8
|
+
} from "./embed.js";
|
|
9
|
+
function useCheckout(checkout) {
|
|
10
|
+
const [status, setStatus] = useState("idle");
|
|
11
|
+
const [step, setStep] = useState();
|
|
12
|
+
const open = useCallback(
|
|
13
|
+
async (body, key, options = {}) => {
|
|
14
|
+
setStatus("opening");
|
|
15
|
+
try {
|
|
16
|
+
const session = await checkout.open(body, key);
|
|
17
|
+
setStatus("open");
|
|
18
|
+
const result = await embedCheckout(checkout, session, {
|
|
19
|
+
...options,
|
|
20
|
+
onStep: (next, paying) => {
|
|
21
|
+
setStep(next);
|
|
22
|
+
options.onStep?.(next, paying);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
setStatus(result.status);
|
|
26
|
+
return result;
|
|
27
|
+
} catch (failure) {
|
|
28
|
+
setStatus("idle");
|
|
29
|
+
throw failure;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
[checkout]
|
|
33
|
+
);
|
|
34
|
+
return { open, close: closeCheckout, status, step };
|
|
35
|
+
}
|
|
36
|
+
export {
|
|
37
|
+
useCheckout
|
|
38
|
+
};
|
|
39
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/checkout/react.ts"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport { useCallback, useState } from \"react\";\nimport type { CheckoutClient } from \"./client\";\nimport {\n closeCheckout,\n embedCheckout,\n type CheckoutResult,\n type EmbedOptions,\n} from \"./embed\";\nimport type { CheckoutRequest } from \"./types\";\n\n/**\n * Open checkout for a cart and follow it. `open` creates the session, then embeds or redirects it as\n * `presentation` decides, and resolves with how it ended. Retain the idempotency key when retrying a cart.\n */\nexport function useCheckout(checkout: CheckoutClient) {\n const [status, setStatus] = useState<\n \"idle\" | \"opening\" | \"open\" | CheckoutResult[\"status\"]\n >(\"idle\");\n const [step, setStep] = useState<string>();\n const open = useCallback(\n async (body: CheckoutRequest, key: string, options: EmbedOptions = {}) => {\n setStatus(\"opening\");\n try {\n const session = await checkout.open(body, key);\n setStatus(\"open\");\n const result = await embedCheckout(checkout, session, {\n ...options,\n onStep: (next, paying) => {\n setStep(next);\n options.onStep?.(next, paying);\n },\n });\n setStatus(result.status);\n return result;\n } catch (failure) {\n setStatus(\"idle\");\n throw failure;\n }\n },\n [checkout],\n );\n return { open, close: closeCheckout, status, step };\n}\n"],
|
|
5
|
+
"mappings": ";;;AACA,SAAS,aAAa,gBAAgB;AAEtC;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAOA,SAAS,YAAY,UAA0B;AACpD,QAAM,CAAC,QAAQ,SAAS,IAAI,SAE1B,MAAM;AACR,QAAM,CAAC,MAAM,OAAO,IAAI,SAAiB;AACzC,QAAM,OAAO;AAAA,IACX,OAAO,MAAuB,KAAa,UAAwB,CAAC,MAAM;AACxE,gBAAU,SAAS;AACnB,UAAI;AACF,cAAM,UAAU,MAAM,SAAS,KAAK,MAAM,GAAG;AAC7C,kBAAU,MAAM;AAChB,cAAM,SAAS,MAAM,cAAc,UAAU,SAAS;AAAA,UACpD,GAAG;AAAA,UACH,QAAQ,CAAC,MAAM,WAAW;AACxB,oBAAQ,IAAI;AACZ,oBAAQ,SAAS,MAAM,MAAM;AAAA,UAC/B;AAAA,QACF,CAAC;AACD,kBAAU,OAAO,MAAM;AACvB,eAAO;AAAA,MACT,SAAS,SAAS;AAChB,kBAAU,MAAM;AAChB,cAAM;AAAA,MACR;AAAA,IACF;AAAA,IACA,CAAC,QAAQ;AAAA,EACX;AACA,SAAO,EAAE,MAAM,OAAO,eAAe,QAAQ,KAAK;AACpD;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/debug.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { ZappieError, isZappieError } from "./errors.js";
|
|
|
10
10
|
export * from "./models.js";
|
|
11
11
|
import { format, money } from "./money.js";
|
|
12
12
|
import { RestTransport } from "./transport/rest.js";
|
|
13
|
-
var VERSION = true ? "0.1.
|
|
13
|
+
var VERSION = true ? "0.1.18" : "0.0.0-development";
|
|
14
14
|
export {
|
|
15
15
|
API_VERSION,
|
|
16
16
|
RestTransport,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zappie/sdk",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Typed, framework-agnostic client for Zappie menus, carts, checkout, auth and events.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -49,6 +49,14 @@
|
|
|
49
49
|
"types": "./dist/checkout/index.d.ts",
|
|
50
50
|
"import": "./dist/checkout/index.js"
|
|
51
51
|
},
|
|
52
|
+
"./checkout/embed": {
|
|
53
|
+
"types": "./dist/checkout/embed.d.ts",
|
|
54
|
+
"import": "./dist/checkout/embed.js"
|
|
55
|
+
},
|
|
56
|
+
"./checkout/react": {
|
|
57
|
+
"types": "./dist/checkout/react.d.ts",
|
|
58
|
+
"import": "./dist/checkout/react.js"
|
|
59
|
+
},
|
|
52
60
|
"./checkout-defaults": {
|
|
53
61
|
"types": "./dist/checkout-defaults/index.d.ts",
|
|
54
62
|
"import": "./dist/checkout-defaults/index.js"
|