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