@code-collective/booking-widget 1.0.6 → 1.0.8
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/README.md +18 -1
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +1874 -2121
- package/dist/booking-widget.min.js +16 -7
- package/dist/booking-widget.umd.cjs +2 -2
- package/package.json +1 -1
- package/src/lib/CartBar.svelte +6 -11
- package/src/lib/CartOverviewButton.svelte +17 -47
- package/src/lib/Checkout.svelte +18 -2
- package/src/lib/CheckoutModal.svelte +189 -30
- package/src/lib/ContactForm.svelte +15 -6
- package/src/lib/EditBookingView.svelte +63 -7
- package/src/lib/PaymentPage.svelte +89 -86
- package/src/lib/ResultView.svelte +43 -3
- package/src/lib/TicketConfigurator.svelte +85 -26
- package/src/lib/api.ts +28 -2
- package/src/lib/cart-manager.ts +30 -4
- package/src/lib/client-types.ts +6 -0
- package/src/lib/config.ts +11 -8
- package/src/lib/elements/bw-cart.svelte +3 -12
- package/src/lib/elements/bw-checkout.svelte +9 -16
- package/src/lib/elements/bw-configurator.svelte +3 -13
- package/src/lib/elements/env.ts +0 -1
- package/src/lib/elements/register.ts +14 -3
- package/src/lib/generated-types.ts +1 -0
- package/src/lib/index.ts +30 -35
- package/src/lib/messages.ts +63 -3
- package/src/lib/peach-sdk.ts +40 -0
- package/src/lib/fake-api.ts +0 -282
- package/src/lib/mock-data.ts +0 -241
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import Checkout from '../Checkout.svelte';
|
|
5
5
|
import { resolveWizardPages, resolveEditPages } from '../config';
|
|
6
|
+
import { onWidgetMessage } from '../messages';
|
|
6
7
|
import { getSharedServices } from './shared';
|
|
7
8
|
|
|
8
9
|
let {
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
let isModal = $derived(isModalAttr !== undefined);
|
|
18
19
|
let showModal = $state(false);
|
|
19
20
|
|
|
20
|
-
const { api, ready: readyPromise } = getSharedServices();
|
|
21
|
+
const { api, cartManager, ready: readyPromise } = getSharedServices();
|
|
21
22
|
|
|
22
23
|
let ready = $state(false);
|
|
23
24
|
readyPromise.then(() => { ready = true; });
|
|
@@ -34,30 +35,22 @@
|
|
|
34
35
|
export function open() { showModal = true; }
|
|
35
36
|
export function close() { showModal = false; dispatch('bw:close', {}); }
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
let d: Record<string, unknown>;
|
|
39
|
-
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
40
|
-
catch { return; }
|
|
41
|
-
if (!d?.type) return;
|
|
42
|
-
|
|
38
|
+
$effect(() => onWidgetMessage((d) => {
|
|
43
39
|
if (d.type === 'modal:close') {
|
|
44
40
|
if (isModal) showModal = false;
|
|
45
41
|
dispatch('bw:close', {});
|
|
46
42
|
}
|
|
47
43
|
if (d.type === 'order:complete') {
|
|
48
|
-
|
|
44
|
+
// Deliberately does not close the modal - order:complete only reports that the order is confirmed
|
|
45
|
+
// (for host analytics/cart-clearing), not that the shopper has finished looking at the result screen.
|
|
46
|
+
// The modal stays open until they dismiss it themselves, which sends modal:close separately.
|
|
49
47
|
dispatch('bw:order-confirmed', {
|
|
50
48
|
cartToken: d.cartToken ?? '',
|
|
51
49
|
value: d.value ?? 0,
|
|
52
50
|
currency: d.currency ?? 'ZAR',
|
|
53
51
|
});
|
|
54
52
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
$effect(() => {
|
|
58
|
-
window.addEventListener('message', handleMessage);
|
|
59
|
-
return () => window.removeEventListener('message', handleMessage);
|
|
60
|
-
});
|
|
53
|
+
}));
|
|
61
54
|
|
|
62
55
|
// Listen for a custom 'bw:open' event so register.ts can trigger it
|
|
63
56
|
$effect(() => {
|
|
@@ -74,12 +67,12 @@
|
|
|
74
67
|
<div class="bw-modal-overlay" onclick={() => close()}>
|
|
75
68
|
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
76
69
|
<div class="bw-modal-inner" onclick={(e) => e.stopPropagation()}>
|
|
77
|
-
<Checkout {api} {wizardPages} {editPages} {autoSelectSingleTimeSlot} />
|
|
70
|
+
<Checkout {api} {cartManager} {wizardPages} {editPages} {autoSelectSingleTimeSlot} />
|
|
78
71
|
</div>
|
|
79
72
|
</div>
|
|
80
73
|
{/if}
|
|
81
74
|
{:else if ready}
|
|
82
|
-
<Checkout {api} {wizardPages} {editPages} {autoSelectSingleTimeSlot} />
|
|
75
|
+
<Checkout {api} {cartManager} {wizardPages} {editPages} {autoSelectSingleTimeSlot} />
|
|
83
76
|
{:else}
|
|
84
77
|
<div class="loading-center"><div class="spinner"></div></div>
|
|
85
78
|
{/if}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<script lang="ts">
|
|
4
4
|
import TicketConfigurator from '../TicketConfigurator.svelte';
|
|
5
5
|
import { resolveWizardPages } from '../config';
|
|
6
|
-
import { postMessage } from '../messages';
|
|
6
|
+
import { onWidgetMessage, postMessage } from '../messages';
|
|
7
7
|
import { getSharedServices } from './shared';
|
|
8
8
|
|
|
9
9
|
let {
|
|
@@ -26,12 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
// Forward postMessage events as CustomEvents
|
|
29
|
-
|
|
30
|
-
let d: Record<string, unknown>;
|
|
31
|
-
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
32
|
-
catch { return; }
|
|
33
|
-
if (!d?.type) return;
|
|
34
|
-
|
|
29
|
+
$effect(() => onWidgetMessage((d) => {
|
|
35
30
|
if (d.type === 'cart:change') {
|
|
36
31
|
dispatch('bw:cart-change', {
|
|
37
32
|
itemCount: d.itemCount ?? 0,
|
|
@@ -39,12 +34,7 @@
|
|
|
39
34
|
totalFormatted: d.totalFormatted ?? '',
|
|
40
35
|
});
|
|
41
36
|
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
$effect(() => {
|
|
45
|
-
window.addEventListener('message', handleMessage);
|
|
46
|
-
return () => window.removeEventListener('message', handleMessage);
|
|
47
|
-
});
|
|
37
|
+
}));
|
|
48
38
|
|
|
49
39
|
const onCancel = cancelable !== undefined
|
|
50
40
|
? () => { postMessage({ type: 'modal:close' }); dispatch('bw:cancel', {}); }
|
package/src/lib/elements/env.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import '../app.css';
|
|
2
2
|
import type { BookingApi } from '../api';
|
|
3
3
|
import { ApiClient } from '../api';
|
|
4
|
-
import { FakeApiClient } from '../fake-api';
|
|
5
4
|
import { SessionManager } from '../session-manager';
|
|
6
5
|
import { CartManager } from '../cart-manager';
|
|
7
|
-
import { defaultApiBaseUrl
|
|
6
|
+
import { defaultApiBaseUrl } from './env';
|
|
7
|
+
import { onWidgetMessage } from '../messages';
|
|
8
8
|
|
|
9
9
|
// Bootstrap shared services BEFORE element imports trigger connectedCallback.
|
|
10
10
|
// Elements read from window.__bwServices instead of importing shared.ts,
|
|
11
11
|
// because the IIFE bundler inlines imports per-element — a module-level
|
|
12
12
|
// singleton would be duplicated. window is the only truly shared scope.
|
|
13
13
|
const checkoutKey = document.querySelector('[checkout-key]')?.getAttribute('checkout-key') ?? '';
|
|
14
|
-
const api: BookingApi =
|
|
14
|
+
const api: BookingApi = new ApiClient(defaultApiBaseUrl);
|
|
15
15
|
const sessionManager = new SessionManager(api);
|
|
16
16
|
const cartManager = new CartManager(api);
|
|
17
17
|
sessionManager.startBackgroundRefresh();
|
|
@@ -22,6 +22,17 @@ import './bw-configurator.svelte';
|
|
|
22
22
|
import './bw-cart.svelte';
|
|
23
23
|
import './bw-checkout.svelte';
|
|
24
24
|
|
|
25
|
+
// Cart-global, not tied to any one element (unlike the bw:* events forwarded per-element in autoWire below),
|
|
26
|
+
// so this is wired once here rather than per bw-configurator/bw-cart/bw-checkout instance. Carries the same
|
|
27
|
+
// cart TicketConfigurator/CheckoutModal already fetch for their own posting - see those files' own comments -
|
|
28
|
+
// so a consumer can build a custom cart summary (item count, remaining time, item details) without calling
|
|
29
|
+
// the checkout API directly.
|
|
30
|
+
onWidgetMessage((d) => {
|
|
31
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
32
|
+
window.dispatchEvent(new CustomEvent('bw:cart-updated', { detail: { cart: d.cart } }));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
25
36
|
interface BwOptions {
|
|
26
37
|
shouldBottomCloseOnModal: boolean;
|
|
27
38
|
autoSelectSingleTimeSlot: boolean;
|
package/src/lib/index.ts
CHANGED
|
@@ -2,10 +2,11 @@ import './app.css';
|
|
|
2
2
|
import { mount, unmount } from 'svelte';
|
|
3
3
|
import type { WizardPages, CartOverviewDisplay } from './config';
|
|
4
4
|
import type { BookingApi } from './api';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
5
|
+
import type { CheckoutCartDetailDto } from './client-types';
|
|
6
|
+
import { ApiClient, ApiError } from './api';
|
|
7
7
|
import { SessionManager } from './session-manager';
|
|
8
8
|
import { CartManager } from './cart-manager';
|
|
9
|
+
import { onWidgetMessage } from './messages';
|
|
9
10
|
import TicketConfiguratorComponent from './TicketConfigurator.svelte';
|
|
10
11
|
import CheckoutComponent from './Checkout.svelte';
|
|
11
12
|
import CartOverviewComponent from './CartOverview.svelte';
|
|
@@ -18,7 +19,7 @@ export { default as CartBar } from './CartBar.svelte';
|
|
|
18
19
|
export { default as CartOverviewButton } from './CartOverviewButton.svelte';
|
|
19
20
|
|
|
20
21
|
// Re-export utilities
|
|
21
|
-
export { ApiClient,
|
|
22
|
+
export { ApiClient, ApiError, SessionManager, CartManager };
|
|
22
23
|
export { resolveWizardPages, resolveEditPages, DEFAULT_WIZARD_PAGES } from './config';
|
|
23
24
|
export type { BookingApi } from './api';
|
|
24
25
|
export type { WizardPages, WizardPageConfig, CartOverviewDisplay, WidgetMode, WizardWidgetType } from './config';
|
|
@@ -30,7 +31,11 @@ export type * from './client-types';
|
|
|
30
31
|
interface BaseConfig {
|
|
31
32
|
apiBaseUrl?: string;
|
|
32
33
|
checkoutKey?: string;
|
|
33
|
-
|
|
34
|
+
// Cart-global, not tied to any one mount function - fires after any add/edit/remove, anywhere on the page,
|
|
35
|
+
// carrying the same cart TicketConfigurator/CheckoutModal already fetch for their own internal use. Lets a
|
|
36
|
+
// consumer build a custom cart summary (item count, remaining time, item details) without calling the
|
|
37
|
+
// checkout API directly.
|
|
38
|
+
onCartUpdated?: (cart: CheckoutCartDetailDto | null) => void;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
export interface ConfiguratorConfig extends BaseConfig {
|
|
@@ -63,9 +68,7 @@ export interface MountedWidget {
|
|
|
63
68
|
// --- Shared bootstrap ---
|
|
64
69
|
|
|
65
70
|
function bootstrap(config: BaseConfig): { api: BookingApi; sessionManager: SessionManager; cartManager: CartManager } {
|
|
66
|
-
const api: BookingApi = config.
|
|
67
|
-
? new FakeApiClient()
|
|
68
|
-
: new ApiClient(config.apiBaseUrl ?? '');
|
|
71
|
+
const api: BookingApi = new ApiClient(config.apiBaseUrl ?? '');
|
|
69
72
|
|
|
70
73
|
const sessionManager = new SessionManager(api);
|
|
71
74
|
const cartManager = new CartManager(api);
|
|
@@ -94,12 +97,7 @@ export async function mountConfigurator(target: HTMLElement, config: Configurato
|
|
|
94
97
|
});
|
|
95
98
|
|
|
96
99
|
// Listen for postMessage events and forward to callbacks
|
|
97
|
-
|
|
98
|
-
let d: Record<string, unknown>;
|
|
99
|
-
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
100
|
-
catch { return; }
|
|
101
|
-
if (!d?.type) return;
|
|
102
|
-
|
|
100
|
+
const stopListening = onWidgetMessage((d) => {
|
|
103
101
|
if (d.type === 'cart:change' && config.onCartChange) {
|
|
104
102
|
config.onCartChange({
|
|
105
103
|
itemCount: (d.itemCount as number) ?? 0,
|
|
@@ -107,12 +105,14 @@ export async function mountConfigurator(target: HTMLElement, config: Configurato
|
|
|
107
105
|
totalFormatted: (d.totalFormatted as string) ?? '',
|
|
108
106
|
});
|
|
109
107
|
}
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
109
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
112
|
|
|
113
113
|
return {
|
|
114
114
|
destroy() {
|
|
115
|
-
|
|
115
|
+
stopListening();
|
|
116
116
|
sessionManager.stop();
|
|
117
117
|
unmount(component);
|
|
118
118
|
},
|
|
@@ -120,7 +120,7 @@ export async function mountConfigurator(target: HTMLElement, config: Configurato
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export async function mountCheckout(target: HTMLElement, config: CheckoutConfig): Promise<MountedWidget> {
|
|
123
|
-
const { api, sessionManager } = bootstrap(config);
|
|
123
|
+
const { api, sessionManager, cartManager } = bootstrap(config);
|
|
124
124
|
|
|
125
125
|
await sessionManager.ensureSession(config.checkoutKey ?? '');
|
|
126
126
|
|
|
@@ -128,18 +128,14 @@ export async function mountCheckout(target: HTMLElement, config: CheckoutConfig)
|
|
|
128
128
|
target,
|
|
129
129
|
props: {
|
|
130
130
|
api,
|
|
131
|
+
cartManager,
|
|
131
132
|
wizardPages: config.wizardPages,
|
|
132
133
|
editPages: config.editPages,
|
|
133
134
|
autoSelectSingleTimeSlot: config.autoSelectSingleTimeSlot ?? false,
|
|
134
135
|
},
|
|
135
136
|
});
|
|
136
137
|
|
|
137
|
-
|
|
138
|
-
let d: Record<string, unknown>;
|
|
139
|
-
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
140
|
-
catch { return; }
|
|
141
|
-
if (!d?.type) return;
|
|
142
|
-
|
|
138
|
+
const stopListening = onWidgetMessage((d) => {
|
|
143
139
|
if (d.type === 'modal:close' && config.onClose) config.onClose();
|
|
144
140
|
if (d.type === 'order:complete' && config.onOrderConfirmed) {
|
|
145
141
|
config.onOrderConfirmed({
|
|
@@ -148,12 +144,14 @@ export async function mountCheckout(target: HTMLElement, config: CheckoutConfig)
|
|
|
148
144
|
currency: (d.currency as string) ?? 'ZAR',
|
|
149
145
|
});
|
|
150
146
|
}
|
|
151
|
-
|
|
152
|
-
|
|
147
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
148
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
153
151
|
|
|
154
152
|
return {
|
|
155
153
|
destroy() {
|
|
156
|
-
|
|
154
|
+
stopListening();
|
|
157
155
|
sessionManager.stop();
|
|
158
156
|
unmount(component);
|
|
159
157
|
},
|
|
@@ -174,19 +172,16 @@ export async function mountCartOverview(target: HTMLElement, config: CartOvervie
|
|
|
174
172
|
},
|
|
175
173
|
});
|
|
176
174
|
|
|
177
|
-
|
|
178
|
-
let d: Record<string, unknown>;
|
|
179
|
-
try { d = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; }
|
|
180
|
-
catch { return; }
|
|
181
|
-
if (!d?.type) return;
|
|
182
|
-
|
|
175
|
+
const stopListening = onWidgetMessage((d) => {
|
|
183
176
|
if (d.type === 'modal:open' && config.onCheckout) config.onCheckout();
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
if (d.type === 'cart:updated' && 'cart' in d) {
|
|
178
|
+
config.onCartUpdated?.(d.cart as CheckoutCartDetailDto | null);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
186
181
|
|
|
187
182
|
return {
|
|
188
183
|
destroy() {
|
|
189
|
-
|
|
184
|
+
stopListening();
|
|
190
185
|
sessionManager.stop();
|
|
191
186
|
unmount(component);
|
|
192
187
|
},
|
package/src/lib/messages.ts
CHANGED
|
@@ -1,3 +1,63 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// The widget's own components talk to each other over postMessage - a cart added in bw-configurator has to
|
|
2
|
+
// reach the bar in bw-cart and the modal in bw-checkout, which are separate custom elements with no shared
|
|
3
|
+
// Svelte tree. That makes every one of these messages same-window and same-origin, and the guards below exist
|
|
4
|
+
// to keep it that way.
|
|
5
|
+
|
|
6
|
+
/** Messages this widget sends itself. Anything else on the wire is not ours. */
|
|
7
|
+
const WIDGET_MESSAGE_TYPES = new Set([
|
|
8
|
+
'cart:change',
|
|
9
|
+
'cart:updated',
|
|
10
|
+
'modal:open',
|
|
11
|
+
'modal:close',
|
|
12
|
+
'order:complete',
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Sends a widget message.
|
|
17
|
+
*
|
|
18
|
+
* Targets this window's own origin rather than '*'. These payloads carry `cart`, and CheckoutCartDetailDto
|
|
19
|
+
* includes `cartToken` - a bearer credential that authorises reading, modifying and paying for the cart. With
|
|
20
|
+
* '*' that went to whatever origin happened to be framing the widget, which in the standalone iframe build is
|
|
21
|
+
* not necessarily anyone we trust.
|
|
22
|
+
*/
|
|
23
|
+
export function postMessage(data: Record<string, unknown>): void {
|
|
24
|
+
window.parent?.postMessage(JSON.stringify(data), window.location.origin);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Subscribes to widget messages, ignoring anything that did not come from this widget.
|
|
29
|
+
*
|
|
30
|
+
* The elements mount directly into the merchant's own document (shadow: 'none'), so these listeners sit on
|
|
31
|
+
* the top-level window of a third-party page. Without this check, any iframe already on that page - an ad, a
|
|
32
|
+
* chat widget, a tag manager - could reach them with window.parent.postMessage and drive the widget: spoof
|
|
33
|
+
* the total on the payment consent screen, fire a forged order confirmation into the merchant's analytics, or
|
|
34
|
+
* close the modal mid-confirm on a card that has already been charged.
|
|
35
|
+
*
|
|
36
|
+
* Checking `source` is what does the real work: a message from another frame carries that frame's own window,
|
|
37
|
+
* never ours, and it cannot be spoofed. The origin check is belt-and-braces for the same-window case.
|
|
38
|
+
*/
|
|
39
|
+
export function onWidgetMessage(handler: (data: Record<string, unknown>) => void): () => void {
|
|
40
|
+
function listener(e: MessageEvent) {
|
|
41
|
+
if (e.source !== window || e.origin !== window.location.origin) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let data: Record<string, unknown>;
|
|
46
|
+
try {
|
|
47
|
+
data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data;
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Anything without one of our own type values is someone else's traffic sharing this window - a library
|
|
53
|
+
// or the host page talking to itself - not something to hand to a widget handler.
|
|
54
|
+
if (typeof data?.type !== 'string' || !WIDGET_MESSAGE_TYPES.has(data.type)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handler(data);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
window.addEventListener('message', listener);
|
|
62
|
+
return () => window.removeEventListener('message', listener);
|
|
63
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const SDK_URLS: Record<string, string> = {
|
|
2
|
+
prod: 'https://checkout.peachpayments.com/js/checkout.js',
|
|
3
|
+
qa: 'https://sandbox-checkout.peachpayments.com/js/checkout.js',
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
let loadPromise: Promise<void> | null = null;
|
|
7
|
+
|
|
8
|
+
// Lazily injects Peach's Copy&Pay SDK the first time PaymentPage mounts, so consumers embedding the widget
|
|
9
|
+
// (any of the three integration options) never have to know this script exists, let alone which
|
|
10
|
+
// environment's URL to point at - same window.BW_CHECKOUT_ENV convention elements/env.ts already uses for
|
|
11
|
+
// the checkout API base URL, with an explicit override for anything that doesn't fit that pattern.
|
|
12
|
+
export function loadPeachSdk(): Promise<void> {
|
|
13
|
+
if ((window as any).Checkout) {
|
|
14
|
+
return Promise.resolve();
|
|
15
|
+
}
|
|
16
|
+
if (loadPromise) {
|
|
17
|
+
return loadPromise;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const w = window as any;
|
|
21
|
+
const env: string = w.BW_CHECKOUT_ENV ?? 'prod';
|
|
22
|
+
const src: string = w.BW_CHECKOUT_PEACH_SDK_URL ?? SDK_URLS[env] ?? SDK_URLS.prod;
|
|
23
|
+
|
|
24
|
+
loadPromise = new Promise((resolve, reject) => {
|
|
25
|
+
const existing = document.querySelector(`script[src="${src}"]`);
|
|
26
|
+
if (existing) {
|
|
27
|
+
existing.addEventListener('load', () => resolve());
|
|
28
|
+
existing.addEventListener('error', () => reject(new Error('Failed to load the Peach Payments SDK script')));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const script = document.createElement('script');
|
|
33
|
+
script.src = src;
|
|
34
|
+
script.onload = () => resolve();
|
|
35
|
+
script.onerror = () => reject(new Error('Failed to load the Peach Payments SDK script'));
|
|
36
|
+
document.head.appendChild(script);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return loadPromise;
|
|
40
|
+
}
|