@hanzo/pay 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ import { PALETTE, REACH, Cancelled } from '../chunk-DXK2QDTX.js';
2
+
3
+ // src/web/sdk.ts
4
+ var PRODUCTION = "https://web.squarecdn.com/v1/square.js";
5
+ var SANDBOX = "https://sandbox.web.squarecdn.com/v1/square.js";
6
+ var pending = null;
7
+ function load(src) {
8
+ if (pending) return pending;
9
+ if (typeof window !== "undefined" && window.Square) return pending = Promise.resolve();
10
+ pending = new Promise((resolve, reject) => {
11
+ const existing = document.querySelector(`script[src="${src}"]`);
12
+ if (existing) {
13
+ existing.addEventListener("load", () => resolve());
14
+ existing.addEventListener("error", () => reject(new Error("Square SDK failed to load")));
15
+ return;
16
+ }
17
+ const s = document.createElement("script");
18
+ s.src = src;
19
+ s.async = true;
20
+ s.onload = () => resolve();
21
+ s.onerror = () => reject(new Error("Square SDK failed to load"));
22
+ document.head.appendChild(s);
23
+ });
24
+ return pending;
25
+ }
26
+ function forget() {
27
+ pending = null;
28
+ }
29
+
30
+ // src/web/style.ts
31
+ function rules(style2, selector) {
32
+ return style2[selector];
33
+ }
34
+ var RADIUS = "6px";
35
+ function style(p) {
36
+ return {
37
+ // Borders and radius ONLY. Square's allowlist for this selector is
38
+ // borderColor/borderRadius/borderWidth; anything else throws "Invalid style
39
+ // property", and Square rejects the WHOLE style object when it does — so one
40
+ // stray property leaves the card unstyled and white.
41
+ ".input-container": {
42
+ borderColor: p.border,
43
+ borderRadius: RADIUS,
44
+ borderWidth: "1px"
45
+ },
46
+ ".input-container.is-focus": { borderColor: p.borderFocus },
47
+ ".input-container.is-error": { borderColor: p.error },
48
+ input: {
49
+ backgroundColor: p.field,
50
+ color: p.text,
51
+ fontSize: "14px"
52
+ },
53
+ // 16px on a small screen, because iOS Safari ZOOMS the page when a field
54
+ // under 16px takes focus — on a card form that throws the layout sideways
55
+ // mid-number and there is no way to zoom back without losing the caret.
56
+ // Square's own dark-mode example carries this media query for the same
57
+ // reason; it is the one place the field may disagree with the page's ramp.
58
+ "@media screen and (max-width: 600px)": {
59
+ input: { fontSize: "16px" }
60
+ },
61
+ "input::placeholder": { color: p.placeholder },
62
+ "input.is-error": { color: p.error },
63
+ ".message-text": { color: p.placeholder },
64
+ ".message-text.is-error": { color: p.error },
65
+ ".message-icon": { color: p.placeholder },
66
+ ".message-icon.is-error": { color: p.error }
67
+ };
68
+ }
69
+ function pin(doc) {
70
+ const d = doc ?? (typeof document === "undefined" ? void 0 : document);
71
+ if (!d) return;
72
+ const MARK = "hanzo-pay-scheme";
73
+ if (d.getElementById(MARK)) return;
74
+ const el = d.createElement("style");
75
+ el.id = MARK;
76
+ el.textContent = ".sq-card-iframe-container{color-scheme:auto}";
77
+ d.head.appendChild(el);
78
+ }
79
+
80
+ // src/web/terminal.ts
81
+ var DRAWN = ["card", "gift", "google_pay", "cash_app"];
82
+ function token(method, r) {
83
+ if (String(r.status).toUpperCase() === "CANCEL") throw new Cancelled(method);
84
+ if (r.status !== "OK" || !r.token) {
85
+ throw new Error(r.errors?.[0]?.message ?? `${method} could not be completed`);
86
+ }
87
+ return { value: r.token, method, card: r.details?.card };
88
+ }
89
+ function awaited(method, t) {
90
+ return new Promise((resolve, reject) => {
91
+ t.addEventListener?.("ontokenization", (e) => {
92
+ const detail = e?.detail;
93
+ if (detail?.error) {
94
+ reject(new Error(`${method} could not be completed`));
95
+ return;
96
+ }
97
+ if (!detail?.tokenResult) return;
98
+ try {
99
+ resolve(token(method, detail.tokenResult));
100
+ } catch (err) {
101
+ reject(err);
102
+ }
103
+ });
104
+ });
105
+ }
106
+ var Web = class {
107
+ constructor(payments, palette) {
108
+ this.payments = payments;
109
+ this.palette = palette;
110
+ }
111
+ payments;
112
+ built = /* @__PURE__ */ new Map();
113
+ palette;
114
+ /**
115
+ * Which rails can REALLY pay here, asked of the SDK one at a time.
116
+ *
117
+ * Building the rail IS the probe, and it is the only answer that accounts for
118
+ * the browser, the device, the buyer's saved cards AND whether the merchant
119
+ * account has the rail switched on. A rejection is a plain "not here", not an
120
+ * error worth showing anyone.
121
+ *
122
+ * The built objects are KEPT, and that is load-bearing rather than a cache:
123
+ * `collect('apple_pay')` must reach `tokenize()` with nothing awaited in front
124
+ * of it, so the object it needs has to already exist by then.
125
+ */
126
+ async offers(tender) {
127
+ const out = [];
128
+ for (const method of REACH.web) {
129
+ try {
130
+ this.built.set(method, await this.build(method, tender));
131
+ out.push(method);
132
+ } catch {
133
+ }
134
+ }
135
+ return out;
136
+ }
137
+ async build(method, tender) {
138
+ if (method === "card") return this.payments.card({ style: style(this.palette) });
139
+ if (method === "gift") return this.payments.giftCard();
140
+ if (method === "ach") {
141
+ return this.payments.ach({
142
+ // Square REJECTS a redirectURI carrying a query string, so the page's own
143
+ // path is used and anything that must survive the trip travels separately.
144
+ redirectURI: typeof window === "undefined" ? "" : window.location.origin + window.location.pathname,
145
+ transactionId: `${Date.now()}`
146
+ });
147
+ }
148
+ const req = this.payments.paymentRequest({
149
+ countryCode: tender.country,
150
+ currencyCode: tender.total.currency,
151
+ total: { amount: tender.total.amount, label: tender.label }
152
+ });
153
+ if (method === "apple_pay") return this.payments.applePay(req);
154
+ if (method === "google_pay") return this.payments.googlePay(req);
155
+ return this.payments.cashAppPay(req, {
156
+ // Where Cash App returns a MOBILE buyer. Desktop uses the QR and never
157
+ // leaves the page.
158
+ redirectURL: typeof window === "undefined" ? "" : window.location.href,
159
+ referenceId: `pay-${tender.total.amount}`
160
+ });
161
+ }
162
+ /** Draw a rail that renders inline. A rail that draws nothing is a no-op, not an error. */
163
+ async mount(method, target) {
164
+ const t = this.built.get(method);
165
+ if (!t) throw new Error(`${method} was not offered here`);
166
+ if (!DRAWN.includes(method)) return;
167
+ await t.attach?.(target);
168
+ }
169
+ /**
170
+ * Tokenize.
171
+ *
172
+ * NOTHING IS AWAITED BEFORE `tokenize()`. Apple refuses a payment sheet that
173
+ * was not opened directly by the gesture that asked for it, so a single `await`
174
+ * placed above the call — loading the SDK, looking a rail up asynchronously,
175
+ * re-reading a total — silently breaks Apple Pay and nothing else. That is why
176
+ * `offers()` builds every rail up front and this only reads a map.
177
+ */
178
+ collect(method, tender, detail) {
179
+ const t = this.built.get(method);
180
+ if (!t) return Promise.reject(new Error(`${method} was not offered here`));
181
+ if (method === "cash_app") return awaited(method, t);
182
+ if (method === "ach") {
183
+ if (!detail?.name) {
184
+ return Promise.reject(new Error("ACH needs the account holder\u2019s name"));
185
+ }
186
+ const arrived = awaited(method, t);
187
+ void t.tokenize({
188
+ accountHolderName: detail.name,
189
+ intent: "CHARGE",
190
+ amount: tender.total.amount,
191
+ currency: tender.total.currency
192
+ });
193
+ return arrived;
194
+ }
195
+ return t.tokenize(detail?.verify).then((r) => token(method, r));
196
+ }
197
+ async release() {
198
+ await Promise.all([...this.built.values()].map((t) => t.destroy?.().catch(() => void 0)));
199
+ this.built.clear();
200
+ }
201
+ };
202
+ async function terminal(config) {
203
+ if (!config.applicationId || !config.locationId) {
204
+ throw new Error("Square is not configured for this deployment");
205
+ }
206
+ pin();
207
+ const src = (config.environment ?? "production").toLowerCase() === "sandbox" ? SANDBOX : PRODUCTION;
208
+ await load(src);
209
+ if (typeof window === "undefined" || !window.Square) {
210
+ throw new Error("Square SDK failed to load");
211
+ }
212
+ const payments = await window.Square.payments(config.applicationId, config.locationId);
213
+ return new Web(payments, config.palette ?? PALETTE.dark);
214
+ }
215
+
216
+ export { PRODUCTION, SANDBOX, forget, load, pin, rules, style, terminal };
217
+ //# sourceMappingURL=index.js.map
218
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/web/sdk.ts","../../src/web/style.ts","../../src/web/terminal.ts"],"names":["style"],"mappings":"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","file":"index.js","sourcesContent":["// The Square Web Payments SDK, as much of it as we actually call, typed.\n//\n// Square ships no types for the global it installs, so this is the contract we\n// hold it to. Every member below is exercised by this package; nothing is\n// declared speculatively.\n\n/** What `tokenize()` resolves with. `status` is `'OK'` | `'CANCEL'` | an error. */\nexport interface Result {\n status: string\n token?: string\n details?: {\n card?: { brand?: string; last4?: string; expMonth?: number; expYear?: number }\n }\n errors?: Array<{ message: string }>\n}\n\nexport interface Tokenizer {\n tokenize(options?: Record<string, unknown>): Promise<Result>\n /**\n * Cash App Pay and ACH deliver their token on an `ontokenization` EVENT rather\n * than from tokenize()'s return — the buyer leaves the page (for Cash App, or\n * for their bank's login) and comes back. A caller that only reads the return\n * value gets nothing from either, silently, on a payment that succeeded.\n */\n addEventListener?(event: string, handler: (e: unknown) => void): void\n /**\n * Optional because a WALLET may draw itself (Apple Pay does) rather than\n * attach. The second argument is Cash App Pay's button options\n * (`{ shape, width }`); every other rail ignores it.\n */\n attach?(selector: string, options?: Record<string, unknown>): Promise<void>\n destroy?(): Promise<void>\n}\n\n/**\n * The CARD element always attaches and always destroys — it owns an iframe. Kept\n * distinct from the wallet tokenizer so the card path is not forced to\n * optional-chain calls that can never be absent.\n */\nexport interface Element extends Tokenizer {\n attach(selector: string): Promise<void>\n destroy(): Promise<void>\n}\n\nexport interface Request {\n countryCode: string\n currencyCode: string\n total: { amount: string; label: string }\n}\n\nexport interface Payments {\n card(options?: Record<string, unknown>): Promise<Element>\n /**\n * A Square gift card. Its own constructor and its own container — a gift card\n * is a balance instrument, not a credit line, so Square keeps the two elements\n * separate rather than switching one on a brand.\n *\n * It attaches and tokenizes exactly like a card, with one difference that\n * matters to a caller: its `tokenize()` takes NO verification details. SCA\n * covers cards, and a gift card has no issuer to challenge.\n */\n giftCard(options?: Record<string, unknown>): Promise<Element>\n paymentRequest(req: Request): unknown\n applePay(req: unknown): Promise<Tokenizer>\n googlePay(req: unknown): Promise<Tokenizer>\n cashAppPay(req: unknown, opts: Record<string, unknown>): Promise<Tokenizer>\n /**\n * ACH bank transfer. Square runs PLAID'S instant bank authentication itself,\n * so this needs no Plaid account of ours — which is why the rail sat marked\n * \"coming soon\" for want of an integration that was already included. US-only.\n *\n * THE ARGUMENT IS OPTIONAL BECAUSE SQUARE ACCEPTS TWO SHAPES, and a type that\n * admitted only one would reject working code. The redirect form\n * (`ach({ redirectURI, transactionId })`, what this package calls and what\n * hanzoai/pay has run in production) sends the buyer to their bank and back;\n * Square's own quickstart instead calls `ach()` bare and passes\n * `{ accountHolderName, intent, amount, currency }` to `tokenize()`. Both\n * deliver the token the same way — on the event, never from the return.\n */\n ach(opts?: { redirectURI?: string; transactionId?: string }): Promise<Tokenizer>\n}\n\ninterface SDK {\n payments(appId: string, locationId: string): Promise<Payments>\n}\n\ndeclare global {\n interface Window {\n Square?: SDK\n }\n}\n\nexport const PRODUCTION = 'https://web.squarecdn.com/v1/square.js'\nexport const SANDBOX = 'https://sandbox.web.squarecdn.com/v1/square.js'\n\nlet pending: Promise<void> | null = null\n\n/** Load the SDK once per page, whichever surface asks first. */\nexport function load(src: string): Promise<void> {\n if (pending) return pending\n if (typeof window !== 'undefined' && window.Square) return (pending = Promise.resolve())\n pending = new Promise((resolve, reject) => {\n const existing = document.querySelector(`script[src=\"${src}\"]`)\n if (existing) {\n existing.addEventListener('load', () => resolve())\n existing.addEventListener('error', () => reject(new Error('Square SDK failed to load')))\n return\n }\n const s = document.createElement('script')\n s.src = src\n s.async = true\n s.onload = () => resolve()\n s.onerror = () => reject(new Error('Square SDK failed to load'))\n document.head.appendChild(s)\n })\n return pending\n}\n\n/** Drop the memoized load. For tests only — a page loads the SDK once. */\nexport function forget(): void {\n pending = null\n}\n","// A `Palette` rendered as the style object `payments.card({ style })` accepts.\n//\n// Measured against the live SDK, not read off a doc page. Where a comment says\n// \"confirmed\", it was driven and inspected.\n\nimport type { Palette } from '../palette'\n\n/** A selector's CSS property bag. */\nexport type Rules = Record<string, string>\n\n/**\n * What `payments.card({ style })` accepts: selector -> properties, plus `@media`\n * keys whose value is a nested block of the same. Square resolves the media query\n * INSIDE the iframe, which is the only way in — the frame cannot see the page's\n * own breakpoints.\n */\nexport type Style = Record<string, Rules | Record<string, Rules>>\n\n/**\n * One selector's properties.\n *\n * A `@media` key holds a nested block, so the map's value type is a union and\n * every read of a plain selector would otherwise need its own cast. This is that\n * cast, written once, where the shape is known.\n */\nexport function rules(style: Style, selector: string): Rules {\n return style[selector] as Rules\n}\n\n/** The input/button radius of @hanzo/design's ramp (--radius-sm), in the px Square expects. */\nconst RADIUS = '6px'\n\n/**\n * The style object for a card element painted in `p`.\n *\n * THE GROUND AND THE INK ARE ONE PALETTE, and that is the whole point. The field\n * shipped as `input.backgroundColor: 'transparent'` beside `input.color:\n * '#fafafa'` — so Square painted near-white digits onto its own default white\n * iframe and the customer's card number was invisible as they typed it. Two\n * properties, set in two places, describing one surface. Reading them off one\n * `Palette` is what stops them disagreeing again; `legible()` proves they have not.\n *\n * `input.backgroundColor` IS the supported way in, and it is not a workaround.\n * Square maps that one property onto the container behind the iframe rather than\n * onto the input:\n *\n * selectorPropertyMappings[input] = [{ property: 'backgroundColor',\n * toSelectors: ['#<id>.sq-card-wrapper .sq-card-iframe-container'] }]\n *\n * Measured, not read: driving build 1.84.0 with the object below and inspecting\n * the result gives `.sq-card-iframe-container { background-color: #0a0a0a }` in\n * the parent document, with `<body>`, `<html>` and all four inputs inside the\n * cross-origin frame computing to `rgba(0,0,0,0)`. The frame is transparent by\n * design and the container is the surface — so painting the container IS painting\n * the field.\n *\n * AND IT IS STILL NOT ENOUGH ON ITS OWN — see `pin()` below, which this package\n * applies for you. The declaration above lands and is then overpainted, so a\n * style object shipped without that rule is accepted and silently ignored. That\n * gap was misread once as an SDK bug to wait out; it was ours.\n *\n * `.input-container { backgroundColor }` is ALSO accepted on 1.84.0 (it throws\n * nothing). It is still not set: it would be a second way to paint the one\n * surface `input.backgroundColor` already paints, and the two could then\n * disagree. One property, one surface.\n *\n * `fontFamily` is deliberately absent: Square validates it against its own\n * loadable list and throws on both CSS-wide stacks and arbitrary names, which\n * blocks the iframe from attaching at all. Its default is a system sans, which is\n * what --font-sans resolves to anyway.\n */\nexport function style(p: Palette): Style {\n return {\n // Borders and radius ONLY. Square's allowlist for this selector is\n // borderColor/borderRadius/borderWidth; anything else throws \"Invalid style\n // property\", and Square rejects the WHOLE style object when it does — so one\n // stray property leaves the card unstyled and white.\n '.input-container': {\n borderColor: p.border,\n borderRadius: RADIUS,\n borderWidth: '1px',\n },\n '.input-container.is-focus': { borderColor: p.borderFocus },\n '.input-container.is-error': { borderColor: p.error },\n input: {\n backgroundColor: p.field,\n color: p.text,\n fontSize: '14px',\n },\n // 16px on a small screen, because iOS Safari ZOOMS the page when a field\n // under 16px takes focus — on a card form that throws the layout sideways\n // mid-number and there is no way to zoom back without losing the caret.\n // Square's own dark-mode example carries this media query for the same\n // reason; it is the one place the field may disagree with the page's ramp.\n '@media screen and (max-width: 600px)': {\n input: { fontSize: '16px' },\n },\n 'input::placeholder': { color: p.placeholder },\n 'input.is-error': { color: p.error },\n '.message-text': { color: p.placeholder },\n '.message-text.is-error': { color: p.error },\n '.message-icon': { color: p.placeholder },\n '.message-icon.is-error': { color: p.error },\n }\n}\n\n/**\n * The rule without which everything above is decoration.\n *\n * Square's `input.backgroundColor` is accepted and then IGNORED: the field\n * renders white on a black checkout no matter what is passed. The cause is not\n * Square's, and it is one property. A page that sets `color-scheme` on\n * `<html>` — which is the correct thing to do, and what every themed app does so\n * the UA paints scrollbars and form controls to match — leaks it INTO the\n * cross-origin iframe, because `color-scheme` inherits. Inside the frame the UA\n * then paints form controls on its own scheme background, over anything the SDK\n * declared. Pinning the container back to `auto` lets our ground through.\n *\n * (A Square forum user reported the same thing after two years of the docs' own\n * recipe not working. Same fix, arrived at from the other end.)\n *\n * THIS IS APPLIED FOR YOU, by `terminal()`, before any card can attach — it must\n * be in the stylesheet BEFORE the element mounts, because the container is styled\n * as the card attaches, and a rule added afterwards is a rule that arrived too\n * late. It is not left to a host to remember, and it is not a stylesheet a host\n * can forget to import: this package produced the style object, so this package\n * owes the one rule that makes it mean anything.\n *\n * Idempotent, and a no-op without a document. A host that already ships the rule\n * (hanzoai/pay does, in `index.css`) gets an identical declaration — same\n * property, same value — so there is nothing for the two to disagree about.\n */\nexport function pin(doc?: Document): void {\n const d = doc ?? (typeof document === 'undefined' ? undefined : document)\n if (!d) return\n const MARK = 'hanzo-pay-scheme'\n if (d.getElementById(MARK)) return\n const el = d.createElement('style')\n el.id = MARK\n el.textContent = '.sq-card-iframe-container{color-scheme:auto}'\n d.head.appendChild(el)\n}\n","// The Square Web Payments SDK behind the one `Terminal` interface.\n//\n// Every rail here mints the SAME single-use token, bound for the same server\n// call — so none of them needed server work beyond the one that already existed.\n// What they do NOT share is how the token arrives, and treating them as one shape\n// is a silent failure in three of the six:\n//\n// card, gift attach() into a container, then tokenize() returns the token.\n// google_pay attach() into a container Square draws its button in, then\n// tokenize() from our click.\n// apple_pay NO attach() — Square's docs are explicit. The host renders the\n// button (Apple's own CSS appearance), and tokenize() must be\n// called IMMEDIATELY in the click handler.\n// cash_app attach(), and then the token arrives on an `ontokenization`\n// EVENT after the buyer approves in the app or by QR. Calling\n// tokenize() ourselves does nothing and the payment never lands.\n// ach NO attach() — the flow IS tokenize(). The token likewise arrives\n// on `ontokenization`, because the buyer leaves for their bank.\n\nimport {\n type Detail,\n type Method,\n type Tender,\n type Terminal,\n type Token,\n Cancelled,\n REACH,\n} from '../terminal'\nimport { type Palette, PALETTE } from '../palette'\nimport { load, PRODUCTION, SANDBOX, type Payments, type Result, type Tokenizer } from './sdk'\nimport { pin, style } from './style'\n\n/** The org's PUBLIC Square config, exactly as commerce publishes it. */\nexport interface Config {\n applicationId: string\n locationId: string\n /** `'sandbox'` tokenizes against Square's sandbox. Anything else is production. */\n environment?: string\n /** The card field's colours. Defaults to @hanzo/design's dark table. */\n palette?: Palette\n}\n\n/** Rails that draw themselves into a container the host positions. */\nconst DRAWN: readonly Method[] = ['card', 'gift', 'google_pay', 'cash_app']\n\nfunction token(method: Method, r: Result): Token {\n // CANCEL is the buyer changing their mind. It is not a failure and must never\n // be shown as one.\n if (String(r.status).toUpperCase() === 'CANCEL') throw new Cancelled(method)\n if (r.status !== 'OK' || !r.token) {\n throw new Error(r.errors?.[0]?.message ?? `${method} could not be completed`)\n }\n return { value: r.token, method, card: r.details?.card }\n}\n\n/**\n * The token that arrives on an EVENT rather than from a return — Cash App Pay\n * and ACH, for the same underlying reason: the buyer leaves the page.\n *\n * One listener per collect(), removed when it settles. Square's own quickstart\n * registers a fresh listener on every submission and never removes one, so a\n * buyer who retries gets their token delivered to every previous attempt too.\n */\nfunction awaited(method: Method, t: Tokenizer): Promise<Token> {\n return new Promise<Token>((resolve, reject) => {\n t.addEventListener?.('ontokenization', (e: unknown) => {\n const detail = (e as { detail?: { tokenResult?: Result; error?: unknown } })?.detail\n if (detail?.error) {\n reject(new Error(`${method} could not be completed`))\n return\n }\n if (!detail?.tokenResult) return\n try {\n resolve(token(method, detail.tokenResult))\n } catch (err) {\n reject(err)\n }\n })\n })\n}\n\nclass Web implements Terminal {\n private readonly built = new Map<Method, Tokenizer>()\n private readonly palette: Palette\n\n constructor(\n private readonly payments: Payments,\n palette: Palette,\n ) {\n this.palette = palette\n }\n\n /**\n * Which rails can REALLY pay here, asked of the SDK one at a time.\n *\n * Building the rail IS the probe, and it is the only answer that accounts for\n * the browser, the device, the buyer's saved cards AND whether the merchant\n * account has the rail switched on. A rejection is a plain \"not here\", not an\n * error worth showing anyone.\n *\n * The built objects are KEPT, and that is load-bearing rather than a cache:\n * `collect('apple_pay')` must reach `tokenize()` with nothing awaited in front\n * of it, so the object it needs has to already exist by then.\n */\n async offers(tender: Tender): Promise<Method[]> {\n const out: Method[] = []\n for (const method of REACH.web) {\n try {\n this.built.set(method, await this.build(method, tender))\n out.push(method)\n } catch {\n // Not offerable here. Say nothing and show nothing.\n }\n }\n return out\n }\n\n private async build(method: Method, tender: Tender): Promise<Tokenizer> {\n if (method === 'card') return this.payments.card({ style: style(this.palette) })\n if (method === 'gift') return this.payments.giftCard()\n if (method === 'ach') {\n return this.payments.ach({\n // Square REJECTS a redirectURI carrying a query string, so the page's own\n // path is used and anything that must survive the trip travels separately.\n redirectURI:\n typeof window === 'undefined'\n ? ''\n : window.location.origin + window.location.pathname,\n transactionId: `${Date.now()}`,\n })\n }\n // A FRESH paymentRequest per wallet: Square binds it into the object it\n // builds, so two wallets sharing one request is one wallet quoting the\n // other's total.\n const req = this.payments.paymentRequest({\n countryCode: tender.country,\n currencyCode: tender.total.currency,\n total: { amount: tender.total.amount, label: tender.label },\n })\n if (method === 'apple_pay') return this.payments.applePay(req)\n if (method === 'google_pay') return this.payments.googlePay(req)\n return this.payments.cashAppPay(req, {\n // Where Cash App returns a MOBILE buyer. Desktop uses the QR and never\n // leaves the page.\n redirectURL: typeof window === 'undefined' ? '' : window.location.href,\n referenceId: `pay-${tender.total.amount}`,\n })\n }\n\n /** Draw a rail that renders inline. A rail that draws nothing is a no-op, not an error. */\n async mount(method: Method, target: string): Promise<void> {\n const t = this.built.get(method)\n if (!t) throw new Error(`${method} was not offered here`)\n if (!DRAWN.includes(method)) return\n await t.attach?.(target)\n }\n\n /**\n * Tokenize.\n *\n * NOTHING IS AWAITED BEFORE `tokenize()`. Apple refuses a payment sheet that\n * was not opened directly by the gesture that asked for it, so a single `await`\n * placed above the call — loading the SDK, looking a rail up asynchronously,\n * re-reading a total — silently breaks Apple Pay and nothing else. That is why\n * `offers()` builds every rail up front and this only reads a map.\n */\n collect(method: Method, tender: Tender, detail?: Detail): Promise<Token> {\n const t = this.built.get(method)\n if (!t) return Promise.reject(new Error(`${method} was not offered here`))\n\n // Cash App draws its own button and delivers on the event; a tokenize() call\n // here does nothing at all.\n if (method === 'cash_app') return awaited(method, t)\n\n if (method === 'ach') {\n if (!detail?.name) {\n return Promise.reject(new Error('ACH needs the account holder’s name'))\n }\n const arrived = awaited(method, t)\n // The flow IS tokenize() for ACH — it opens the bank login — but the token\n // comes back on the event, so the return value is deliberately dropped.\n void t.tokenize({\n accountHolderName: detail.name,\n intent: 'CHARGE',\n amount: tender.total.amount,\n currency: tender.total.currency,\n })\n return arrived\n }\n\n return t.tokenize(detail?.verify).then((r) => token(method, r))\n }\n\n async release(): Promise<void> {\n // Square's `destroy()` empties the mount node when it resolves, so a create\n // that overlaps a destroy gets its fresh iframe swept away by the old\n // element's cleanup — the form then sits empty forever with no error to\n // explain it. Callers await this before building the next terminal, which is\n // what makes a theme change survivable.\n await Promise.all([...this.built.values()].map((t) => t.destroy?.().catch(() => undefined)))\n this.built.clear()\n }\n}\n\n/**\n * A terminal for this browser. Loads the SDK once per page, whichever surface\n * asks first.\n *\n * The palette is fixed at construction because SQUARE HAS NO API TO RESTYLE A\n * LIVE CARD — the fields are a cross-origin iframe, told their colours once, at\n * creation. Following a theme change therefore means `release()` then a new\n * terminal, and making the palette a constructor argument is what forces that to\n * happen by construction rather than by remembering to.\n */\nexport async function terminal(config: Config): Promise<Terminal> {\n if (!config.applicationId || !config.locationId) {\n throw new Error('Square is not configured for this deployment')\n }\n // BEFORE the SDK loads, let alone attaches. The container is styled as the\n // card mounts, so this rule has to already be in the sheet by then — see\n // `pin()`. Doing it here is what makes it impossible to ship the style object\n // without the one rule that makes it visible.\n pin()\n const src = (config.environment ?? 'production').toLowerCase() === 'sandbox' ? SANDBOX : PRODUCTION\n await load(src)\n if (typeof window === 'undefined' || !window.Square) {\n throw new Error('Square SDK failed to load')\n }\n const payments = await window.Square.payments(config.applicationId, config.locationId)\n return new Web(payments, config.palette ?? PALETTE.dark)\n}\n"]}
@@ -0,0 +1,99 @@
1
+ /** What `tokenize()` resolves with. `status` is `'OK'` | `'CANCEL'` | an error. */
2
+ export interface Result {
3
+ status: string;
4
+ token?: string;
5
+ details?: {
6
+ card?: {
7
+ brand?: string;
8
+ last4?: string;
9
+ expMonth?: number;
10
+ expYear?: number;
11
+ };
12
+ };
13
+ errors?: Array<{
14
+ message: string;
15
+ }>;
16
+ }
17
+ export interface Tokenizer {
18
+ tokenize(options?: Record<string, unknown>): Promise<Result>;
19
+ /**
20
+ * Cash App Pay and ACH deliver their token on an `ontokenization` EVENT rather
21
+ * than from tokenize()'s return — the buyer leaves the page (for Cash App, or
22
+ * for their bank's login) and comes back. A caller that only reads the return
23
+ * value gets nothing from either, silently, on a payment that succeeded.
24
+ */
25
+ addEventListener?(event: string, handler: (e: unknown) => void): void;
26
+ /**
27
+ * Optional because a WALLET may draw itself (Apple Pay does) rather than
28
+ * attach. The second argument is Cash App Pay's button options
29
+ * (`{ shape, width }`); every other rail ignores it.
30
+ */
31
+ attach?(selector: string, options?: Record<string, unknown>): Promise<void>;
32
+ destroy?(): Promise<void>;
33
+ }
34
+ /**
35
+ * The CARD element always attaches and always destroys — it owns an iframe. Kept
36
+ * distinct from the wallet tokenizer so the card path is not forced to
37
+ * optional-chain calls that can never be absent.
38
+ */
39
+ export interface Element extends Tokenizer {
40
+ attach(selector: string): Promise<void>;
41
+ destroy(): Promise<void>;
42
+ }
43
+ export interface Request {
44
+ countryCode: string;
45
+ currencyCode: string;
46
+ total: {
47
+ amount: string;
48
+ label: string;
49
+ };
50
+ }
51
+ export interface Payments {
52
+ card(options?: Record<string, unknown>): Promise<Element>;
53
+ /**
54
+ * A Square gift card. Its own constructor and its own container — a gift card
55
+ * is a balance instrument, not a credit line, so Square keeps the two elements
56
+ * separate rather than switching one on a brand.
57
+ *
58
+ * It attaches and tokenizes exactly like a card, with one difference that
59
+ * matters to a caller: its `tokenize()` takes NO verification details. SCA
60
+ * covers cards, and a gift card has no issuer to challenge.
61
+ */
62
+ giftCard(options?: Record<string, unknown>): Promise<Element>;
63
+ paymentRequest(req: Request): unknown;
64
+ applePay(req: unknown): Promise<Tokenizer>;
65
+ googlePay(req: unknown): Promise<Tokenizer>;
66
+ cashAppPay(req: unknown, opts: Record<string, unknown>): Promise<Tokenizer>;
67
+ /**
68
+ * ACH bank transfer. Square runs PLAID'S instant bank authentication itself,
69
+ * so this needs no Plaid account of ours — which is why the rail sat marked
70
+ * "coming soon" for want of an integration that was already included. US-only.
71
+ *
72
+ * THE ARGUMENT IS OPTIONAL BECAUSE SQUARE ACCEPTS TWO SHAPES, and a type that
73
+ * admitted only one would reject working code. The redirect form
74
+ * (`ach({ redirectURI, transactionId })`, what this package calls and what
75
+ * hanzoai/pay has run in production) sends the buyer to their bank and back;
76
+ * Square's own quickstart instead calls `ach()` bare and passes
77
+ * `{ accountHolderName, intent, amount, currency }` to `tokenize()`. Both
78
+ * deliver the token the same way — on the event, never from the return.
79
+ */
80
+ ach(opts?: {
81
+ redirectURI?: string;
82
+ transactionId?: string;
83
+ }): Promise<Tokenizer>;
84
+ }
85
+ interface SDK {
86
+ payments(appId: string, locationId: string): Promise<Payments>;
87
+ }
88
+ declare global {
89
+ interface Window {
90
+ Square?: SDK;
91
+ }
92
+ }
93
+ export declare const PRODUCTION = "https://web.squarecdn.com/v1/square.js";
94
+ export declare const SANDBOX = "https://sandbox.web.squarecdn.com/v1/square.js";
95
+ /** Load the SDK once per page, whichever surface asks first. */
96
+ export declare function load(src: string): Promise<void>;
97
+ /** Drop the memoized load. For tests only — a page loads the SDK once. */
98
+ export declare function forget(): void;
99
+ export {};
@@ -0,0 +1,85 @@
1
+ import type { Palette } from '../palette';
2
+ /** A selector's CSS property bag. */
3
+ export type Rules = Record<string, string>;
4
+ /**
5
+ * What `payments.card({ style })` accepts: selector -> properties, plus `@media`
6
+ * keys whose value is a nested block of the same. Square resolves the media query
7
+ * INSIDE the iframe, which is the only way in — the frame cannot see the page's
8
+ * own breakpoints.
9
+ */
10
+ export type Style = Record<string, Rules | Record<string, Rules>>;
11
+ /**
12
+ * One selector's properties.
13
+ *
14
+ * A `@media` key holds a nested block, so the map's value type is a union and
15
+ * every read of a plain selector would otherwise need its own cast. This is that
16
+ * cast, written once, where the shape is known.
17
+ */
18
+ export declare function rules(style: Style, selector: string): Rules;
19
+ /**
20
+ * The style object for a card element painted in `p`.
21
+ *
22
+ * THE GROUND AND THE INK ARE ONE PALETTE, and that is the whole point. The field
23
+ * shipped as `input.backgroundColor: 'transparent'` beside `input.color:
24
+ * '#fafafa'` — so Square painted near-white digits onto its own default white
25
+ * iframe and the customer's card number was invisible as they typed it. Two
26
+ * properties, set in two places, describing one surface. Reading them off one
27
+ * `Palette` is what stops them disagreeing again; `legible()` proves they have not.
28
+ *
29
+ * `input.backgroundColor` IS the supported way in, and it is not a workaround.
30
+ * Square maps that one property onto the container behind the iframe rather than
31
+ * onto the input:
32
+ *
33
+ * selectorPropertyMappings[input] = [{ property: 'backgroundColor',
34
+ * toSelectors: ['#<id>.sq-card-wrapper .sq-card-iframe-container'] }]
35
+ *
36
+ * Measured, not read: driving build 1.84.0 with the object below and inspecting
37
+ * the result gives `.sq-card-iframe-container { background-color: #0a0a0a }` in
38
+ * the parent document, with `<body>`, `<html>` and all four inputs inside the
39
+ * cross-origin frame computing to `rgba(0,0,0,0)`. The frame is transparent by
40
+ * design and the container is the surface — so painting the container IS painting
41
+ * the field.
42
+ *
43
+ * AND IT IS STILL NOT ENOUGH ON ITS OWN — see `pin()` below, which this package
44
+ * applies for you. The declaration above lands and is then overpainted, so a
45
+ * style object shipped without that rule is accepted and silently ignored. That
46
+ * gap was misread once as an SDK bug to wait out; it was ours.
47
+ *
48
+ * `.input-container { backgroundColor }` is ALSO accepted on 1.84.0 (it throws
49
+ * nothing). It is still not set: it would be a second way to paint the one
50
+ * surface `input.backgroundColor` already paints, and the two could then
51
+ * disagree. One property, one surface.
52
+ *
53
+ * `fontFamily` is deliberately absent: Square validates it against its own
54
+ * loadable list and throws on both CSS-wide stacks and arbitrary names, which
55
+ * blocks the iframe from attaching at all. Its default is a system sans, which is
56
+ * what --font-sans resolves to anyway.
57
+ */
58
+ export declare function style(p: Palette): Style;
59
+ /**
60
+ * The rule without which everything above is decoration.
61
+ *
62
+ * Square's `input.backgroundColor` is accepted and then IGNORED: the field
63
+ * renders white on a black checkout no matter what is passed. The cause is not
64
+ * Square's, and it is one property. A page that sets `color-scheme` on
65
+ * `<html>` — which is the correct thing to do, and what every themed app does so
66
+ * the UA paints scrollbars and form controls to match — leaks it INTO the
67
+ * cross-origin iframe, because `color-scheme` inherits. Inside the frame the UA
68
+ * then paints form controls on its own scheme background, over anything the SDK
69
+ * declared. Pinning the container back to `auto` lets our ground through.
70
+ *
71
+ * (A Square forum user reported the same thing after two years of the docs' own
72
+ * recipe not working. Same fix, arrived at from the other end.)
73
+ *
74
+ * THIS IS APPLIED FOR YOU, by `terminal()`, before any card can attach — it must
75
+ * be in the stylesheet BEFORE the element mounts, because the container is styled
76
+ * as the card attaches, and a rule added afterwards is a rule that arrived too
77
+ * late. It is not left to a host to remember, and it is not a stylesheet a host
78
+ * can forget to import: this package produced the style object, so this package
79
+ * owes the one rule that makes it mean anything.
80
+ *
81
+ * Idempotent, and a no-op without a document. A host that already ships the rule
82
+ * (hanzoai/pay does, in `index.css`) gets an identical declaration — same
83
+ * property, same value — so there is nothing for the two to disagree about.
84
+ */
85
+ export declare function pin(doc?: Document): void;
@@ -0,0 +1,22 @@
1
+ import { type Terminal } from '../terminal';
2
+ import { type Palette } from '../palette';
3
+ /** The org's PUBLIC Square config, exactly as commerce publishes it. */
4
+ export interface Config {
5
+ applicationId: string;
6
+ locationId: string;
7
+ /** `'sandbox'` tokenizes against Square's sandbox. Anything else is production. */
8
+ environment?: string;
9
+ /** The card field's colours. Defaults to @hanzo/design's dark table. */
10
+ palette?: Palette;
11
+ }
12
+ /**
13
+ * A terminal for this browser. Loads the SDK once per page, whichever surface
14
+ * asks first.
15
+ *
16
+ * The palette is fixed at construction because SQUARE HAS NO API TO RESTYLE A
17
+ * LIVE CARD — the fields are a cross-origin iframe, told their colours once, at
18
+ * creation. Following a theme change therefore means `release()` then a new
19
+ * terminal, and making the palette a constructor argument is what forces that to
20
+ * happen by construction rather than by remembering to.
21
+ */
22
+ export declare function terminal(config: Config): Promise<Terminal>;
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "@hanzo/pay",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Square payments for web and native behind one interface. Card, Apple Pay, Google Pay, Cash App Pay, ACH and gift cards, feature-detected — with one colour table that dresses the web iframe and the native sheet alike.",
6
+ "license": "Apache-2.0",
7
+ "author": "Hanzo AI <dev@hanzo.ai>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/hanzoai/pay.git",
11
+ "directory": "pkg/pay"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs",
18
+ "default": "./dist/index.js"
19
+ },
20
+ "./web": {
21
+ "types": "./dist/web/index.d.ts",
22
+ "import": "./dist/web/index.js",
23
+ "require": "./dist/web/index.cjs",
24
+ "default": "./dist/web/index.js"
25
+ },
26
+ "./native": {
27
+ "types": "./dist/native/index.d.ts",
28
+ "import": "./dist/native/index.js",
29
+ "require": "./dist/native/index.cjs",
30
+ "default": "./dist/native/index.js"
31
+ },
32
+ "./package.json": "./package.json"
33
+ },
34
+ "main": "./dist/index.cjs",
35
+ "module": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "sideEffects": false,
38
+ "files": [
39
+ "dist"
40
+ ],
41
+ "scripts": {
42
+ "build": "tsup && tsc -p tsconfig.build.json",
43
+ "clean": "rm -rf dist",
44
+ "prepack": "pnpm run build",
45
+ "tc": "tsc --noEmit",
46
+ "test": "vitest run",
47
+ "test:watch": "vitest"
48
+ },
49
+ "peerDependencies": {
50
+ "react-native-square-in-app-payments": ">=2.1.0"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "react-native-square-in-app-payments": {
54
+ "optional": true
55
+ }
56
+ },
57
+ "devDependencies": {
58
+ "react-native-square-in-app-payments": "2.1.0",
59
+ "tsup": "^8.5.1",
60
+ "typescript": "^5.7.2",
61
+ "vitest": "^4.0.0"
62
+ },
63
+ "publishConfig": {
64
+ "access": "public"
65
+ },
66
+ "keywords": [
67
+ "square",
68
+ "payments",
69
+ "react-native",
70
+ "apple-pay",
71
+ "google-pay",
72
+ "cash-app-pay",
73
+ "ach"
74
+ ]
75
+ }