@code-collective/booking-widget 1.0.10 → 1.0.12
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 +46 -0
- package/README.md +500 -388
- package/dist/booking-widget.css +1 -1
- package/dist/booking-widget.js +2415 -1745
- package/dist/booking-widget.min.css +1 -1
- package/dist/booking-widget.min.js +39 -23
- package/dist/booking-widget.umd.cjs +5 -3
- package/package.json +58 -55
- package/src/lib/BookingProvider.svelte +21 -0
- package/src/lib/CartBar.svelte +26 -41
- package/src/lib/CartBarView.svelte +78 -61
- package/src/lib/CartExpiryGuard.svelte +15 -9
- package/src/lib/CartOverview.svelte +30 -20
- package/src/lib/CartOverviewButton.svelte +104 -101
- package/src/lib/Checkout.svelte +141 -128
- package/src/lib/CheckoutModal.svelte +838 -805
- package/src/lib/CheckoutPanel.svelte +121 -0
- package/src/lib/PaymentPage.svelte +191 -177
- package/src/lib/PickupPointPicker.svelte +1 -1
- package/src/lib/TicketConfigurator.svelte +166 -152
- package/src/lib/UnitCounter.svelte +16 -2
- package/src/lib/WizardPage.svelte +102 -35
- package/src/lib/app.css +0 -6
- package/src/lib/booking-context.ts +33 -0
- package/src/lib/cart-overview.svelte.ts +97 -0
- package/src/lib/config.ts +162 -153
- package/src/lib/elements/bw-cart.svelte +49 -35
- package/src/lib/elements/bw-checkout.svelte +97 -97
- package/src/lib/elements/bw-configurator.svelte +72 -56
- package/src/lib/elements/register.ts +171 -196
- package/src/lib/elements/shared.ts +18 -14
- package/src/lib/elements/theme.css +0 -6
- package/src/lib/host.svelte.ts +336 -0
- package/src/lib/index.ts +242 -196
- package/src/lib/layout.svelte.ts +52 -0
- package/src/lib/messages.ts +157 -77
- package/src/lib/peach-sdk.ts +86 -40
- package/src/lib/portal.ts +23 -0
- package/src/lib/CartExpiryGuard.test.ts +0 -331
- package/src/lib/CheckoutModal.confirm-outcome.test.ts +0 -91
- package/src/lib/CheckoutModal.payment-timeout.test.ts +0 -140
- package/src/lib/test/fixtures.ts +0 -107
- package/src/lib/test/messages-mock.ts +0 -34
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { BookingApi } from './api';
|
|
3
|
+
import type { WizardPages } from './config';
|
|
4
|
+
import { DEFAULT_WIZARD_PAGES } from './config';
|
|
5
|
+
import type { CheckoutCartDetailDto } from './client-types';
|
|
6
|
+
import { isInvalidCart } from './api';
|
|
7
|
+
import { onWidgetMessage, postMessage } from './messages';
|
|
8
|
+
import type { CartManager } from './cart-manager';
|
|
9
|
+
import CheckoutModal from './CheckoutModal.svelte';
|
|
10
|
+
import CartExpiredView from './CartExpiredView.svelte';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
api: BookingApi;
|
|
14
|
+
cartManager?: CartManager;
|
|
15
|
+
wizardPages?: WizardPages;
|
|
16
|
+
editPages?: WizardPages;
|
|
17
|
+
autoSelectSingleTimeSlot?: boolean;
|
|
18
|
+
}
|
|
19
|
+
let { api, cartManager, wizardPages = DEFAULT_WIZARD_PAGES, editPages: editPagesProp,
|
|
20
|
+
autoSelectSingleTimeSlot = false }: Props = $props();
|
|
21
|
+
let editPages = $derived(editPagesProp ?? wizardPages);
|
|
22
|
+
|
|
23
|
+
let cart = $state<CheckoutCartDetailDto | null>(null);
|
|
24
|
+
let isLoading = $state(true);
|
|
25
|
+
let expired = $state(false);
|
|
26
|
+
|
|
27
|
+
async function load() {
|
|
28
|
+
// A token this widget still holds for a cart the server has already let go (its window ran out while the
|
|
29
|
+
// tab sat there) used to render as a bare "No items in cart", with the dead token left in storage for the
|
|
30
|
+
// next add to trip over. Only a held token can mean that - with none, an empty cart is just empty.
|
|
31
|
+
const hadCart = api.cartToken !== '';
|
|
32
|
+
try {
|
|
33
|
+
cart = await api.getCart();
|
|
34
|
+
} catch (e) {
|
|
35
|
+
cart = null;
|
|
36
|
+
if (hadCart && isInvalidCart(e)) {
|
|
37
|
+
onCartExpired();
|
|
38
|
+
expired = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
isLoading = false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
load();
|
|
45
|
+
|
|
46
|
+
// Drops the stored token so whatever the shopper does next starts a fresh cart, and clears bw-cart's
|
|
47
|
+
// bar/button and any host-side summary built from bw:cart-updated - the cart they were describing no longer
|
|
48
|
+
// exists. Mirrors what onOrderConfirmed below does once a cart has served its purpose the other way.
|
|
49
|
+
function onCartExpired() {
|
|
50
|
+
cartManager?.reset();
|
|
51
|
+
postMessage({ type: 'cart:updated', cart: null });
|
|
52
|
+
postMessage({ type: 'cart:expired' });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// In modal mode, modal:close is what actually dismisses this screen - bw-checkout.svelte remounts this
|
|
56
|
+
// component fresh the next time it opens, so the click is fully handled there. Rendered permanently
|
|
57
|
+
// in-page instead, nothing is listening for modal:close, so without the reload below the click would do
|
|
58
|
+
// nothing a shopper can see: expired flips off, but cart is still the stale null from the load() that
|
|
59
|
+
// found it gone, leaving the same "No items in cart" text up with no sign anything happened.
|
|
60
|
+
function startAgain() {
|
|
61
|
+
expired = false;
|
|
62
|
+
postMessage({ type: 'modal:close' });
|
|
63
|
+
void load();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// In modal mode this is redundant - bw-checkout.svelte only mounts this component fresh each time the
|
|
67
|
+
// modal opens, so it already gets a current cart. Rendered permanently in-page instead (no is-modal), it
|
|
68
|
+
// would otherwise only ever see the cart as it was on first mount. The event already carries the fresh
|
|
69
|
+
// cart (see TicketConfigurator/CheckoutModal's own posting sites), so this applies it directly rather than
|
|
70
|
+
// triggering a second, redundant getCart() call or flashing the spinner over an already-visible cart.
|
|
71
|
+
$effect(() => onWidgetMessage((d) => {
|
|
72
|
+
if (d.type === 'cart:updated' && 'cart' in d) cart = d.cart as CheckoutCartDetailDto | null;
|
|
73
|
+
}));
|
|
74
|
+
</script>
|
|
75
|
+
|
|
76
|
+
<div class="bw-widget">
|
|
77
|
+
{#if isLoading}
|
|
78
|
+
<div class="loading-center" style="height:100vh">
|
|
79
|
+
<div class="spinner"></div>
|
|
80
|
+
</div>
|
|
81
|
+
{:else if cart && cart.items.length > 0}
|
|
82
|
+
<CheckoutModal
|
|
83
|
+
{cart}
|
|
84
|
+
{api}
|
|
85
|
+
{wizardPages}
|
|
86
|
+
{editPages}
|
|
87
|
+
{autoSelectSingleTimeSlot}
|
|
88
|
+
onClose={() => postMessage({ type: 'modal:close' })}
|
|
89
|
+
onOrderConfirmed={() => {
|
|
90
|
+
const total = cart!.items.reduce((s, i) => s + i.amount, 0);
|
|
91
|
+
const currency = cart!.items[0]?.currencyCode ?? 'ZAR';
|
|
92
|
+
postMessage({
|
|
93
|
+
type: 'order:complete',
|
|
94
|
+
cartToken: cart!.cartToken ?? '',
|
|
95
|
+
value: total,
|
|
96
|
+
currency,
|
|
97
|
+
});
|
|
98
|
+
// The cart is paid and confirmed, so its token has done its job. Nothing used to clear it, so it sat
|
|
99
|
+
// in localStorage on the merchant's origin until absoluteExpiresAt - readable by every third-party
|
|
100
|
+
// script they load, and picked up by the next person to use a shared or kiosk browser.
|
|
101
|
+
cartManager?.reset();
|
|
102
|
+
}}
|
|
103
|
+
/>
|
|
104
|
+
{:else if expired}
|
|
105
|
+
<div style="height:100vh">
|
|
106
|
+
<CartExpiredView onStartAgain={startAgain} />
|
|
107
|
+
</div>
|
|
108
|
+
{:else}
|
|
109
|
+
<div class="loading-center" style="height:100vh;color:var(--bw-color-text-secondary)">
|
|
110
|
+
No items in cart.
|
|
111
|
+
</div>
|
|
112
|
+
{/if}
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<style>
|
|
116
|
+
/* display: contents - a plain box here would break CheckoutModal's own .modal{height:100%}, which needs
|
|
117
|
+
to resolve against this component's real parent, not an unsized wrapper inserted in between. */
|
|
118
|
+
.bw-widget {
|
|
119
|
+
display: contents;
|
|
120
|
+
}
|
|
121
|
+
</style>
|
|
@@ -1,177 +1,191 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { onMount, onDestroy } from 'svelte';
|
|
3
|
-
import { loadPeachSdk } from './peach-sdk';
|
|
4
|
-
import CountdownTimer from './CountdownTimer.svelte';
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
checkoutId: string;
|
|
8
|
-
entityId: string;
|
|
9
|
-
onPaymentComplete: (result: { status: string }) => void;
|
|
10
|
-
// The cart's remaining life, shown in this dialog's own header so the shopper can see it over Peach's
|
|
11
|
-
// form - CheckoutModal's own cart timer is behind this overlay. Optional so callers that predate it need
|
|
12
|
-
// not pass one. CartExpiryGuard.svelte is what actually warns and acts as this counts down to zero - it
|
|
13
|
-
// renders above this overlay too, so there is nothing else for this dialog to do near the deadline.
|
|
14
|
-
expiresAt?: Date;
|
|
15
|
-
}
|
|
16
|
-
let { checkoutId, entityId, onPaymentComplete, expiresAt }: Props = $props();
|
|
17
|
-
|
|
18
|
-
let failed = $state(false);
|
|
19
|
-
let errorMessage = $state('');
|
|
20
|
-
|
|
21
|
-
let peachInstance: any = null;
|
|
22
|
-
|
|
23
|
-
onMount(() => {
|
|
24
|
-
(async () => {
|
|
25
|
-
try {
|
|
26
|
-
await loadPeachSdk();
|
|
27
|
-
|
|
28
|
-
const Checkout = (window as any).Checkout;
|
|
29
|
-
if (!Checkout) {
|
|
30
|
-
failed = true;
|
|
31
|
-
errorMessage = 'Payment SDK not loaded';
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
peachInstance = Checkout.initiate({
|
|
36
|
-
checkoutId,
|
|
37
|
-
// Peach's own "key" field, not our checkoutKey/entityId naming - without it Checkout.initiate()
|
|
38
|
-
// throws (Cannot read properties of undefined (reading 'trim')) before ever rendering anything,
|
|
39
|
-
// since the embedded widget authenticates to Peach directly with this rather than through us.
|
|
40
|
-
key: entityId,
|
|
41
|
-
customisations: {
|
|
42
|
-
card: { submitButtonText: 'Pay Now' },
|
|
43
|
-
theme: { brand: { primary: '#E30613' } },
|
|
44
|
-
// Peach's own Cancel (method-selection screen) and Back (card-entry screen) are the only
|
|
45
|
-
// back/exit controls in this dialog - we don't render one of our own alongside them, so there's
|
|
46
|
-
// exactly one way out per screen instead of ours and Peach's competing for the same job.
|
|
47
|
-
},
|
|
48
|
-
eventHandlers: {
|
|
49
|
-
onCompleted: () => { cleanup(); onPaymentComplete({ status: 'completed' }); },
|
|
50
|
-
onCancelled: () => { cleanup(); onPaymentComplete({ status: 'cancelled' }); },
|
|
51
|
-
onExpired: () => { cleanup(); onPaymentComplete({ status: 'expired' }); },
|
|
52
|
-
onError: () => { cleanup(); onPaymentComplete({ status: 'error' }); },
|
|
53
|
-
},
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
peachInstance.render('#peach-container');
|
|
57
|
-
} catch (e) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
</
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
.
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { loadPeachSdk, loadedPeachSdkUrl } from './peach-sdk';
|
|
4
|
+
import CountdownTimer from './CountdownTimer.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
checkoutId: string;
|
|
8
|
+
entityId: string;
|
|
9
|
+
onPaymentComplete: (result: { status: string }) => void;
|
|
10
|
+
// The cart's remaining life, shown in this dialog's own header so the shopper can see it over Peach's
|
|
11
|
+
// form - CheckoutModal's own cart timer is behind this overlay. Optional so callers that predate it need
|
|
12
|
+
// not pass one. CartExpiryGuard.svelte is what actually warns and acts as this counts down to zero - it
|
|
13
|
+
// renders above this overlay too, so there is nothing else for this dialog to do near the deadline.
|
|
14
|
+
expiresAt?: Date;
|
|
15
|
+
}
|
|
16
|
+
let { checkoutId, entityId, onPaymentComplete, expiresAt }: Props = $props();
|
|
17
|
+
|
|
18
|
+
let failed = $state(false);
|
|
19
|
+
let errorMessage = $state('');
|
|
20
|
+
|
|
21
|
+
let peachInstance: any = null;
|
|
22
|
+
|
|
23
|
+
onMount(() => {
|
|
24
|
+
(async () => {
|
|
25
|
+
try {
|
|
26
|
+
await loadPeachSdk();
|
|
27
|
+
|
|
28
|
+
const Checkout = (window as any).Checkout;
|
|
29
|
+
if (!Checkout) {
|
|
30
|
+
failed = true;
|
|
31
|
+
errorMessage = 'Payment SDK not loaded';
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
peachInstance = Checkout.initiate({
|
|
36
|
+
checkoutId,
|
|
37
|
+
// Peach's own "key" field, not our checkoutKey/entityId naming - without it Checkout.initiate()
|
|
38
|
+
// throws (Cannot read properties of undefined (reading 'trim')) before ever rendering anything,
|
|
39
|
+
// since the embedded widget authenticates to Peach directly with this rather than through us.
|
|
40
|
+
key: entityId,
|
|
41
|
+
customisations: {
|
|
42
|
+
card: { submitButtonText: 'Pay Now' },
|
|
43
|
+
theme: { brand: { primary: '#E30613' } },
|
|
44
|
+
// Peach's own Cancel (method-selection screen) and Back (card-entry screen) are the only
|
|
45
|
+
// back/exit controls in this dialog - we don't render one of our own alongside them, so there's
|
|
46
|
+
// exactly one way out per screen instead of ours and Peach's competing for the same job.
|
|
47
|
+
},
|
|
48
|
+
eventHandlers: {
|
|
49
|
+
onCompleted: () => { cleanup(); onPaymentComplete({ status: 'completed' }); },
|
|
50
|
+
onCancelled: () => { cleanup(); onPaymentComplete({ status: 'cancelled' }); },
|
|
51
|
+
onExpired: () => { cleanup(); onPaymentComplete({ status: 'expired' }); },
|
|
52
|
+
onError: () => { reportPeachFailure('Peach reported an error rendering the card form'); cleanup(); onPaymentComplete({ status: 'error' }); },
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
peachInstance.render('#peach-container');
|
|
57
|
+
} catch (e) {
|
|
58
|
+
reportPeachFailure(String(e));
|
|
59
|
+
failed = true;
|
|
60
|
+
errorMessage = String(e);
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Peach renders its own "an unrecoverable error has occurred" card inside its iframe, which tells a
|
|
66
|
+
// developer nothing about why. By far the most common cause is an SDK from one environment against a
|
|
67
|
+
// checkoutId created in another - the checkout API makes the checkout, so the two have to agree - and
|
|
68
|
+
// that is invisible unless the URL actually loaded is named.
|
|
69
|
+
function reportPeachFailure(reason: string) {
|
|
70
|
+
console.error(
|
|
71
|
+
`[booking-widget] Peach checkout did not render: ${reason}. ` +
|
|
72
|
+
`SDK loaded from ${loadedPeachSdkUrl() ?? 'an already-present window.Checkout'} for checkoutId ` +
|
|
73
|
+
`${checkoutId}. If that environment does not match the checkout API this cart came from, set ` +
|
|
74
|
+
`peachEnv on createBookingHost (or window.BW_CHECKOUT_PEACH_SDK_URL).`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cleanup() {
|
|
79
|
+
if (peachInstance) {
|
|
80
|
+
peachInstance.unmount();
|
|
81
|
+
peachInstance = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
onDestroy(cleanup);
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<!-- Peach's own card form renders inside a cross-origin iframe with its own Cancel/Back controls we have no
|
|
89
|
+
way to hide or reach (no SDK option, no CSS/JS access across origins) - see Checkout.initiate()'s
|
|
90
|
+
customisations doc comment above. Presenting it as its own nested dialog over the checkout modal, rather
|
|
91
|
+
than as just another view swapped into that modal's own body, means Peach's own Cancel/Back read as
|
|
92
|
+
belonging to Peach's own layer instead of visually competing with a second, separate Back of ours - so
|
|
93
|
+
this dialog has no back/exit control of its own at all; onCancelled below routes back to contact. -->
|
|
94
|
+
<div class="payment-overlay">
|
|
95
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
96
|
+
<div class="payment-dialog" onclick={(e) => e.stopPropagation()}>
|
|
97
|
+
<div class="page-header">
|
|
98
|
+
<h2>Payment</h2>
|
|
99
|
+
{#if expiresAt}
|
|
100
|
+
<span class="spacer"></span>
|
|
101
|
+
<CountdownTimer {expiresAt} />
|
|
102
|
+
{/if}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<div class="body">
|
|
106
|
+
{#if failed}
|
|
107
|
+
<div class="error-state">
|
|
108
|
+
<div class="error-icon">!</div>
|
|
109
|
+
<p class="error-title">Unable to load payment form</p>
|
|
110
|
+
<p class="error-msg">{errorMessage || 'The checkout session may have expired.'}</p>
|
|
111
|
+
<!-- Peach's own Cancel/Back never rendered, so this dialog needs one exit of its own here. Reported
|
|
112
|
+
as an error rather than a cancel: nothing was charged, and CheckoutModal's error path abandons
|
|
113
|
+
the attempt so a retry mints a fresh Peach checkout instead of reopening this one - which is the
|
|
114
|
+
only sensible outcome for a checkout that would not even render (e.g. one resumed after a reload
|
|
115
|
+
that Peach has since expired). -->
|
|
116
|
+
<button class="btn btn-primary" onclick={() => onPaymentComplete({ status: 'error' })}>Start over</button>
|
|
117
|
+
</div>
|
|
118
|
+
{:else}
|
|
119
|
+
<div id="peach-container"></div>
|
|
120
|
+
{/if}
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<style>
|
|
126
|
+
.payment-overlay {
|
|
127
|
+
position: fixed;
|
|
128
|
+
inset: 0;
|
|
129
|
+
/* Above bw-checkout's own .bw-modal-overlay (z-index 10000) so this reads as a second dialog opening
|
|
130
|
+
on top of the checkout modal, not content living inside it. */
|
|
131
|
+
z-index: 10001;
|
|
132
|
+
background: rgba(0, 0, 0, 0.5);
|
|
133
|
+
display: flex;
|
|
134
|
+
align-items: center;
|
|
135
|
+
justify-content: center;
|
|
136
|
+
padding: 16px;
|
|
137
|
+
}
|
|
138
|
+
.payment-dialog {
|
|
139
|
+
display: flex;
|
|
140
|
+
flex-direction: column;
|
|
141
|
+
width: 100%;
|
|
142
|
+
max-width: 480px;
|
|
143
|
+
height: min(700px, 90vh);
|
|
144
|
+
background: var(--bw-color-bg);
|
|
145
|
+
border-radius: var(--bw-radius-lg);
|
|
146
|
+
box-shadow: var(--bw-shadow-3);
|
|
147
|
+
overflow: hidden;
|
|
148
|
+
}
|
|
149
|
+
.body {
|
|
150
|
+
flex: 1;
|
|
151
|
+
/* A definite height (not just flex:1 alone) so #peach-container's own height:100% below has something
|
|
152
|
+
real to resolve against, and so this scrolls as a single region if content overflows - Peach's own
|
|
153
|
+
root is `height: inherit`, so without that it collapses to its 360px fallback regardless of how tall
|
|
154
|
+
the card form actually gets (e.g. once billing address fields are showing), and its submit button
|
|
155
|
+
ends up overlapping the fields above it. */
|
|
156
|
+
min-height: 0;
|
|
157
|
+
overflow-y: auto;
|
|
158
|
+
padding: 16px;
|
|
159
|
+
}
|
|
160
|
+
#peach-container {
|
|
161
|
+
height: 100%;
|
|
162
|
+
}
|
|
163
|
+
.error-state {
|
|
164
|
+
text-align: center;
|
|
165
|
+
padding: 32px 16px;
|
|
166
|
+
}
|
|
167
|
+
.error-icon {
|
|
168
|
+
width: 48px;
|
|
169
|
+
height: 48px;
|
|
170
|
+
border-radius: 50%;
|
|
171
|
+
border: 2px solid var(--bw-color-border);
|
|
172
|
+
display: flex;
|
|
173
|
+
align-items: center;
|
|
174
|
+
justify-content: center;
|
|
175
|
+
font-size: 24px;
|
|
176
|
+
color: var(--bw-color-text-secondary);
|
|
177
|
+
margin: 0 auto 16px;
|
|
178
|
+
}
|
|
179
|
+
.error-title {
|
|
180
|
+
font-size: 16px;
|
|
181
|
+
font-weight: 600;
|
|
182
|
+
margin-bottom: 8px;
|
|
183
|
+
}
|
|
184
|
+
.error-msg {
|
|
185
|
+
font-size: 14px;
|
|
186
|
+
color: var(--bw-color-text-secondary);
|
|
187
|
+
}
|
|
188
|
+
.error-state .btn {
|
|
189
|
+
margin-top: 16px;
|
|
190
|
+
}
|
|
191
|
+
</style>
|