@eventlook/sdk 1.7.3 → 1.7.4
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/cjs/index-EJYDEfV5.js +41925 -0
- package/dist/cjs/index-EJYDEfV5.js.map +1 -0
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/index.umd-CfKeY_Zj.js +13397 -0
- package/dist/cjs/index.umd-CfKeY_Zj.js.map +1 -0
- package/dist/esm/index-CAjHrQdF.js +41904 -0
- package/dist/esm/index-CAjHrQdF.js.map +1 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.umd-DjIEPZqJ.js +13395 -0
- package/dist/esm/index.umd-DjIEPZqJ.js.map +1 -0
- package/dist/types/form/Payment.d.ts +1 -0
- package/dist/types/form/payment/StripeCheckoutProvider.d.ts +36 -0
- package/dist/types/hooks/data/useStripeConfig.d.ts +3 -0
- package/dist/types/locales/cs.d.ts +3 -0
- package/dist/types/locales/en.d.ts +3 -0
- package/dist/types/locales/es.d.ts +3 -0
- package/dist/types/locales/pl.d.ts +3 -0
- package/dist/types/locales/sk.d.ts +3 -0
- package/dist/types/locales/uk.d.ts +3 -0
- package/dist/types/modules/order.d.ts +3 -0
- package/dist/types/utils/types/order.type.d.ts +2 -0
- package/dist/types/utils/types/payment-method.type.d.ts +1 -0
- package/package.json +3 -1
- package/src/form/Payment.tsx +42 -3
- package/src/form/PaymentOverviewBox.tsx +28 -9
- package/src/form/TicketForm.tsx +262 -167
- package/src/form/payment/StripeCheckoutProvider.tsx +154 -0
- package/src/form/tickets/TicketSelectionMobile.tsx +1 -1
- package/src/hooks/data/useStripeConfig.ts +14 -0
- package/src/locales/cs.tsx +3 -0
- package/src/locales/en.tsx +3 -0
- package/src/locales/es.tsx +3 -0
- package/src/locales/pl.tsx +3 -0
- package/src/locales/sk.tsx +3 -0
- package/src/locales/uk.tsx +3 -0
- package/src/modules/order.ts +3 -0
- package/src/utils/types/order.type.ts +5 -0
- package/src/utils/types/payment-method.type.ts +1 -0
package/src/form/TicketForm.tsx
CHANGED
|
@@ -63,6 +63,21 @@ import { getPlaceAsString } from '@utils/place';
|
|
|
63
63
|
import Services from '@form/services';
|
|
64
64
|
import PaydroidPage from './paydroid/PaydroidPage';
|
|
65
65
|
import { isPaydroidPage } from '@utils/page';
|
|
66
|
+
import StripeCheckoutProvider, { StripeCheckoutApi } from '@form/payment/StripeCheckoutProvider';
|
|
67
|
+
import useAllowedPaymentMethods from '@hooks/data/useAllowedPaymentMethods';
|
|
68
|
+
import useStripeConfig from '@hooks/data/useStripeConfig';
|
|
69
|
+
|
|
70
|
+
// Append the order number so the post-payment confirmation poll (PaymentSuccess)
|
|
71
|
+
// can resolve the order — same ?id contract the hosted-gateway return used.
|
|
72
|
+
const withOrderId = (url: string, orderNumber: number) => {
|
|
73
|
+
try {
|
|
74
|
+
const u = new URL(url);
|
|
75
|
+
u.searchParams.set('id', String(orderNumber));
|
|
76
|
+
return u.toString();
|
|
77
|
+
} catch {
|
|
78
|
+
return url;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
66
81
|
|
|
67
82
|
interface Props {
|
|
68
83
|
event: IEvent;
|
|
@@ -98,12 +113,31 @@ const TicketForm: React.FC<Props> = ({
|
|
|
98
113
|
isInline,
|
|
99
114
|
headerSlot,
|
|
100
115
|
}) => {
|
|
101
|
-
const {
|
|
102
|
-
|
|
116
|
+
const {
|
|
117
|
+
t,
|
|
118
|
+
lang,
|
|
119
|
+
setGlobal,
|
|
120
|
+
callbacks,
|
|
121
|
+
links,
|
|
122
|
+
user,
|
|
123
|
+
options,
|
|
124
|
+
showSnackbar,
|
|
125
|
+
content,
|
|
126
|
+
seatingIframeUrl,
|
|
127
|
+
} = useGlobal();
|
|
103
128
|
const { transformErrors } = useErrors(t('event.tickets.error.order'));
|
|
104
129
|
const { data: eventProducts, isLoading } = useActiveEventProducts(event.id);
|
|
105
130
|
const [paymentRedirect, setPaymentRedirect] = useState<string | null>(null);
|
|
106
131
|
const [hasGopayId, setHasGopayId] = useState<boolean>(hasGopayIdSsr);
|
|
132
|
+
|
|
133
|
+
// Inline Stripe (Payment Element, expand-on-select). The Elements provider is
|
|
134
|
+
// mounted up front when a Stripe method is on offer and the publishable key is
|
|
135
|
+
// loaded; the form's submit handler drives confirmation via this ref.
|
|
136
|
+
const { data: allowedPaymentMethods } = useAllowedPaymentMethods(event.currency, event.id);
|
|
137
|
+
const stripeMethod = allowedPaymentMethods?.find((m) => m.provider === 'STRIPE');
|
|
138
|
+
const { publishableKey } = useStripeConfig(!!stripeMethod);
|
|
139
|
+
const stripeOffered = !!stripeMethod && !!publishableKey;
|
|
140
|
+
const stripeCheckoutRef = useRef<StripeCheckoutApi | null>(null);
|
|
107
141
|
const [page, setPage] = useState<string | null>(null);
|
|
108
142
|
const [isPaying, setIsPaying] = useState<boolean>(false);
|
|
109
143
|
const [formStep, setFormStep] = useState<number>(1);
|
|
@@ -390,6 +424,14 @@ const TicketForm: React.FC<Props> = ({
|
|
|
390
424
|
const cartItemCount = getCartUniqueItemCount(values);
|
|
391
425
|
const onInvalid = useScrollToFirstError(methods);
|
|
392
426
|
|
|
427
|
+
// Stripe deferred Elements rejects amounts below the per-currency minimum, so
|
|
428
|
+
// only mount it once there's a real, positive cart total (in minor units).
|
|
429
|
+
// Free orders with items verify the card instead (SetupIntent, no charge).
|
|
430
|
+
const stripeAmountMinor = Math.round((Number(values.total) || 0) * 100);
|
|
431
|
+
const isStripeVerification = stripeAmountMinor === 0 && cartItemCount > 0;
|
|
432
|
+
const stripeMode: 'payment' | 'setup' = isStripeVerification ? 'setup' : 'payment';
|
|
433
|
+
const stripeReady = stripeOffered && (stripeAmountMinor > 0 || isStripeVerification);
|
|
434
|
+
|
|
393
435
|
const onSubmit = async (values: ITicketForm) => {
|
|
394
436
|
if (cartItemCount <= 0) {
|
|
395
437
|
showSnackbar(t('form.validation.count_tickets_or_products'), {
|
|
@@ -427,6 +469,21 @@ const TicketForm: React.FC<Props> = ({
|
|
|
427
469
|
},
|
|
428
470
|
{} as Record<number, ITicketFormTicket[]>
|
|
429
471
|
);
|
|
472
|
+
// Inline Stripe (expand-on-select): validate + collect the card fields
|
|
473
|
+
// before creating the order, so a card error never leaves a dangling
|
|
474
|
+
// order/reservation behind.
|
|
475
|
+
const selectedMethod = allowedPaymentMethods?.find(
|
|
476
|
+
(m) => m.id === Number(values.paymentMethodId)
|
|
477
|
+
);
|
|
478
|
+
const isStripeSelected = selectedMethod?.provider === 'STRIPE';
|
|
479
|
+
if (isStripeSelected) {
|
|
480
|
+
const cardError = await stripeCheckoutRef.current?.submit();
|
|
481
|
+
if (cardError) {
|
|
482
|
+
showSnackbar(cardError, { variant: 'error' });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
430
487
|
const { data: orderData } = await postOrder(data);
|
|
431
488
|
localStorage.removeItem('cartToken');
|
|
432
489
|
const items = [
|
|
@@ -482,6 +539,32 @@ const TicketForm: React.FC<Props> = ({
|
|
|
482
539
|
gtmPurchase(null);
|
|
483
540
|
gtmPurchase(item, pixels, userData);
|
|
484
541
|
}
|
|
542
|
+
// Inline Stripe: confirm the PaymentIntent in-page (no redirect). On
|
|
543
|
+
// success land on the order page with ?id=<orderNumber> so the existing
|
|
544
|
+
// PaymentSuccess poll + webhook finalize the order.
|
|
545
|
+
if (orderData.clientSecret) {
|
|
546
|
+
const returnUrl = withOrderId(
|
|
547
|
+
data.callback || window.location.href,
|
|
548
|
+
orderData.orderEntity.number
|
|
549
|
+
);
|
|
550
|
+
const result = await stripeCheckoutRef.current?.confirm(
|
|
551
|
+
orderData.clientSecret,
|
|
552
|
+
returnUrl
|
|
553
|
+
);
|
|
554
|
+
if (!result?.ok) {
|
|
555
|
+
showSnackbar(result?.error || t('form.labels.card_payment_error'), {
|
|
556
|
+
variant: 'error',
|
|
557
|
+
});
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (!isInline && isIframe) {
|
|
561
|
+
parent.postMessage({ type: 'eventlookFrameGopayRedirect', gwUrl: returnUrl }, '*');
|
|
562
|
+
} else {
|
|
563
|
+
window.location.replace(returnUrl);
|
|
564
|
+
}
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
485
568
|
setPaymentRedirect(orderData.gwUrl);
|
|
486
569
|
methods.reset();
|
|
487
570
|
} catch (err: any) {
|
|
@@ -577,204 +660,216 @@ const TicketForm: React.FC<Props> = ({
|
|
|
577
660
|
isInline={isInline}
|
|
578
661
|
/>
|
|
579
662
|
) : (
|
|
580
|
-
<
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
663
|
+
<StripeCheckoutProvider
|
|
664
|
+
active={stripeReady}
|
|
665
|
+
publishableKey={publishableKey}
|
|
666
|
+
mode={stripeMode}
|
|
667
|
+
amount={stripeAmountMinor}
|
|
668
|
+
currency={event.currency}
|
|
669
|
+
locale={lang}
|
|
670
|
+
apiRef={stripeCheckoutRef}
|
|
585
671
|
>
|
|
586
|
-
<
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}}
|
|
672
|
+
<FormProvider
|
|
673
|
+
methods={methods}
|
|
674
|
+
// @ts-ignore -- handleSubmit type mismatch with FormProvider onSubmit prop
|
|
675
|
+
onSubmit={methods.handleSubmit(onSubmit, onInvalid)}
|
|
676
|
+
formId={EVENTLOOK_ORDER_FORM_ID}
|
|
592
677
|
>
|
|
593
|
-
<
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
678
|
+
<Stack
|
|
679
|
+
className="overview-card__event-info"
|
|
680
|
+
display={{ md: 'none' }}
|
|
681
|
+
sx={{
|
|
682
|
+
mb: 2,
|
|
683
|
+
}}
|
|
684
|
+
>
|
|
685
|
+
<Typography variant="h3" component="h1">
|
|
686
|
+
{event.name}
|
|
687
|
+
</Typography>
|
|
688
|
+
<Typography variant="h5" component="h2">
|
|
689
|
+
{dayjs(event.startDate).format('DD.MM.YYYY HH:mm')}
|
|
690
|
+
</Typography>
|
|
691
|
+
<Typography variant="body2" mt={1}>
|
|
692
|
+
{getPlaceAsString(event.place)}
|
|
693
|
+
</Typography>
|
|
694
|
+
{headerSlot ? <>{headerSlot}</> : null}
|
|
695
|
+
</Stack>
|
|
696
|
+
<Grid
|
|
697
|
+
container
|
|
698
|
+
spacing={2}
|
|
699
|
+
sx={{
|
|
700
|
+
pb: {
|
|
701
|
+
xs: isPaymentOverviewDrawerOpen ? cartItemCount * 4 + 18 : 0,
|
|
702
|
+
md: 0,
|
|
703
|
+
},
|
|
704
|
+
}}
|
|
705
|
+
>
|
|
706
|
+
<Grid size={{ xs: 12, md: 8 }}>
|
|
707
|
+
<Stepper
|
|
708
|
+
orientation="vertical"
|
|
709
|
+
sx={(theme) => ({
|
|
710
|
+
[theme.breakpoints.down('sm')]: {
|
|
711
|
+
'& .MuiStepContent-root': {
|
|
712
|
+
borderLeftWidth: 0,
|
|
713
|
+
paddingLeft: 0,
|
|
714
|
+
marginLeft: 0,
|
|
715
|
+
},
|
|
716
|
+
'& .MuiStepConnector-line': { borderLeftWidth: 0 },
|
|
623
717
|
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
718
|
+
})}
|
|
719
|
+
>
|
|
720
|
+
{event.type === EventType.RECURRING && (
|
|
721
|
+
<Step active>
|
|
722
|
+
<StepLabel>{t('event.tickets.stepper.6.title')}</StepLabel>
|
|
723
|
+
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
724
|
+
<TimeslotSelection event={event} />
|
|
725
|
+
</StepContent>
|
|
726
|
+
</Step>
|
|
727
|
+
)}
|
|
629
728
|
<Step active>
|
|
630
|
-
<StepLabel>{t('event.tickets.stepper.
|
|
729
|
+
<StepLabel>{t('event.tickets.stepper.1.title')}</StepLabel>
|
|
631
730
|
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
632
|
-
|
|
731
|
+
{event.mapId && seatingIframeUrl ? (
|
|
732
|
+
<TicketSelectionMap event={event} />
|
|
733
|
+
) : event.hasMerchandise ? (
|
|
734
|
+
<TicketWithMerchandiseSelection event={event} />
|
|
735
|
+
) : (
|
|
736
|
+
<TicketSelection event={event} />
|
|
737
|
+
)}
|
|
633
738
|
</StepContent>
|
|
634
739
|
</Step>
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
</Step>
|
|
648
|
-
{event.hasMerchandise && eventProducts.length && (
|
|
740
|
+
{event.hasMerchandise && eventProducts.length && (
|
|
741
|
+
<Step active>
|
|
742
|
+
<StepLabel>{t('event.tickets.stepper.4.title')}</StepLabel>
|
|
743
|
+
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
744
|
+
<MerchandiseSelection
|
|
745
|
+
eventProducts={eventProducts}
|
|
746
|
+
eventId={event.id}
|
|
747
|
+
isLoading={isLoading}
|
|
748
|
+
/>
|
|
749
|
+
</StepContent>
|
|
750
|
+
</Step>
|
|
751
|
+
)}
|
|
649
752
|
<Step active>
|
|
650
|
-
<StepLabel>{t('event.tickets.stepper.
|
|
753
|
+
<StepLabel>{t('event.tickets.stepper.8.title')}</StepLabel>
|
|
651
754
|
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
652
|
-
<
|
|
653
|
-
eventProducts={eventProducts}
|
|
654
|
-
eventId={event.id}
|
|
655
|
-
isLoading={isLoading}
|
|
656
|
-
/>
|
|
755
|
+
<Services event={event} />
|
|
657
756
|
</StepContent>
|
|
658
757
|
</Step>
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
758
|
+
{event.children.length && (
|
|
759
|
+
<Step active>
|
|
760
|
+
<StepLabel>{t('event.tickets.stepper.7.title')}</StepLabel>
|
|
761
|
+
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
762
|
+
<ChildEventSection events={event.children} />
|
|
763
|
+
</StepContent>
|
|
764
|
+
</Step>
|
|
765
|
+
)}
|
|
667
766
|
<Step active>
|
|
668
|
-
<StepLabel>{t('event.tickets.stepper.
|
|
767
|
+
<StepLabel>{t('event.tickets.stepper.2.title')}</StepLabel>
|
|
669
768
|
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
670
|
-
<
|
|
769
|
+
<ContactPerson event={event} />
|
|
671
770
|
</StepContent>
|
|
672
771
|
</Step>
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
772
|
+
{event.hasMerchandise && showShippingMethods() && (
|
|
773
|
+
<Step active>
|
|
774
|
+
<StepLabel>{t('event.tickets.stepper.5.title')}</StepLabel>
|
|
775
|
+
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
776
|
+
<Shipping event={event} />
|
|
777
|
+
</StepContent>
|
|
778
|
+
</Step>
|
|
779
|
+
)}
|
|
681
780
|
<Step active>
|
|
682
|
-
<StepLabel>
|
|
781
|
+
<StepLabel>
|
|
782
|
+
{t(
|
|
783
|
+
`event.tickets.stepper.3.${values.isPaymentVerify ? 'title_verify' : 'title'}`
|
|
784
|
+
)}
|
|
785
|
+
</StepLabel>
|
|
683
786
|
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
684
|
-
<
|
|
787
|
+
<Payment event={event} stripeReady={stripeReady} />
|
|
685
788
|
</StepContent>
|
|
686
789
|
</Step>
|
|
687
|
-
|
|
688
|
-
<
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
<StepContent sx={{ pr: { xs: 0 } }}>
|
|
695
|
-
<Payment event={event} />
|
|
696
|
-
</StepContent>
|
|
697
|
-
</Step>
|
|
698
|
-
</Stepper>
|
|
699
|
-
<Stack
|
|
700
|
-
ref={termsAndConditionsRef}
|
|
701
|
-
mt={2}
|
|
702
|
-
ml={{ xs: 1, md: 4 }}
|
|
703
|
-
sx={{ scrollMarginBottom: { xs: 220, md: 0 } }}
|
|
704
|
-
>
|
|
705
|
-
<RHFCheckbox
|
|
706
|
-
name="termsAndConditions"
|
|
707
|
-
label={
|
|
708
|
-
<>
|
|
709
|
-
<Trans
|
|
710
|
-
text="event.tickets.terms_and_conditions"
|
|
711
|
-
values={{
|
|
712
|
-
termsAndConditionsCompanies: options?.termsAndConditionsCompanies
|
|
713
|
-
? options.termsAndConditionsCompanies.join(t('and'))
|
|
714
|
-
: ['Eventlook', 'GoPay'].join(` ${t('and')} `),
|
|
715
|
-
}}
|
|
716
|
-
components={{
|
|
717
|
-
0: <CustomLink key={2} href={links.termsAndConditions} target="_blank" />,
|
|
718
|
-
1: <CustomLink key={1} href={links.gdpr} target="_blank" />,
|
|
719
|
-
}}
|
|
720
|
-
/>
|
|
721
|
-
</>
|
|
722
|
-
}
|
|
723
|
-
/>
|
|
724
|
-
{values.ticketInsurance && (
|
|
790
|
+
</Stepper>
|
|
791
|
+
<Stack
|
|
792
|
+
ref={termsAndConditionsRef}
|
|
793
|
+
mt={2}
|
|
794
|
+
ml={{ xs: 1, md: 4 }}
|
|
795
|
+
sx={{ scrollMarginBottom: { xs: 220, md: 0 } }}
|
|
796
|
+
>
|
|
725
797
|
<RHFCheckbox
|
|
726
|
-
name="
|
|
798
|
+
name="termsAndConditions"
|
|
727
799
|
label={
|
|
728
800
|
<>
|
|
729
801
|
<Trans
|
|
730
|
-
text="event.tickets.
|
|
802
|
+
text="event.tickets.terms_and_conditions"
|
|
803
|
+
values={{
|
|
804
|
+
termsAndConditionsCompanies: options?.termsAndConditionsCompanies
|
|
805
|
+
? options.termsAndConditionsCompanies.join(t('and'))
|
|
806
|
+
: ['Eventlook', 'GoPay'].join(` ${t('and')} `),
|
|
807
|
+
}}
|
|
731
808
|
components={{
|
|
732
809
|
0: (
|
|
733
|
-
<CustomLink
|
|
734
|
-
key={2}
|
|
735
|
-
href="https://eventigo.s3-central.vshosting.cloud/production/colonnade/pp-storno-cz-eventlook-012026.pdf"
|
|
736
|
-
target="_blank"
|
|
737
|
-
/>
|
|
738
|
-
),
|
|
739
|
-
1: (
|
|
740
|
-
<CustomLink
|
|
741
|
-
key={1}
|
|
742
|
-
href="https://eventigo.s3-central.vshosting.cloud/production/colonnade/ipid-storno-cz-112025.pdf"
|
|
743
|
-
target="_blank"
|
|
744
|
-
/>
|
|
810
|
+
<CustomLink key={2} href={links.termsAndConditions} target="_blank" />
|
|
745
811
|
),
|
|
812
|
+
1: <CustomLink key={1} href={links.gdpr} target="_blank" />,
|
|
746
813
|
}}
|
|
747
814
|
/>
|
|
748
815
|
</>
|
|
749
816
|
}
|
|
750
817
|
/>
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
818
|
+
{values.ticketInsurance && (
|
|
819
|
+
<RHFCheckbox
|
|
820
|
+
name="insuranceTermsAndConditions"
|
|
821
|
+
label={
|
|
822
|
+
<>
|
|
823
|
+
<Trans
|
|
824
|
+
text="event.tickets.insurance.checkbox"
|
|
825
|
+
components={{
|
|
826
|
+
0: (
|
|
827
|
+
<CustomLink
|
|
828
|
+
key={2}
|
|
829
|
+
href="https://eventigo.s3-central.vshosting.cloud/production/colonnade/pp-storno-cz-eventlook-012026.pdf"
|
|
830
|
+
target="_blank"
|
|
831
|
+
/>
|
|
832
|
+
),
|
|
833
|
+
1: (
|
|
834
|
+
<CustomLink
|
|
835
|
+
key={1}
|
|
836
|
+
href="https://eventigo.s3-central.vshosting.cloud/production/colonnade/ipid-storno-cz-112025.pdf"
|
|
837
|
+
target="_blank"
|
|
838
|
+
/>
|
|
839
|
+
),
|
|
840
|
+
}}
|
|
841
|
+
/>
|
|
842
|
+
</>
|
|
843
|
+
}
|
|
844
|
+
/>
|
|
845
|
+
)}
|
|
846
|
+
</Stack>
|
|
847
|
+
</Grid>
|
|
848
|
+
<Grid size={12} sx={{ display: { xs: 'block', md: 'none' } }}>
|
|
849
|
+
<Divider sx={{ borderStyle: 'dashed' }} />
|
|
850
|
+
</Grid>
|
|
851
|
+
<Grid size={{ xs: 12, md: 4 }} sx={{ mt: { xs: 0, md: 0 } }}>
|
|
852
|
+
<PaymentOverviewBox event={event} withoutPadding />
|
|
853
|
+
</Grid>
|
|
756
854
|
</Grid>
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
855
|
+
|
|
856
|
+
{!isIframe && (
|
|
857
|
+
<PaymentOverviewDrawer
|
|
858
|
+
event={event}
|
|
859
|
+
totalPrice={values.total}
|
|
860
|
+
termsAndConditionsRef={termsAndConditionsRef}
|
|
861
|
+
onOpenChange={setIsPaymentOverviewDrawerOpen}
|
|
862
|
+
/>
|
|
863
|
+
)}
|
|
864
|
+
|
|
865
|
+
<EmailConfirmation
|
|
866
|
+
open={formStep === 2 && !isIframe}
|
|
867
|
+
onClose={() => setFormStep(1)}
|
|
868
|
+
// @ts-ignore -- handleSubmit type mismatch with onConfirm prop
|
|
869
|
+
onConfirm={methods.handleSubmit(onSubmit, onInvalid)}
|
|
768
870
|
/>
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
<EmailConfirmation
|
|
772
|
-
open={formStep === 2 && !isIframe}
|
|
773
|
-
onClose={() => setFormStep(1)}
|
|
774
|
-
// @ts-ignore -- handleSubmit type mismatch with onConfirm prop
|
|
775
|
-
onConfirm={methods.handleSubmit(onSubmit, onInvalid)}
|
|
776
|
-
/>
|
|
777
|
-
</FormProvider>
|
|
871
|
+
</FormProvider>
|
|
872
|
+
</StripeCheckoutProvider>
|
|
778
873
|
)}
|
|
779
874
|
</Box>
|
|
780
875
|
);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { MutableRefObject, useEffect, useMemo } from 'react';
|
|
4
|
+
import { useTheme } from '@mui/material';
|
|
5
|
+
import { loadStripe, Appearance } from '@stripe/stripe-js';
|
|
6
|
+
import { Elements, useElements, useStripe } from '@stripe/react-stripe-js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Imperative handle the order form uses to drive the inline Stripe payment from
|
|
10
|
+
* its own submit handler (which lives outside the <Elements> tree).
|
|
11
|
+
*/
|
|
12
|
+
export interface StripeCheckoutApi {
|
|
13
|
+
ready: boolean;
|
|
14
|
+
/** Validate + collect the card fields. Returns an error message, or null. */
|
|
15
|
+
submit: () => Promise<string | null>;
|
|
16
|
+
/** Confirm the server-created PaymentIntent. */
|
|
17
|
+
confirm: (clientSecret: string, returnUrl: string) => Promise<{ ok: boolean; error?: string }>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Locales the Stripe Elements UI supports; anything else falls back to auto.
|
|
21
|
+
const STRIPE_LOCALES = ['cs', 'de', 'en', 'es', 'fr', 'pl', 'sk', 'it'];
|
|
22
|
+
|
|
23
|
+
// Bridges the Stripe/Elements instances (only available inside <Elements>) up to
|
|
24
|
+
// a ref the form's submit handler can call.
|
|
25
|
+
const Bridge: React.FC<{ apiRef: MutableRefObject<StripeCheckoutApi | null> }> = ({ apiRef }) => {
|
|
26
|
+
const stripe = useStripe();
|
|
27
|
+
const elements = useElements();
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
apiRef.current = {
|
|
31
|
+
ready: !!stripe && !!elements,
|
|
32
|
+
submit: async () => {
|
|
33
|
+
if (!elements) return 'Stripe not ready';
|
|
34
|
+
const { error } = await elements.submit();
|
|
35
|
+
return error?.message ?? null;
|
|
36
|
+
},
|
|
37
|
+
confirm: async (clientSecret, returnUrl) => {
|
|
38
|
+
if (!stripe || !elements) return { ok: false, error: 'Stripe not ready' };
|
|
39
|
+
// Resolve in-page when possible (incl. the 3-D Secure modal); Stripe
|
|
40
|
+
// only does a full redirect when the method strictly requires it.
|
|
41
|
+
const confirmParams = { return_url: returnUrl };
|
|
42
|
+
|
|
43
|
+
// Free (0-amount) orders confirm a SetupIntent (seti_…) — card
|
|
44
|
+
// verification for bot protection, no charge; paid orders confirm a
|
|
45
|
+
// PaymentIntent (pi_…).
|
|
46
|
+
if (clientSecret.startsWith('seti_')) {
|
|
47
|
+
const { error, setupIntent } = await stripe.confirmSetup({
|
|
48
|
+
elements,
|
|
49
|
+
clientSecret,
|
|
50
|
+
confirmParams,
|
|
51
|
+
redirect: 'if_required',
|
|
52
|
+
});
|
|
53
|
+
if (error) return { ok: false, error: error.message };
|
|
54
|
+
return { ok: setupIntent?.status === 'succeeded' };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { error, paymentIntent } = await stripe.confirmPayment({
|
|
58
|
+
elements,
|
|
59
|
+
clientSecret,
|
|
60
|
+
confirmParams,
|
|
61
|
+
redirect: 'if_required',
|
|
62
|
+
});
|
|
63
|
+
if (error) return { ok: false, error: error.message };
|
|
64
|
+
const ok =
|
|
65
|
+
!!paymentIntent &&
|
|
66
|
+
(paymentIntent.status === 'succeeded' || paymentIntent.status === 'processing');
|
|
67
|
+
return { ok };
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
return () => {
|
|
71
|
+
apiRef.current = null;
|
|
72
|
+
};
|
|
73
|
+
}, [stripe, elements, apiRef]);
|
|
74
|
+
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
interface Props {
|
|
79
|
+
/** Mount Elements only when a Stripe method is offered and the key is loaded. */
|
|
80
|
+
active: boolean;
|
|
81
|
+
publishableKey: string | null;
|
|
82
|
+
/** 'payment' for paid orders, 'setup' for free (0-amount) card verification. */
|
|
83
|
+
mode: 'payment' | 'setup';
|
|
84
|
+
/** Charge amount in minor units (used by Elements; the server intent is authoritative). */
|
|
85
|
+
amount: number;
|
|
86
|
+
currency: string;
|
|
87
|
+
locale: string;
|
|
88
|
+
apiRef: MutableRefObject<StripeCheckoutApi | null>;
|
|
89
|
+
children: React.ReactNode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Wraps the order form in a deferred-mode <Elements> provider so the Payment
|
|
94
|
+
* Element can be shown inline (expanded under the selected method) before the
|
|
95
|
+
* order — and its PaymentIntent — exists. When no Stripe method is on offer it
|
|
96
|
+
* renders children untouched and loads nothing.
|
|
97
|
+
*/
|
|
98
|
+
const StripeCheckoutProvider: React.FC<Props> = ({
|
|
99
|
+
active,
|
|
100
|
+
publishableKey,
|
|
101
|
+
mode,
|
|
102
|
+
amount,
|
|
103
|
+
currency,
|
|
104
|
+
locale,
|
|
105
|
+
apiRef,
|
|
106
|
+
children,
|
|
107
|
+
}) => {
|
|
108
|
+
const theme = useTheme();
|
|
109
|
+
const stripePromise = useMemo(
|
|
110
|
+
() => (active && publishableKey ? loadStripe(publishableKey) : null),
|
|
111
|
+
[active, publishableKey]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (!active || !stripePromise) return <>{children}</>;
|
|
115
|
+
|
|
116
|
+
const stripeLocale = STRIPE_LOCALES.includes(locale) ? locale : 'auto';
|
|
117
|
+
|
|
118
|
+
// Drive the Stripe Element off the host MUI theme so it matches light/dark and
|
|
119
|
+
// the brand colors instead of Stripe's default white card.
|
|
120
|
+
const appearance: Appearance = {
|
|
121
|
+
theme: theme.palette.mode === 'dark' ? 'night' : 'stripe',
|
|
122
|
+
variables: {
|
|
123
|
+
colorPrimary: theme.palette.primary.main,
|
|
124
|
+
colorBackground: theme.palette.background.paper,
|
|
125
|
+
colorText: theme.palette.text.primary,
|
|
126
|
+
colorTextSecondary: theme.palette.text.secondary,
|
|
127
|
+
colorDanger: theme.palette.error.main,
|
|
128
|
+
fontFamily: String(theme.typography.fontFamily ?? 'inherit'),
|
|
129
|
+
borderRadius: `${Number(theme.shape.borderRadius) || 8}px`,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Elements
|
|
135
|
+
stripe={stripePromise}
|
|
136
|
+
options={{
|
|
137
|
+
// Free orders verify the card (SetupIntent, no charge); paid orders
|
|
138
|
+
// charge it (PaymentIntent, needs an amount).
|
|
139
|
+
...(mode === 'setup'
|
|
140
|
+
? { mode: 'setup' as const }
|
|
141
|
+
: { mode: 'payment' as const, amount: Math.max(1, Math.round(amount)) }),
|
|
142
|
+
currency: currency.toLowerCase(),
|
|
143
|
+
paymentMethodTypes: ['card'],
|
|
144
|
+
locale: stripeLocale as 'auto',
|
|
145
|
+
appearance,
|
|
146
|
+
}}
|
|
147
|
+
>
|
|
148
|
+
<Bridge apiRef={apiRef} />
|
|
149
|
+
{children}
|
|
150
|
+
</Elements>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export default StripeCheckoutProvider;
|