@snap-pay/elements 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +87 -0
- package/dist/index.cjs +1337 -0
- package/dist/index.d.cts +299 -0
- package/dist/index.d.ts +299 -0
- package/dist/index.js +1306 -0
- package/package.json +47 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1337 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var js = require('@snap-pay/js');
|
|
4
|
+
|
|
5
|
+
// src/index.ts
|
|
6
|
+
|
|
7
|
+
// src/base-element.ts
|
|
8
|
+
var BaseElement = class {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.container = null;
|
|
11
|
+
this.mounted = false;
|
|
12
|
+
this.listeners = /* @__PURE__ */ new Map();
|
|
13
|
+
// Subclasses override this to suppress the mount-time
|
|
14
|
+
// auto-ready. See BaseElement.mount above.
|
|
15
|
+
this.deferReadyOnMount = false;
|
|
16
|
+
}
|
|
17
|
+
mount(target) {
|
|
18
|
+
if (this.mounted) {
|
|
19
|
+
throw new Error(
|
|
20
|
+
`${this.type} element is already mounted. Call unmount() before mounting again.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const node = resolveTarget(target);
|
|
24
|
+
this.container = node;
|
|
25
|
+
this.render(node);
|
|
26
|
+
this.mounted = true;
|
|
27
|
+
if (!this.deferReadyOnMount) {
|
|
28
|
+
queueMicrotask(() => {
|
|
29
|
+
if (this.mounted) this.emit("ready", void 0);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
unmount() {
|
|
34
|
+
if (!this.mounted || !this.container) return;
|
|
35
|
+
this.container.innerHTML = "";
|
|
36
|
+
this.container = null;
|
|
37
|
+
this.mounted = false;
|
|
38
|
+
}
|
|
39
|
+
destroy() {
|
|
40
|
+
this.unmount();
|
|
41
|
+
this.listeners.clear();
|
|
42
|
+
}
|
|
43
|
+
on(event, handler) {
|
|
44
|
+
let set = this.listeners.get(event);
|
|
45
|
+
if (!set) {
|
|
46
|
+
set = /* @__PURE__ */ new Set();
|
|
47
|
+
this.listeners.set(event, set);
|
|
48
|
+
}
|
|
49
|
+
set.add(handler);
|
|
50
|
+
}
|
|
51
|
+
off(event, handler) {
|
|
52
|
+
const set = this.listeners.get(event);
|
|
53
|
+
if (!set) return;
|
|
54
|
+
set.delete(handler);
|
|
55
|
+
}
|
|
56
|
+
// Internal — used by subclasses to fire an event to every
|
|
57
|
+
// registered handler. Never called from userland.
|
|
58
|
+
//
|
|
59
|
+
// Review fix #7 — each handler runs in its own try/catch so a
|
|
60
|
+
// buggy `onChange` can't cascade into breaking `onReady`. We
|
|
61
|
+
// log to console.error rather than swallow silently — a
|
|
62
|
+
// silent failure would be worse for merchants debugging their
|
|
63
|
+
// integration than a noisy one.
|
|
64
|
+
emit(event, payload) {
|
|
65
|
+
const set = this.listeners.get(event);
|
|
66
|
+
if (!set) return;
|
|
67
|
+
for (const handler of set) {
|
|
68
|
+
try {
|
|
69
|
+
handler(payload);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
console.error(
|
|
72
|
+
`[snap-elements] handler for "${event}" threw:`,
|
|
73
|
+
err
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Optional method for form-collecting elements (BillingAddress,
|
|
79
|
+
// Customer, OTP). Default returns null; subclasses override to
|
|
80
|
+
// expose their form data imperatively. Kept in the base so
|
|
81
|
+
// subclasses can `override` without triggering TS4113.
|
|
82
|
+
getValue() {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
function resolveTarget(target) {
|
|
87
|
+
if (typeof target === "string") {
|
|
88
|
+
const node = document.querySelector(target);
|
|
89
|
+
if (!node) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Mount target "${target}" was not found in the document.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return node;
|
|
95
|
+
}
|
|
96
|
+
return target;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/style.ts
|
|
100
|
+
var DEFAULTS = {
|
|
101
|
+
theme: "light",
|
|
102
|
+
primaryColor: "#0066ff",
|
|
103
|
+
borderRadius: "8px",
|
|
104
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
105
|
+
spacingUnit: "8px",
|
|
106
|
+
labelColor: "#475569",
|
|
107
|
+
errorColor: "#dc2626",
|
|
108
|
+
mutedColor: "#94a3b8"
|
|
109
|
+
};
|
|
110
|
+
function resolveAppearance(input, forType) {
|
|
111
|
+
const theme = pickTheme(input.theme ?? DEFAULTS.theme);
|
|
112
|
+
const palette = theme === "dark" ? DARK_PALETTE : LIGHT_PALETTE;
|
|
113
|
+
const rule = forType && input.rules ? input.rules[forType] ?? {} : {};
|
|
114
|
+
return {
|
|
115
|
+
primaryColor: input.primaryColor ?? DEFAULTS.primaryColor,
|
|
116
|
+
borderRadius: rule.borderRadius ?? input.borderRadius ?? DEFAULTS.borderRadius,
|
|
117
|
+
fontFamily: rule.fontFamily ?? input.fontFamily ?? DEFAULTS.fontFamily,
|
|
118
|
+
theme,
|
|
119
|
+
background: rule.background ?? palette.background,
|
|
120
|
+
foreground: rule.foreground ?? palette.foreground,
|
|
121
|
+
border: rule.border ?? palette.border,
|
|
122
|
+
hoverBackground: palette.hoverBackground,
|
|
123
|
+
spacingUnit: input.spacingUnit ?? DEFAULTS.spacingUnit,
|
|
124
|
+
labelColor: input.labelColor ?? DEFAULTS.labelColor,
|
|
125
|
+
errorColor: input.errorColor ?? DEFAULTS.errorColor,
|
|
126
|
+
mutedColor: input.mutedColor ?? DEFAULTS.mutedColor
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function pickTheme(theme) {
|
|
130
|
+
if (theme !== "auto") return theme;
|
|
131
|
+
if (typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
|
132
|
+
return "dark";
|
|
133
|
+
}
|
|
134
|
+
return "light";
|
|
135
|
+
}
|
|
136
|
+
var LIGHT_PALETTE = {
|
|
137
|
+
background: "#ffffff",
|
|
138
|
+
foreground: "#0f172a",
|
|
139
|
+
border: "#e2e8f0",
|
|
140
|
+
hoverBackground: "#f8fafc"
|
|
141
|
+
};
|
|
142
|
+
var DARK_PALETTE = {
|
|
143
|
+
background: "#0f172a",
|
|
144
|
+
foreground: "#f8fafc",
|
|
145
|
+
border: "#334155",
|
|
146
|
+
hoverBackground: "#1e293b"
|
|
147
|
+
};
|
|
148
|
+
function formInputStyle(a) {
|
|
149
|
+
return {
|
|
150
|
+
width: "100%",
|
|
151
|
+
padding: `10px 12px`,
|
|
152
|
+
borderRadius: a.borderRadius,
|
|
153
|
+
border: `1px solid ${a.border}`,
|
|
154
|
+
background: a.background,
|
|
155
|
+
color: a.foreground,
|
|
156
|
+
fontFamily: a.fontFamily,
|
|
157
|
+
fontSize: "14px",
|
|
158
|
+
outline: "none",
|
|
159
|
+
boxSizing: "border-box"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function formLabelStyle(a) {
|
|
163
|
+
return {
|
|
164
|
+
fontSize: "12px",
|
|
165
|
+
color: a.labelColor,
|
|
166
|
+
fontFamily: a.fontFamily,
|
|
167
|
+
display: "block",
|
|
168
|
+
marginBottom: "4px"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/wallet-providers.ts
|
|
173
|
+
var WALLET_PROVIDER_LABELS = {
|
|
174
|
+
evcPlus: "EVC Plus",
|
|
175
|
+
sahal: "Sahal",
|
|
176
|
+
zaad: "Zaad",
|
|
177
|
+
edahab: "eDahab",
|
|
178
|
+
jeeb: "Jeeb",
|
|
179
|
+
premierWallet: "Premier Wallet"
|
|
180
|
+
};
|
|
181
|
+
var WALLET_PROVIDERS = Object.keys(
|
|
182
|
+
WALLET_PROVIDER_LABELS
|
|
183
|
+
);
|
|
184
|
+
var WALLET_SET = new Set(WALLET_PROVIDERS);
|
|
185
|
+
function isWalletProvider(p) {
|
|
186
|
+
return WALLET_SET.has(p);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// src/wallet-element.ts
|
|
190
|
+
var WalletElement = class extends BaseElement {
|
|
191
|
+
constructor(args) {
|
|
192
|
+
super();
|
|
193
|
+
this.args = args;
|
|
194
|
+
this.type = "wallet";
|
|
195
|
+
this.selected = null;
|
|
196
|
+
this.allowed = [];
|
|
197
|
+
this.buttons = /* @__PURE__ */ new Map();
|
|
198
|
+
this.renderTarget = null;
|
|
199
|
+
this.resolvedAppearance = resolveAppearance(args.appearance);
|
|
200
|
+
this.unregisterChild = args.registerChild(
|
|
201
|
+
() => this.selected === null ? null : { kind: "provider", provider: this.selected }
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
destroy() {
|
|
205
|
+
this.unregisterChild();
|
|
206
|
+
super.destroy();
|
|
207
|
+
}
|
|
208
|
+
render(container) {
|
|
209
|
+
this.renderTarget = container;
|
|
210
|
+
if (this.args.initialSession) {
|
|
211
|
+
this.allowed = this.args.initialSession.allowedProviders.filter(
|
|
212
|
+
isWalletProvider
|
|
213
|
+
);
|
|
214
|
+
this.renderButtons(container);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
container.textContent = "";
|
|
218
|
+
const loading = document.createElement("div");
|
|
219
|
+
loading.textContent = "Loading payment methods\u2026";
|
|
220
|
+
loading.style.padding = "12px";
|
|
221
|
+
loading.style.color = this.resolvedAppearance.foreground;
|
|
222
|
+
loading.style.fontFamily = this.resolvedAppearance.fontFamily;
|
|
223
|
+
loading.style.fontSize = "13px";
|
|
224
|
+
container.appendChild(loading);
|
|
225
|
+
js.fetchSession(this.args.apiBase, this.args.publishableKey, this.args.clientSecret).then((session) => {
|
|
226
|
+
this.allowed = session.allowedProviders.filter(isWalletProvider);
|
|
227
|
+
if (this.renderTarget === container) this.renderButtons(container);
|
|
228
|
+
}).catch((err) => {
|
|
229
|
+
if (this.renderTarget !== container) return;
|
|
230
|
+
container.textContent = "";
|
|
231
|
+
const errNode = document.createElement("div");
|
|
232
|
+
errNode.textContent = err instanceof Error ? err.message : "Failed to load payment methods.";
|
|
233
|
+
errNode.style.padding = "12px";
|
|
234
|
+
errNode.style.color = "#dc2626";
|
|
235
|
+
errNode.style.fontFamily = this.resolvedAppearance.fontFamily;
|
|
236
|
+
errNode.style.fontSize = "13px";
|
|
237
|
+
container.appendChild(errNode);
|
|
238
|
+
this.emit("failed", {
|
|
239
|
+
error: err instanceof Error ? err.message : String(err)
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
renderButtons(container) {
|
|
244
|
+
container.textContent = "";
|
|
245
|
+
if (this.allowed.length === 0) {
|
|
246
|
+
const empty = document.createElement("div");
|
|
247
|
+
empty.textContent = "No mobile wallet methods available for this session.";
|
|
248
|
+
empty.style.padding = "12px";
|
|
249
|
+
empty.style.color = this.resolvedAppearance.foreground;
|
|
250
|
+
empty.style.fontFamily = this.resolvedAppearance.fontFamily;
|
|
251
|
+
empty.style.fontSize = "13px";
|
|
252
|
+
container.appendChild(empty);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const list = document.createElement("div");
|
|
256
|
+
list.setAttribute("role", "radiogroup");
|
|
257
|
+
list.setAttribute("aria-label", "Choose a payment method");
|
|
258
|
+
list.style.display = "grid";
|
|
259
|
+
list.style.gap = "8px";
|
|
260
|
+
list.style.fontFamily = this.resolvedAppearance.fontFamily;
|
|
261
|
+
for (const provider of this.allowed) {
|
|
262
|
+
const button = this.renderProviderButton(provider);
|
|
263
|
+
this.buttons.set(provider, button);
|
|
264
|
+
list.appendChild(button);
|
|
265
|
+
}
|
|
266
|
+
container.appendChild(list);
|
|
267
|
+
}
|
|
268
|
+
renderProviderButton(provider) {
|
|
269
|
+
const btn = document.createElement("button");
|
|
270
|
+
btn.type = "button";
|
|
271
|
+
btn.setAttribute("role", "radio");
|
|
272
|
+
btn.setAttribute("aria-checked", "false");
|
|
273
|
+
btn.dataset.snapProvider = provider;
|
|
274
|
+
btn.textContent = WALLET_PROVIDER_LABELS[provider];
|
|
275
|
+
Object.assign(btn.style, this.buttonStyle(false));
|
|
276
|
+
btn.addEventListener("mouseenter", () => {
|
|
277
|
+
if (this.selected !== provider) {
|
|
278
|
+
btn.style.background = this.resolvedAppearance.hoverBackground;
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
btn.addEventListener("mouseleave", () => {
|
|
282
|
+
Object.assign(
|
|
283
|
+
btn.style,
|
|
284
|
+
this.buttonStyle(this.selected === provider)
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
btn.addEventListener("focus", () => this.emit("focus", void 0));
|
|
288
|
+
btn.addEventListener("blur", () => this.emit("blur", void 0));
|
|
289
|
+
btn.addEventListener("click", () => this.select(provider));
|
|
290
|
+
return btn;
|
|
291
|
+
}
|
|
292
|
+
select(provider) {
|
|
293
|
+
this.selected = provider;
|
|
294
|
+
for (const [p, button] of this.buttons) {
|
|
295
|
+
const isSelected = p === provider;
|
|
296
|
+
button.setAttribute("aria-checked", isSelected ? "true" : "false");
|
|
297
|
+
Object.assign(button.style, this.buttonStyle(isSelected));
|
|
298
|
+
}
|
|
299
|
+
this.emit("change", { complete: true });
|
|
300
|
+
}
|
|
301
|
+
buttonStyle(isSelected) {
|
|
302
|
+
const a = this.resolvedAppearance;
|
|
303
|
+
return {
|
|
304
|
+
padding: "12px 16px",
|
|
305
|
+
borderRadius: a.borderRadius,
|
|
306
|
+
border: isSelected ? `2px solid ${a.primaryColor}` : `1px solid ${a.border}`,
|
|
307
|
+
background: isSelected ? a.hoverBackground : a.background,
|
|
308
|
+
color: a.foreground,
|
|
309
|
+
fontFamily: a.fontFamily,
|
|
310
|
+
fontSize: "14px",
|
|
311
|
+
fontWeight: isSelected ? "600" : "500",
|
|
312
|
+
textAlign: "left",
|
|
313
|
+
cursor: "pointer",
|
|
314
|
+
// outline: keep browser focus ring so keyboard nav is visible
|
|
315
|
+
transition: "background 120ms, border-color 120ms",
|
|
316
|
+
// width: 100% wrapped for the grid layout
|
|
317
|
+
width: "100%",
|
|
318
|
+
boxSizing: "border-box"
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
function createWalletElement(args) {
|
|
323
|
+
return new WalletElement(args);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// src/card-element.ts
|
|
327
|
+
var IFRAME_READY_TIMEOUT_MS = 5e3;
|
|
328
|
+
var CardElement = class extends BaseElement {
|
|
329
|
+
constructor(args) {
|
|
330
|
+
super();
|
|
331
|
+
this.args = args;
|
|
332
|
+
this.type = "card";
|
|
333
|
+
// "Ready" for CardElement means the iframe has finished loading
|
|
334
|
+
// AND handshook via postMessage. BaseElement's mount-time
|
|
335
|
+
// auto-ready would fire too early, before the iframe is even
|
|
336
|
+
// reachable — suppress it.
|
|
337
|
+
this.deferReadyOnMount = true;
|
|
338
|
+
// Current selection — populated by the iframe after a successful
|
|
339
|
+
// submit. Cleared on any `change` from the iframe so a merchant
|
|
340
|
+
// that changes the amount and re-confirms doesn't reuse a stale
|
|
341
|
+
// token.
|
|
342
|
+
this.selection = null;
|
|
343
|
+
// Signals whether the iframe has fired `ready`. Non-ready messages
|
|
344
|
+
// are silently dropped.
|
|
345
|
+
this.ready = false;
|
|
346
|
+
// Review fix #2 — once we've emitted `failed` (typically from the
|
|
347
|
+
// ready timeout), we suppress ANY subsequent `ready` so a slow
|
|
348
|
+
// iframe that finally handshakes doesn't leave the merchant with
|
|
349
|
+
// both `failed` and `ready` fired.
|
|
350
|
+
this.failedFired = false;
|
|
351
|
+
this.iframe = null;
|
|
352
|
+
this.messageHandler = (e) => this.onMessage(e);
|
|
353
|
+
// Guard for the READY timeout — cleared on `ready` receipt, fires
|
|
354
|
+
// an emit('failed', …) if the iframe never wakes up.
|
|
355
|
+
this.readyTimer = null;
|
|
356
|
+
this.frameOrigin = deriveOrigin(args.cardFrameUrl);
|
|
357
|
+
this.unregisterChild = args.registerChild(() => {
|
|
358
|
+
if (this.selection === null) return null;
|
|
359
|
+
return {
|
|
360
|
+
kind: "card",
|
|
361
|
+
token: this.selection.token,
|
|
362
|
+
brand: this.selection.brand,
|
|
363
|
+
last4: this.selection.last4
|
|
364
|
+
};
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
destroy() {
|
|
368
|
+
this.unregisterChild();
|
|
369
|
+
this.clearReadyTimer();
|
|
370
|
+
if (typeof window !== "undefined") {
|
|
371
|
+
window.removeEventListener("message", this.messageHandler);
|
|
372
|
+
}
|
|
373
|
+
super.destroy();
|
|
374
|
+
}
|
|
375
|
+
render(container) {
|
|
376
|
+
container.textContent = "";
|
|
377
|
+
if (!this.args.cardFrameUrl) {
|
|
378
|
+
const err = document.createElement("div");
|
|
379
|
+
err.textContent = "Card Element requires `cardFrameUrl` (Snap constructor option).";
|
|
380
|
+
err.style.color = "#dc2626";
|
|
381
|
+
err.style.padding = "12px";
|
|
382
|
+
err.style.fontSize = "13px";
|
|
383
|
+
container.appendChild(err);
|
|
384
|
+
this.failedFired = true;
|
|
385
|
+
this.emit("failed", { error: "cardFrameUrl not configured" });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const iframe = document.createElement("iframe");
|
|
389
|
+
iframe.src = this.args.cardFrameUrl;
|
|
390
|
+
iframe.title = "Card payment fields";
|
|
391
|
+
iframe.style.width = "100%";
|
|
392
|
+
iframe.style.minHeight = "160px";
|
|
393
|
+
iframe.style.border = "0";
|
|
394
|
+
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
395
|
+
iframe.setAttribute("allow", "payment");
|
|
396
|
+
container.appendChild(iframe);
|
|
397
|
+
this.iframe = iframe;
|
|
398
|
+
if (typeof window !== "undefined") {
|
|
399
|
+
window.addEventListener("message", this.messageHandler);
|
|
400
|
+
}
|
|
401
|
+
this.readyTimer = setTimeout(() => {
|
|
402
|
+
if (!this.ready && !this.failedFired) {
|
|
403
|
+
this.failedFired = true;
|
|
404
|
+
this.emit("failed", {
|
|
405
|
+
error: `Card iframe did not become ready within ${IFRAME_READY_TIMEOUT_MS}ms.`
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}, IFRAME_READY_TIMEOUT_MS);
|
|
409
|
+
}
|
|
410
|
+
onMessage(event) {
|
|
411
|
+
if (!this.frameOrigin) return;
|
|
412
|
+
if (event.origin !== this.frameOrigin) return;
|
|
413
|
+
if (this.iframe && event.source !== this.iframe.contentWindow) return;
|
|
414
|
+
const data = event.data;
|
|
415
|
+
if (!data || data.channel !== "snap-card") return;
|
|
416
|
+
switch (data.type) {
|
|
417
|
+
case "ready":
|
|
418
|
+
if (this.failedFired) return;
|
|
419
|
+
this.ready = true;
|
|
420
|
+
this.clearReadyTimer();
|
|
421
|
+
this.sendInit();
|
|
422
|
+
this.emit("ready", void 0);
|
|
423
|
+
return;
|
|
424
|
+
case "change":
|
|
425
|
+
this.selection = null;
|
|
426
|
+
this.emit("change", { complete: data.complete });
|
|
427
|
+
return;
|
|
428
|
+
case "focus":
|
|
429
|
+
this.emit("focus", void 0);
|
|
430
|
+
return;
|
|
431
|
+
case "blur":
|
|
432
|
+
this.emit("blur", void 0);
|
|
433
|
+
return;
|
|
434
|
+
case "token": {
|
|
435
|
+
if (!this.ready) return;
|
|
436
|
+
const sel = { token: data.token, brand: data.brand, last4: data.last4 };
|
|
437
|
+
this.selection = sel;
|
|
438
|
+
this.emit("change", { complete: true });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
case "error":
|
|
442
|
+
this.failedFired = true;
|
|
443
|
+
this.emit("failed", { error: data.message });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
sendInit() {
|
|
448
|
+
if (!this.iframe || !this.frameOrigin) return;
|
|
449
|
+
const msg = {
|
|
450
|
+
channel: "snap-card",
|
|
451
|
+
type: "init",
|
|
452
|
+
publishableKey: this.args.publishableKey,
|
|
453
|
+
clientSecret: this.args.clientSecret,
|
|
454
|
+
environment: this.args.environment ?? "sandbox",
|
|
455
|
+
appearance: this.args.appearance
|
|
456
|
+
};
|
|
457
|
+
this.iframe.contentWindow?.postMessage(msg, this.frameOrigin);
|
|
458
|
+
}
|
|
459
|
+
clearReadyTimer() {
|
|
460
|
+
if (this.readyTimer !== null) {
|
|
461
|
+
clearTimeout(this.readyTimer);
|
|
462
|
+
this.readyTimer = null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
function createCardElement(args) {
|
|
467
|
+
return new CardElement(args);
|
|
468
|
+
}
|
|
469
|
+
function deriveOrigin(url) {
|
|
470
|
+
if (!url) return null;
|
|
471
|
+
try {
|
|
472
|
+
return new URL(url).origin;
|
|
473
|
+
} catch {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
var PaymentElement = class extends BaseElement {
|
|
478
|
+
constructor(args) {
|
|
479
|
+
super();
|
|
480
|
+
this.args = args;
|
|
481
|
+
this.type = "payment";
|
|
482
|
+
this.deferReadyOnMount = true;
|
|
483
|
+
this.children = [];
|
|
484
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "payment");
|
|
485
|
+
}
|
|
486
|
+
destroy() {
|
|
487
|
+
for (const child of this.children) child.destroy();
|
|
488
|
+
this.children = [];
|
|
489
|
+
super.destroy();
|
|
490
|
+
}
|
|
491
|
+
render(container) {
|
|
492
|
+
container.textContent = "";
|
|
493
|
+
const a = this.resolvedAppearance;
|
|
494
|
+
const loading = document.createElement("div");
|
|
495
|
+
loading.textContent = "Loading payment methods\u2026";
|
|
496
|
+
Object.assign(loading.style, {
|
|
497
|
+
padding: "12px",
|
|
498
|
+
color: a.mutedColor,
|
|
499
|
+
fontFamily: a.fontFamily,
|
|
500
|
+
fontSize: "13px"
|
|
501
|
+
});
|
|
502
|
+
container.appendChild(loading);
|
|
503
|
+
js.fetchSession(this.args.apiBase, this.args.publishableKey, this.args.clientSecret).then((session) => {
|
|
504
|
+
this.mountChildren(container, session);
|
|
505
|
+
}).catch((err) => {
|
|
506
|
+
container.textContent = "";
|
|
507
|
+
const errNode = document.createElement("div");
|
|
508
|
+
errNode.textContent = err instanceof Error ? err.message : "Failed to load payment methods.";
|
|
509
|
+
Object.assign(errNode.style, {
|
|
510
|
+
padding: "12px",
|
|
511
|
+
color: a.errorColor,
|
|
512
|
+
fontFamily: a.fontFamily,
|
|
513
|
+
fontSize: "13px"
|
|
514
|
+
});
|
|
515
|
+
container.appendChild(errNode);
|
|
516
|
+
this.emit("failed", {
|
|
517
|
+
error: err instanceof Error ? err.message : String(err)
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
mountChildren(container, session) {
|
|
522
|
+
container.textContent = "";
|
|
523
|
+
const a = this.resolvedAppearance;
|
|
524
|
+
const providers = session.allowedProviders;
|
|
525
|
+
const hasWallets = providers.some(isWalletProvider);
|
|
526
|
+
const hasCard = providers.includes("premierMastercard");
|
|
527
|
+
if (!hasWallets && !hasCard) {
|
|
528
|
+
const empty = document.createElement("div");
|
|
529
|
+
empty.textContent = "No payment methods available for this session.";
|
|
530
|
+
Object.assign(empty.style, {
|
|
531
|
+
padding: "12px",
|
|
532
|
+
color: a.mutedColor,
|
|
533
|
+
fontFamily: a.fontFamily,
|
|
534
|
+
fontSize: "13px"
|
|
535
|
+
});
|
|
536
|
+
container.appendChild(empty);
|
|
537
|
+
this.emit("ready", void 0);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const wrapper = document.createElement("div");
|
|
541
|
+
wrapper.style.display = "grid";
|
|
542
|
+
wrapper.style.gap = a.spacingUnit;
|
|
543
|
+
container.appendChild(wrapper);
|
|
544
|
+
if (hasWallets) {
|
|
545
|
+
const walletSlot = document.createElement("div");
|
|
546
|
+
wrapper.appendChild(walletSlot);
|
|
547
|
+
const wallet = createWalletElement({
|
|
548
|
+
clientSecret: this.args.clientSecret,
|
|
549
|
+
appearance: this.args.appearance,
|
|
550
|
+
publishableKey: this.args.publishableKey,
|
|
551
|
+
apiBase: this.args.apiBase,
|
|
552
|
+
environment: this.args.environment,
|
|
553
|
+
cardFrameUrl: this.args.cardFrameUrl,
|
|
554
|
+
registerChild: this.args.registerChild,
|
|
555
|
+
registerValue: this.args.registerValue,
|
|
556
|
+
// Share the session we already fetched — otherwise the
|
|
557
|
+
// WalletElement would re-fetch on mount.
|
|
558
|
+
initialSession: session,
|
|
559
|
+
options: {}
|
|
560
|
+
});
|
|
561
|
+
wallet.mount(walletSlot);
|
|
562
|
+
this.children.push(wallet);
|
|
563
|
+
}
|
|
564
|
+
if (hasWallets && hasCard) {
|
|
565
|
+
const divider = document.createElement("div");
|
|
566
|
+
divider.textContent = "Or pay with card";
|
|
567
|
+
Object.assign(divider.style, {
|
|
568
|
+
textAlign: "center",
|
|
569
|
+
color: a.mutedColor,
|
|
570
|
+
fontFamily: a.fontFamily,
|
|
571
|
+
fontSize: "12px",
|
|
572
|
+
padding: "4px 0"
|
|
573
|
+
});
|
|
574
|
+
wrapper.appendChild(divider);
|
|
575
|
+
}
|
|
576
|
+
if (hasCard) {
|
|
577
|
+
const cardSlot = document.createElement("div");
|
|
578
|
+
wrapper.appendChild(cardSlot);
|
|
579
|
+
const card = createCardElement({
|
|
580
|
+
clientSecret: this.args.clientSecret,
|
|
581
|
+
appearance: this.args.appearance,
|
|
582
|
+
publishableKey: this.args.publishableKey,
|
|
583
|
+
apiBase: this.args.apiBase,
|
|
584
|
+
environment: this.args.environment,
|
|
585
|
+
cardFrameUrl: this.args.cardFrameUrl,
|
|
586
|
+
registerChild: this.args.registerChild,
|
|
587
|
+
registerValue: this.args.registerValue,
|
|
588
|
+
options: {}
|
|
589
|
+
});
|
|
590
|
+
card.mount(cardSlot);
|
|
591
|
+
this.children.push(card);
|
|
592
|
+
}
|
|
593
|
+
for (const child of this.children) {
|
|
594
|
+
child.on("change", (payload) => this.emit("change", payload));
|
|
595
|
+
child.on("focus", () => this.emit("focus", void 0));
|
|
596
|
+
child.on("blur", () => this.emit("blur", void 0));
|
|
597
|
+
child.on("failed", (payload) => this.emit("failed", payload));
|
|
598
|
+
}
|
|
599
|
+
this.emit("ready", void 0);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
function createPaymentElement(args) {
|
|
603
|
+
return new PaymentElement(args);
|
|
604
|
+
}
|
|
605
|
+
var EXPRESS_LABEL_PREFIX = "Pay with ";
|
|
606
|
+
var ExpressCheckoutElement = class extends BaseElement {
|
|
607
|
+
constructor(args) {
|
|
608
|
+
super();
|
|
609
|
+
this.args = args;
|
|
610
|
+
this.type = "express";
|
|
611
|
+
this.selected = null;
|
|
612
|
+
this.allowed = [];
|
|
613
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "express");
|
|
614
|
+
this.unregisterChild = args.registerChild(
|
|
615
|
+
() => this.selected === null ? null : { kind: "provider", provider: this.selected }
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
destroy() {
|
|
619
|
+
this.unregisterChild();
|
|
620
|
+
super.destroy();
|
|
621
|
+
}
|
|
622
|
+
render(container) {
|
|
623
|
+
const a = this.resolvedAppearance;
|
|
624
|
+
if (this.args.initialSession) {
|
|
625
|
+
this.allowed = this.args.initialSession.allowedProviders.filter(
|
|
626
|
+
isWalletProvider
|
|
627
|
+
);
|
|
628
|
+
this.paint(container);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
container.textContent = "";
|
|
632
|
+
const loading = document.createElement("div");
|
|
633
|
+
loading.textContent = "Loading express checkout\u2026";
|
|
634
|
+
Object.assign(loading.style, {
|
|
635
|
+
padding: "12px",
|
|
636
|
+
color: a.mutedColor,
|
|
637
|
+
fontFamily: a.fontFamily,
|
|
638
|
+
fontSize: "13px"
|
|
639
|
+
});
|
|
640
|
+
container.appendChild(loading);
|
|
641
|
+
js.fetchSession(this.args.apiBase, this.args.publishableKey, this.args.clientSecret).then((session) => {
|
|
642
|
+
this.allowed = session.allowedProviders.filter(isWalletProvider);
|
|
643
|
+
this.paint(container);
|
|
644
|
+
}).catch((err) => {
|
|
645
|
+
container.textContent = "";
|
|
646
|
+
const errNode = document.createElement("div");
|
|
647
|
+
errNode.textContent = err instanceof Error ? err.message : "Failed to load express checkout.";
|
|
648
|
+
Object.assign(errNode.style, {
|
|
649
|
+
padding: "12px",
|
|
650
|
+
color: a.errorColor,
|
|
651
|
+
fontFamily: a.fontFamily,
|
|
652
|
+
fontSize: "13px"
|
|
653
|
+
});
|
|
654
|
+
container.appendChild(errNode);
|
|
655
|
+
this.emit("failed", {
|
|
656
|
+
error: err instanceof Error ? err.message : String(err)
|
|
657
|
+
});
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
paint(container) {
|
|
661
|
+
const a = this.resolvedAppearance;
|
|
662
|
+
container.textContent = "";
|
|
663
|
+
if (this.allowed.length === 0) {
|
|
664
|
+
const empty = document.createElement("div");
|
|
665
|
+
empty.textContent = "No express payment methods available.";
|
|
666
|
+
Object.assign(empty.style, {
|
|
667
|
+
padding: "12px",
|
|
668
|
+
color: a.mutedColor,
|
|
669
|
+
fontFamily: a.fontFamily,
|
|
670
|
+
fontSize: "13px"
|
|
671
|
+
});
|
|
672
|
+
container.appendChild(empty);
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
const grid = document.createElement("div");
|
|
676
|
+
grid.style.display = "grid";
|
|
677
|
+
grid.style.gap = a.spacingUnit;
|
|
678
|
+
grid.style.fontFamily = a.fontFamily;
|
|
679
|
+
for (const provider of this.allowed) {
|
|
680
|
+
grid.appendChild(this.renderButton(provider));
|
|
681
|
+
}
|
|
682
|
+
container.appendChild(grid);
|
|
683
|
+
}
|
|
684
|
+
renderButton(provider) {
|
|
685
|
+
const a = this.resolvedAppearance;
|
|
686
|
+
const btn = document.createElement("button");
|
|
687
|
+
btn.type = "button";
|
|
688
|
+
btn.dataset.snapExpressProvider = provider;
|
|
689
|
+
btn.textContent = `${EXPRESS_LABEL_PREFIX}${WALLET_PROVIDER_LABELS[provider]}`;
|
|
690
|
+
Object.assign(btn.style, {
|
|
691
|
+
padding: "14px 20px",
|
|
692
|
+
borderRadius: a.borderRadius,
|
|
693
|
+
border: "none",
|
|
694
|
+
background: a.primaryColor,
|
|
695
|
+
color: "#ffffff",
|
|
696
|
+
fontFamily: a.fontFamily,
|
|
697
|
+
fontSize: "15px",
|
|
698
|
+
fontWeight: "600",
|
|
699
|
+
cursor: "pointer",
|
|
700
|
+
width: "100%",
|
|
701
|
+
boxSizing: "border-box"
|
|
702
|
+
});
|
|
703
|
+
btn.addEventListener("click", () => {
|
|
704
|
+
this.selected = provider;
|
|
705
|
+
this.emit("change", { complete: true });
|
|
706
|
+
this.emit("submit", void 0);
|
|
707
|
+
});
|
|
708
|
+
btn.addEventListener("focus", () => this.emit("focus", void 0));
|
|
709
|
+
btn.addEventListener("blur", () => this.emit("blur", void 0));
|
|
710
|
+
return btn;
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
function createExpressCheckoutElement(args) {
|
|
714
|
+
return new ExpressCheckoutElement(args);
|
|
715
|
+
}
|
|
716
|
+
var PaymentMethodSelector = class extends BaseElement {
|
|
717
|
+
constructor(args) {
|
|
718
|
+
super();
|
|
719
|
+
this.args = args;
|
|
720
|
+
this.type = "paymentMethodSelector";
|
|
721
|
+
this.deferReadyOnMount = true;
|
|
722
|
+
this.walletChild = null;
|
|
723
|
+
this.cardChild = null;
|
|
724
|
+
this.activeTab = "wallet";
|
|
725
|
+
this.walletSlot = null;
|
|
726
|
+
this.cardSlot = null;
|
|
727
|
+
this.tabs = /* @__PURE__ */ new Map();
|
|
728
|
+
this.hasWallets = false;
|
|
729
|
+
this.hasCard = false;
|
|
730
|
+
// Session cached between render() and applyTab() so the wallet
|
|
731
|
+
// child mounts without a second HTTP request.
|
|
732
|
+
this.session = null;
|
|
733
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "paymentMethodSelector");
|
|
734
|
+
}
|
|
735
|
+
destroy() {
|
|
736
|
+
this.walletChild?.destroy();
|
|
737
|
+
this.cardChild?.destroy();
|
|
738
|
+
this.walletChild = null;
|
|
739
|
+
this.cardChild = null;
|
|
740
|
+
super.destroy();
|
|
741
|
+
}
|
|
742
|
+
render(container) {
|
|
743
|
+
const a = this.resolvedAppearance;
|
|
744
|
+
container.textContent = "";
|
|
745
|
+
const loading = document.createElement("div");
|
|
746
|
+
loading.textContent = "Loading payment methods\u2026";
|
|
747
|
+
Object.assign(loading.style, {
|
|
748
|
+
padding: "12px",
|
|
749
|
+
color: a.mutedColor,
|
|
750
|
+
fontFamily: a.fontFamily,
|
|
751
|
+
fontSize: "13px"
|
|
752
|
+
});
|
|
753
|
+
container.appendChild(loading);
|
|
754
|
+
js.fetchSession(this.args.apiBase, this.args.publishableKey, this.args.clientSecret).then((session) => {
|
|
755
|
+
this.session = session;
|
|
756
|
+
this.hasWallets = session.allowedProviders.some(isWalletProvider);
|
|
757
|
+
this.hasCard = session.allowedProviders.includes("premierMastercard");
|
|
758
|
+
this.mountUi(container);
|
|
759
|
+
}).catch((err) => {
|
|
760
|
+
container.textContent = "";
|
|
761
|
+
const errNode = document.createElement("div");
|
|
762
|
+
errNode.textContent = err instanceof Error ? err.message : "Failed to load payment methods.";
|
|
763
|
+
Object.assign(errNode.style, {
|
|
764
|
+
padding: "12px",
|
|
765
|
+
color: a.errorColor,
|
|
766
|
+
fontFamily: a.fontFamily,
|
|
767
|
+
fontSize: "13px"
|
|
768
|
+
});
|
|
769
|
+
container.appendChild(errNode);
|
|
770
|
+
this.emit("failed", {
|
|
771
|
+
error: err instanceof Error ? err.message : String(err)
|
|
772
|
+
});
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
mountUi(container) {
|
|
776
|
+
container.textContent = "";
|
|
777
|
+
const a = this.resolvedAppearance;
|
|
778
|
+
if (!this.hasWallets && !this.hasCard) {
|
|
779
|
+
const empty = document.createElement("div");
|
|
780
|
+
empty.textContent = "No payment methods available for this session.";
|
|
781
|
+
Object.assign(empty.style, {
|
|
782
|
+
padding: "12px",
|
|
783
|
+
color: a.mutedColor,
|
|
784
|
+
fontFamily: a.fontFamily,
|
|
785
|
+
fontSize: "13px"
|
|
786
|
+
});
|
|
787
|
+
container.appendChild(empty);
|
|
788
|
+
this.emit("ready", void 0);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const wrapper = document.createElement("div");
|
|
792
|
+
wrapper.style.display = "grid";
|
|
793
|
+
wrapper.style.gap = a.spacingUnit;
|
|
794
|
+
const tabBar = document.createElement("div");
|
|
795
|
+
tabBar.setAttribute("role", "tablist");
|
|
796
|
+
Object.assign(tabBar.style, {
|
|
797
|
+
display: "grid",
|
|
798
|
+
gridTemplateColumns: this.hasWallets && this.hasCard ? "1fr 1fr" : "1fr",
|
|
799
|
+
gap: "4px",
|
|
800
|
+
padding: "4px",
|
|
801
|
+
background: a.hoverBackground,
|
|
802
|
+
borderRadius: a.borderRadius
|
|
803
|
+
});
|
|
804
|
+
if (this.hasWallets) tabBar.appendChild(this.renderTab("wallet", "Wallet"));
|
|
805
|
+
if (this.hasCard) tabBar.appendChild(this.renderTab("card", "Card"));
|
|
806
|
+
wrapper.appendChild(tabBar);
|
|
807
|
+
this.walletSlot = document.createElement("div");
|
|
808
|
+
this.cardSlot = document.createElement("div");
|
|
809
|
+
wrapper.appendChild(this.walletSlot);
|
|
810
|
+
wrapper.appendChild(this.cardSlot);
|
|
811
|
+
container.appendChild(wrapper);
|
|
812
|
+
this.activeTab = this.hasWallets ? "wallet" : "card";
|
|
813
|
+
this.applyTab();
|
|
814
|
+
this.emit("ready", void 0);
|
|
815
|
+
}
|
|
816
|
+
renderTab(tab, label) {
|
|
817
|
+
const a = this.resolvedAppearance;
|
|
818
|
+
const btn = document.createElement("button");
|
|
819
|
+
btn.type = "button";
|
|
820
|
+
btn.setAttribute("role", "tab");
|
|
821
|
+
btn.dataset.snapMethodTab = tab;
|
|
822
|
+
btn.textContent = label;
|
|
823
|
+
Object.assign(btn.style, {
|
|
824
|
+
padding: "8px 12px",
|
|
825
|
+
border: "none",
|
|
826
|
+
background: "transparent",
|
|
827
|
+
color: a.foreground,
|
|
828
|
+
fontFamily: a.fontFamily,
|
|
829
|
+
fontSize: "14px",
|
|
830
|
+
fontWeight: "500",
|
|
831
|
+
borderRadius: a.borderRadius,
|
|
832
|
+
cursor: "pointer"
|
|
833
|
+
});
|
|
834
|
+
btn.addEventListener("click", () => {
|
|
835
|
+
if (this.activeTab === tab) return;
|
|
836
|
+
this.activeTab = tab;
|
|
837
|
+
this.applyTab();
|
|
838
|
+
});
|
|
839
|
+
this.tabs.set(tab, btn);
|
|
840
|
+
return btn;
|
|
841
|
+
}
|
|
842
|
+
applyTab() {
|
|
843
|
+
const a = this.resolvedAppearance;
|
|
844
|
+
for (const [tab, btn] of this.tabs) {
|
|
845
|
+
const active = tab === this.activeTab;
|
|
846
|
+
btn.setAttribute("aria-selected", String(active));
|
|
847
|
+
btn.style.background = active ? a.background : "transparent";
|
|
848
|
+
btn.style.boxShadow = active ? "0 1px 2px rgba(0,0,0,0.04)" : "none";
|
|
849
|
+
}
|
|
850
|
+
if (this.walletSlot) {
|
|
851
|
+
this.walletSlot.style.display = this.activeTab === "wallet" ? "block" : "none";
|
|
852
|
+
}
|
|
853
|
+
if (this.cardSlot) {
|
|
854
|
+
this.cardSlot.style.display = this.activeTab === "card" ? "block" : "none";
|
|
855
|
+
}
|
|
856
|
+
if (this.activeTab === "wallet" && !this.walletChild && this.walletSlot) {
|
|
857
|
+
this.walletChild = createWalletElement({
|
|
858
|
+
clientSecret: this.args.clientSecret,
|
|
859
|
+
appearance: this.args.appearance,
|
|
860
|
+
publishableKey: this.args.publishableKey,
|
|
861
|
+
apiBase: this.args.apiBase,
|
|
862
|
+
environment: this.args.environment,
|
|
863
|
+
cardFrameUrl: this.args.cardFrameUrl,
|
|
864
|
+
registerChild: this.args.registerChild,
|
|
865
|
+
registerValue: this.args.registerValue,
|
|
866
|
+
// Pass the session we already fetched so the wallet child
|
|
867
|
+
// renders synchronously instead of firing a second GET.
|
|
868
|
+
initialSession: this.session ?? void 0,
|
|
869
|
+
options: {}
|
|
870
|
+
});
|
|
871
|
+
this.walletChild.mount(this.walletSlot);
|
|
872
|
+
this.walletChild.on("change", (p) => this.emit("change", p));
|
|
873
|
+
this.walletChild.on("failed", (p) => this.emit("failed", p));
|
|
874
|
+
}
|
|
875
|
+
if (this.activeTab === "card" && !this.cardChild && this.cardSlot) {
|
|
876
|
+
this.cardChild = createCardElement({
|
|
877
|
+
clientSecret: this.args.clientSecret,
|
|
878
|
+
appearance: this.args.appearance,
|
|
879
|
+
publishableKey: this.args.publishableKey,
|
|
880
|
+
apiBase: this.args.apiBase,
|
|
881
|
+
environment: this.args.environment,
|
|
882
|
+
cardFrameUrl: this.args.cardFrameUrl,
|
|
883
|
+
registerChild: this.args.registerChild,
|
|
884
|
+
registerValue: this.args.registerValue,
|
|
885
|
+
options: {}
|
|
886
|
+
});
|
|
887
|
+
this.cardChild.mount(this.cardSlot);
|
|
888
|
+
this.cardChild.on("change", (p) => this.emit("change", p));
|
|
889
|
+
this.cardChild.on("failed", (p) => this.emit("failed", p));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
function createPaymentMethodSelector(args) {
|
|
894
|
+
return new PaymentMethodSelector(args);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// src/billing-address-element.ts
|
|
898
|
+
var FIELDS = [
|
|
899
|
+
{ key: "name", label: "Full name", autocomplete: "name" },
|
|
900
|
+
{ key: "line1", label: "Address line 1", autocomplete: "address-line1" },
|
|
901
|
+
{ key: "line2", label: "Address line 2 (optional)", autocomplete: "address-line2" },
|
|
902
|
+
{ key: "city", label: "City", autocomplete: "address-level2", half: true },
|
|
903
|
+
{ key: "region", label: "Region / state", autocomplete: "address-level1", half: true },
|
|
904
|
+
{ key: "postalCode", label: "Postal code", autocomplete: "postal-code", half: true },
|
|
905
|
+
{ key: "country", label: "Country", autocomplete: "country-name", half: true }
|
|
906
|
+
];
|
|
907
|
+
var BillingAddressElement = class extends BaseElement {
|
|
908
|
+
constructor(args) {
|
|
909
|
+
super();
|
|
910
|
+
this.args = args;
|
|
911
|
+
this.type = "billingAddress";
|
|
912
|
+
this.value = {};
|
|
913
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "billingAddress");
|
|
914
|
+
if (args.options.defaultCountry) {
|
|
915
|
+
this.value.country = args.options.defaultCountry;
|
|
916
|
+
}
|
|
917
|
+
this.unregisterValue = args.registerValue("billingAddress", () => {
|
|
918
|
+
const anyFilled = Object.values(this.value).some(
|
|
919
|
+
(v) => typeof v === "string" && v.length > 0
|
|
920
|
+
);
|
|
921
|
+
return anyFilled ? { ...this.value } : null;
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
destroy() {
|
|
925
|
+
this.unregisterValue();
|
|
926
|
+
super.destroy();
|
|
927
|
+
}
|
|
928
|
+
// Public API — merchants call `element.getValue()` to read the
|
|
929
|
+
// form state imperatively (e.g. for a custom validation step
|
|
930
|
+
// before confirmPayment). Optional, mirrors Stripe's pattern.
|
|
931
|
+
getValue() {
|
|
932
|
+
return { ...this.value };
|
|
933
|
+
}
|
|
934
|
+
render(container) {
|
|
935
|
+
container.textContent = "";
|
|
936
|
+
const a = this.resolvedAppearance;
|
|
937
|
+
const wrapper = document.createElement("div");
|
|
938
|
+
wrapper.style.display = "grid";
|
|
939
|
+
wrapper.style.gap = a.spacingUnit;
|
|
940
|
+
wrapper.style.fontFamily = a.fontFamily;
|
|
941
|
+
const half = [];
|
|
942
|
+
const flushHalfRow = () => {
|
|
943
|
+
if (half.length === 0) return;
|
|
944
|
+
const row = document.createElement("div");
|
|
945
|
+
row.style.display = "grid";
|
|
946
|
+
row.style.gridTemplateColumns = "1fr 1fr";
|
|
947
|
+
row.style.gap = a.spacingUnit;
|
|
948
|
+
for (const el of half) row.appendChild(el);
|
|
949
|
+
wrapper.appendChild(row);
|
|
950
|
+
half.length = 0;
|
|
951
|
+
};
|
|
952
|
+
for (const field of FIELDS) {
|
|
953
|
+
const node = this.renderField(field);
|
|
954
|
+
if (field.half) {
|
|
955
|
+
half.push(node);
|
|
956
|
+
if (half.length === 2) flushHalfRow();
|
|
957
|
+
} else {
|
|
958
|
+
flushHalfRow();
|
|
959
|
+
wrapper.appendChild(node);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
flushHalfRow();
|
|
963
|
+
container.appendChild(wrapper);
|
|
964
|
+
}
|
|
965
|
+
renderField(field) {
|
|
966
|
+
const label = document.createElement("label");
|
|
967
|
+
label.style.display = "block";
|
|
968
|
+
const span = document.createElement("span");
|
|
969
|
+
span.textContent = field.label;
|
|
970
|
+
Object.assign(span.style, formLabelStyle(this.resolvedAppearance));
|
|
971
|
+
const input = document.createElement("input");
|
|
972
|
+
input.type = "text";
|
|
973
|
+
input.setAttribute("autocomplete", field.autocomplete);
|
|
974
|
+
input.value = this.value[field.key] ?? "";
|
|
975
|
+
input.dataset.snapBillingField = field.key;
|
|
976
|
+
Object.assign(input.style, formInputStyle(this.resolvedAppearance));
|
|
977
|
+
input.addEventListener("input", () => {
|
|
978
|
+
const trimmed = input.value;
|
|
979
|
+
if (trimmed.length === 0) {
|
|
980
|
+
delete this.value[field.key];
|
|
981
|
+
} else {
|
|
982
|
+
this.value[field.key] = trimmed;
|
|
983
|
+
}
|
|
984
|
+
this.emit("change", { complete: this.isComplete() });
|
|
985
|
+
});
|
|
986
|
+
input.addEventListener("focus", () => this.emit("focus", void 0));
|
|
987
|
+
input.addEventListener("blur", () => this.emit("blur", void 0));
|
|
988
|
+
label.appendChild(span);
|
|
989
|
+
label.appendChild(input);
|
|
990
|
+
return label;
|
|
991
|
+
}
|
|
992
|
+
// A merchant asking `complete` here is asking "should I enable
|
|
993
|
+
// the confirm button?" — for a billing form, we consider it
|
|
994
|
+
// complete when any single field is filled, because a merchant
|
|
995
|
+
// that mounts a BillingAddress alongside a Wallet may only care
|
|
996
|
+
// about country. The Element deliberately doesn't gate confirm.
|
|
997
|
+
isComplete() {
|
|
998
|
+
return Object.values(this.value).some(
|
|
999
|
+
(v) => typeof v === "string" && v.length > 0
|
|
1000
|
+
);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
function createBillingAddressElement(args) {
|
|
1004
|
+
return new BillingAddressElement(args);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// src/customer-element.ts
|
|
1008
|
+
var FIELDS2 = [
|
|
1009
|
+
{ key: "name", label: "Full name", type: "text", autocomplete: "name" },
|
|
1010
|
+
{ key: "email", label: "Email", type: "email", autocomplete: "email", placeholder: "you@example.com" },
|
|
1011
|
+
{ key: "phone", label: "Phone", type: "tel", autocomplete: "tel", placeholder: "+252 \u2026" }
|
|
1012
|
+
];
|
|
1013
|
+
var CustomerElement = class extends BaseElement {
|
|
1014
|
+
constructor(args) {
|
|
1015
|
+
super();
|
|
1016
|
+
this.args = args;
|
|
1017
|
+
this.type = "customer";
|
|
1018
|
+
this.value = {};
|
|
1019
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "customer");
|
|
1020
|
+
this.unregisterValue = args.registerValue("customer", () => {
|
|
1021
|
+
const anyFilled = Object.values(this.value).some(
|
|
1022
|
+
(v) => typeof v === "string" && v.length > 0
|
|
1023
|
+
);
|
|
1024
|
+
return anyFilled ? { ...this.value } : null;
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
destroy() {
|
|
1028
|
+
this.unregisterValue();
|
|
1029
|
+
super.destroy();
|
|
1030
|
+
}
|
|
1031
|
+
getValue() {
|
|
1032
|
+
return { ...this.value };
|
|
1033
|
+
}
|
|
1034
|
+
render(container) {
|
|
1035
|
+
container.textContent = "";
|
|
1036
|
+
const a = this.resolvedAppearance;
|
|
1037
|
+
const wrapper = document.createElement("div");
|
|
1038
|
+
wrapper.style.display = "grid";
|
|
1039
|
+
wrapper.style.gap = a.spacingUnit;
|
|
1040
|
+
wrapper.style.fontFamily = a.fontFamily;
|
|
1041
|
+
for (const field of FIELDS2) {
|
|
1042
|
+
wrapper.appendChild(this.renderField(field));
|
|
1043
|
+
}
|
|
1044
|
+
container.appendChild(wrapper);
|
|
1045
|
+
}
|
|
1046
|
+
renderField(field) {
|
|
1047
|
+
const label = document.createElement("label");
|
|
1048
|
+
const span = document.createElement("span");
|
|
1049
|
+
span.textContent = field.label;
|
|
1050
|
+
Object.assign(span.style, formLabelStyle(this.resolvedAppearance));
|
|
1051
|
+
const input = document.createElement("input");
|
|
1052
|
+
input.type = field.type;
|
|
1053
|
+
input.setAttribute("autocomplete", field.autocomplete);
|
|
1054
|
+
if (field.placeholder) input.placeholder = field.placeholder;
|
|
1055
|
+
input.value = this.value[field.key] ?? "";
|
|
1056
|
+
input.dataset.snapCustomerField = field.key;
|
|
1057
|
+
Object.assign(input.style, formInputStyle(this.resolvedAppearance));
|
|
1058
|
+
input.addEventListener("input", () => {
|
|
1059
|
+
const trimmed = input.value;
|
|
1060
|
+
if (trimmed.length === 0) {
|
|
1061
|
+
delete this.value[field.key];
|
|
1062
|
+
} else {
|
|
1063
|
+
this.value[field.key] = trimmed;
|
|
1064
|
+
}
|
|
1065
|
+
this.emit("change", { complete: this.isComplete() });
|
|
1066
|
+
});
|
|
1067
|
+
input.addEventListener("focus", () => this.emit("focus", void 0));
|
|
1068
|
+
input.addEventListener("blur", () => this.emit("blur", void 0));
|
|
1069
|
+
label.appendChild(span);
|
|
1070
|
+
label.appendChild(input);
|
|
1071
|
+
return label;
|
|
1072
|
+
}
|
|
1073
|
+
isComplete() {
|
|
1074
|
+
return Object.values(this.value).some(
|
|
1075
|
+
(v) => typeof v === "string" && v.length > 0
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
};
|
|
1079
|
+
function createCustomerElement(args) {
|
|
1080
|
+
return new CustomerElement(args);
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// src/otp-element.ts
|
|
1084
|
+
var DEFAULT_LENGTH = 6;
|
|
1085
|
+
var OtpElement = class extends BaseElement {
|
|
1086
|
+
constructor(args) {
|
|
1087
|
+
super();
|
|
1088
|
+
this.args = args;
|
|
1089
|
+
this.type = "otp";
|
|
1090
|
+
this.inputs = [];
|
|
1091
|
+
this.length = args.options.otpLength && args.options.otpLength >= 4 && args.options.otpLength <= 10 ? args.options.otpLength : DEFAULT_LENGTH;
|
|
1092
|
+
this.digits = Array.from({ length: this.length }, () => "");
|
|
1093
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "otp");
|
|
1094
|
+
this.unregisterValue = args.registerValue("otp", () => {
|
|
1095
|
+
const code = this.digits.join("");
|
|
1096
|
+
if (code.length !== this.length) return null;
|
|
1097
|
+
const value = { code };
|
|
1098
|
+
return value;
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
destroy() {
|
|
1102
|
+
this.unregisterValue();
|
|
1103
|
+
super.destroy();
|
|
1104
|
+
}
|
|
1105
|
+
getValue() {
|
|
1106
|
+
return { code: this.digits.join("") };
|
|
1107
|
+
}
|
|
1108
|
+
render(container) {
|
|
1109
|
+
container.textContent = "";
|
|
1110
|
+
this.inputs = [];
|
|
1111
|
+
const a = this.resolvedAppearance;
|
|
1112
|
+
const wrapper = document.createElement("div");
|
|
1113
|
+
wrapper.style.display = "grid";
|
|
1114
|
+
wrapper.style.gridTemplateColumns = `repeat(${this.length}, 1fr)`;
|
|
1115
|
+
wrapper.style.gap = a.spacingUnit;
|
|
1116
|
+
wrapper.style.fontFamily = a.fontFamily;
|
|
1117
|
+
for (let i = 0; i < this.length; i++) {
|
|
1118
|
+
const input = document.createElement("input");
|
|
1119
|
+
input.type = "text";
|
|
1120
|
+
input.inputMode = "numeric";
|
|
1121
|
+
input.setAttribute("autocomplete", i === 0 ? "one-time-code" : "off");
|
|
1122
|
+
input.maxLength = 1;
|
|
1123
|
+
input.value = this.digits[i] ?? "";
|
|
1124
|
+
input.dataset.snapOtpIndex = String(i);
|
|
1125
|
+
Object.assign(input.style, {
|
|
1126
|
+
width: "100%",
|
|
1127
|
+
aspectRatio: "1 / 1",
|
|
1128
|
+
textAlign: "center",
|
|
1129
|
+
fontSize: "20px",
|
|
1130
|
+
fontWeight: "600",
|
|
1131
|
+
border: `1px solid ${a.border}`,
|
|
1132
|
+
borderRadius: a.borderRadius,
|
|
1133
|
+
background: a.background,
|
|
1134
|
+
color: a.foreground,
|
|
1135
|
+
outline: "none",
|
|
1136
|
+
boxSizing: "border-box"
|
|
1137
|
+
});
|
|
1138
|
+
input.addEventListener("input", (e) => this.onInput(e, i));
|
|
1139
|
+
input.addEventListener("keydown", (e) => this.onKeyDown(e, i));
|
|
1140
|
+
input.addEventListener("paste", (e) => this.onPaste(e));
|
|
1141
|
+
input.addEventListener("focus", () => this.emit("focus", void 0));
|
|
1142
|
+
input.addEventListener("blur", () => this.emit("blur", void 0));
|
|
1143
|
+
this.inputs.push(input);
|
|
1144
|
+
wrapper.appendChild(input);
|
|
1145
|
+
}
|
|
1146
|
+
container.appendChild(wrapper);
|
|
1147
|
+
}
|
|
1148
|
+
onInput(event, index) {
|
|
1149
|
+
const input = this.inputs[index];
|
|
1150
|
+
if (!input) return;
|
|
1151
|
+
const digit = input.value.replace(/[^\d]/g, "").slice(-1);
|
|
1152
|
+
input.value = digit;
|
|
1153
|
+
this.digits[index] = digit;
|
|
1154
|
+
if (digit && index + 1 < this.length) {
|
|
1155
|
+
this.inputs[index + 1]?.focus();
|
|
1156
|
+
}
|
|
1157
|
+
this.emit("change", { complete: this.isComplete() });
|
|
1158
|
+
}
|
|
1159
|
+
onKeyDown(event, index) {
|
|
1160
|
+
if (event.key === "Backspace" && !this.digits[index] && index > 0) {
|
|
1161
|
+
event.preventDefault();
|
|
1162
|
+
this.inputs[index - 1]?.focus();
|
|
1163
|
+
} else if (event.key === "ArrowLeft" && index > 0) {
|
|
1164
|
+
event.preventDefault();
|
|
1165
|
+
this.inputs[index - 1]?.focus();
|
|
1166
|
+
} else if (event.key === "ArrowRight" && index + 1 < this.length) {
|
|
1167
|
+
event.preventDefault();
|
|
1168
|
+
this.inputs[index + 1]?.focus();
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
onPaste(event) {
|
|
1172
|
+
const text = event.clipboardData?.getData("text") ?? "";
|
|
1173
|
+
const digits = text.replace(/[^\d]/g, "").slice(0, this.length);
|
|
1174
|
+
if (digits.length === 0) return;
|
|
1175
|
+
event.preventDefault();
|
|
1176
|
+
for (let i = 0; i < this.length; i++) {
|
|
1177
|
+
const d = digits[i] ?? "";
|
|
1178
|
+
this.digits[i] = d;
|
|
1179
|
+
const input = this.inputs[i];
|
|
1180
|
+
if (input) input.value = d;
|
|
1181
|
+
}
|
|
1182
|
+
const focusTarget = this.inputs[Math.min(digits.length, this.length - 1)];
|
|
1183
|
+
focusTarget?.focus();
|
|
1184
|
+
this.emit("change", { complete: this.isComplete() });
|
|
1185
|
+
}
|
|
1186
|
+
isComplete() {
|
|
1187
|
+
return this.digits.every((d) => d.length === 1);
|
|
1188
|
+
}
|
|
1189
|
+
};
|
|
1190
|
+
function createOtpElement(args) {
|
|
1191
|
+
return new OtpElement(args);
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// src/qr-element.ts
|
|
1195
|
+
var QrElement = class extends BaseElement {
|
|
1196
|
+
constructor(args) {
|
|
1197
|
+
super();
|
|
1198
|
+
this.args = args;
|
|
1199
|
+
this.type = "qr";
|
|
1200
|
+
this.deferReadyOnMount = true;
|
|
1201
|
+
this.resolvedAppearance = resolveAppearance(args.appearance, "qr");
|
|
1202
|
+
}
|
|
1203
|
+
render(container) {
|
|
1204
|
+
container.textContent = "";
|
|
1205
|
+
const a = this.resolvedAppearance;
|
|
1206
|
+
const panel = document.createElement("div");
|
|
1207
|
+
Object.assign(panel.style, {
|
|
1208
|
+
padding: "24px",
|
|
1209
|
+
border: `1px dashed ${a.border}`,
|
|
1210
|
+
borderRadius: a.borderRadius,
|
|
1211
|
+
background: a.background,
|
|
1212
|
+
color: a.mutedColor,
|
|
1213
|
+
fontFamily: a.fontFamily,
|
|
1214
|
+
fontSize: "13px",
|
|
1215
|
+
textAlign: "center"
|
|
1216
|
+
});
|
|
1217
|
+
panel.textContent = "QR-based checkout is not available in this SDK version.";
|
|
1218
|
+
container.appendChild(panel);
|
|
1219
|
+
setTimeout(() => {
|
|
1220
|
+
this.emit("failed", {
|
|
1221
|
+
error: "QR Element is not yet supported."
|
|
1222
|
+
});
|
|
1223
|
+
}, 0);
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
function createQrElement(args) {
|
|
1227
|
+
return new QrElement(args);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/card-validation.ts
|
|
1231
|
+
function normalizeCardNumber(raw) {
|
|
1232
|
+
return raw.replace(/[^\d]/g, "");
|
|
1233
|
+
}
|
|
1234
|
+
function luhnValid(cardNumber) {
|
|
1235
|
+
const digits = normalizeCardNumber(cardNumber);
|
|
1236
|
+
if (digits.length < 12 || digits.length > 19) return false;
|
|
1237
|
+
let sum = 0;
|
|
1238
|
+
let alternate = false;
|
|
1239
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
1240
|
+
let n = digits.charCodeAt(i) - 48;
|
|
1241
|
+
if (n < 0 || n > 9) return false;
|
|
1242
|
+
if (alternate) {
|
|
1243
|
+
n *= 2;
|
|
1244
|
+
if (n > 9) n -= 9;
|
|
1245
|
+
}
|
|
1246
|
+
sum += n;
|
|
1247
|
+
alternate = !alternate;
|
|
1248
|
+
}
|
|
1249
|
+
return sum % 10 === 0;
|
|
1250
|
+
}
|
|
1251
|
+
function detectBrand(cardNumber) {
|
|
1252
|
+
const d = normalizeCardNumber(cardNumber);
|
|
1253
|
+
if (d.length === 0) return "unknown";
|
|
1254
|
+
if (/^3[47]/.test(d)) return "amex";
|
|
1255
|
+
if (/^5[1-5]/.test(d)) return "mastercard";
|
|
1256
|
+
if (/^2(2[2-9][1-9]|[3-6]\d\d|7[01]\d|720)/.test(d)) return "mastercard";
|
|
1257
|
+
if (/^4/.test(d)) return "visa";
|
|
1258
|
+
if (/^(6011|65|64[4-9])/.test(d)) return "discover";
|
|
1259
|
+
return "unknown";
|
|
1260
|
+
}
|
|
1261
|
+
function formatCardNumber(raw) {
|
|
1262
|
+
const digits = normalizeCardNumber(raw);
|
|
1263
|
+
const brand = detectBrand(digits);
|
|
1264
|
+
if (brand === "amex") {
|
|
1265
|
+
return digits.slice(0, 15).replace(
|
|
1266
|
+
/(\d{4})(\d{0,6})(\d{0,5}).*/,
|
|
1267
|
+
(_m, a, b, c) => [a, b, c].filter(Boolean).join(" ")
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
return digits.slice(0, 19).replace(/(\d{4})/g, "$1 ").trim();
|
|
1271
|
+
}
|
|
1272
|
+
function cvcLengthFor(brand) {
|
|
1273
|
+
return brand === "amex" ? 4 : 3;
|
|
1274
|
+
}
|
|
1275
|
+
function cvcValid(cvc, brand) {
|
|
1276
|
+
if (!/^\d+$/.test(cvc)) return false;
|
|
1277
|
+
if (brand === "unknown") return cvc.length === 3 || cvc.length === 4;
|
|
1278
|
+
return cvc.length === cvcLengthFor(brand);
|
|
1279
|
+
}
|
|
1280
|
+
function expiryValid(input, now = Date.now()) {
|
|
1281
|
+
const match = /^(\d{1,2})\s*[/-]\s*(\d{2}|\d{4})$/.exec(input.trim());
|
|
1282
|
+
if (!match) return false;
|
|
1283
|
+
const month = Number.parseInt(match[1], 10);
|
|
1284
|
+
const yearRaw = Number.parseInt(match[2], 10);
|
|
1285
|
+
if (!Number.isFinite(month) || !Number.isFinite(yearRaw)) return false;
|
|
1286
|
+
if (month < 1 || month > 12) return false;
|
|
1287
|
+
const year = match[2].length === 2 ? 2e3 + yearRaw : yearRaw;
|
|
1288
|
+
const nextMonth = new Date(Date.UTC(year, month, 1)).getTime();
|
|
1289
|
+
return nextMonth > now;
|
|
1290
|
+
}
|
|
1291
|
+
function formatExpiry(raw) {
|
|
1292
|
+
const digits = raw.replace(/[^\d]/g, "").slice(0, 4);
|
|
1293
|
+
if (digits.length <= 2) return digits;
|
|
1294
|
+
return `${digits.slice(0, 2)}/${digits.slice(2)}`;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// src/index.ts
|
|
1298
|
+
js.registerElement("wallet", createWalletElement);
|
|
1299
|
+
js.registerElement("card", createCardElement);
|
|
1300
|
+
js.registerElement("payment", createPaymentElement);
|
|
1301
|
+
js.registerElement("express", createExpressCheckoutElement);
|
|
1302
|
+
js.registerElement("paymentMethodSelector", createPaymentMethodSelector);
|
|
1303
|
+
js.registerElement("billingAddress", createBillingAddressElement);
|
|
1304
|
+
js.registerElement("customer", createCustomerElement);
|
|
1305
|
+
js.registerElement("otp", createOtpElement);
|
|
1306
|
+
js.registerElement("qr", createQrElement);
|
|
1307
|
+
|
|
1308
|
+
exports.BaseElement = BaseElement;
|
|
1309
|
+
exports.BillingAddressElement = BillingAddressElement;
|
|
1310
|
+
exports.CardElement = CardElement;
|
|
1311
|
+
exports.CustomerElement = CustomerElement;
|
|
1312
|
+
exports.ExpressCheckoutElement = ExpressCheckoutElement;
|
|
1313
|
+
exports.OtpElement = OtpElement;
|
|
1314
|
+
exports.PaymentElement = PaymentElement;
|
|
1315
|
+
exports.PaymentMethodSelector = PaymentMethodSelector;
|
|
1316
|
+
exports.QrElement = QrElement;
|
|
1317
|
+
exports.WalletElement = WalletElement;
|
|
1318
|
+
exports.createBillingAddressElement = createBillingAddressElement;
|
|
1319
|
+
exports.createCardElement = createCardElement;
|
|
1320
|
+
exports.createCustomerElement = createCustomerElement;
|
|
1321
|
+
exports.createExpressCheckoutElement = createExpressCheckoutElement;
|
|
1322
|
+
exports.createOtpElement = createOtpElement;
|
|
1323
|
+
exports.createPaymentElement = createPaymentElement;
|
|
1324
|
+
exports.createPaymentMethodSelector = createPaymentMethodSelector;
|
|
1325
|
+
exports.createQrElement = createQrElement;
|
|
1326
|
+
exports.createWalletElement = createWalletElement;
|
|
1327
|
+
exports.cvcLengthFor = cvcLengthFor;
|
|
1328
|
+
exports.cvcValid = cvcValid;
|
|
1329
|
+
exports.detectBrand = detectBrand;
|
|
1330
|
+
exports.expiryValid = expiryValid;
|
|
1331
|
+
exports.formInputStyle = formInputStyle;
|
|
1332
|
+
exports.formLabelStyle = formLabelStyle;
|
|
1333
|
+
exports.formatCardNumber = formatCardNumber;
|
|
1334
|
+
exports.formatExpiry = formatExpiry;
|
|
1335
|
+
exports.luhnValid = luhnValid;
|
|
1336
|
+
exports.normalizeCardNumber = normalizeCardNumber;
|
|
1337
|
+
exports.resolveAppearance = resolveAppearance;
|