@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.
Files changed (38) hide show
  1. package/dist/cjs/index-EJYDEfV5.js +41925 -0
  2. package/dist/cjs/index-EJYDEfV5.js.map +1 -0
  3. package/dist/cjs/index.js +1 -1
  4. package/dist/cjs/index.umd-CfKeY_Zj.js +13397 -0
  5. package/dist/cjs/index.umd-CfKeY_Zj.js.map +1 -0
  6. package/dist/esm/index-CAjHrQdF.js +41904 -0
  7. package/dist/esm/index-CAjHrQdF.js.map +1 -0
  8. package/dist/esm/index.js +1 -1
  9. package/dist/esm/index.umd-DjIEPZqJ.js +13395 -0
  10. package/dist/esm/index.umd-DjIEPZqJ.js.map +1 -0
  11. package/dist/types/form/Payment.d.ts +1 -0
  12. package/dist/types/form/payment/StripeCheckoutProvider.d.ts +36 -0
  13. package/dist/types/hooks/data/useStripeConfig.d.ts +3 -0
  14. package/dist/types/locales/cs.d.ts +3 -0
  15. package/dist/types/locales/en.d.ts +3 -0
  16. package/dist/types/locales/es.d.ts +3 -0
  17. package/dist/types/locales/pl.d.ts +3 -0
  18. package/dist/types/locales/sk.d.ts +3 -0
  19. package/dist/types/locales/uk.d.ts +3 -0
  20. package/dist/types/modules/order.d.ts +3 -0
  21. package/dist/types/utils/types/order.type.d.ts +2 -0
  22. package/dist/types/utils/types/payment-method.type.d.ts +1 -0
  23. package/package.json +3 -1
  24. package/src/form/Payment.tsx +42 -3
  25. package/src/form/PaymentOverviewBox.tsx +28 -9
  26. package/src/form/TicketForm.tsx +262 -167
  27. package/src/form/payment/StripeCheckoutProvider.tsx +154 -0
  28. package/src/form/tickets/TicketSelectionMobile.tsx +1 -1
  29. package/src/hooks/data/useStripeConfig.ts +14 -0
  30. package/src/locales/cs.tsx +3 -0
  31. package/src/locales/en.tsx +3 -0
  32. package/src/locales/es.tsx +3 -0
  33. package/src/locales/pl.tsx +3 -0
  34. package/src/locales/sk.tsx +3 -0
  35. package/src/locales/uk.tsx +3 -0
  36. package/src/modules/order.ts +3 -0
  37. package/src/utils/types/order.type.ts +5 -0
  38. package/src/utils/types/payment-method.type.ts +1 -0
@@ -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 { t, setGlobal, callbacks, links, user, options, showSnackbar, content, seatingIframeUrl } =
102
- useGlobal();
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
- <FormProvider
581
- methods={methods}
582
- // @ts-ignore -- handleSubmit type mismatch with FormProvider onSubmit prop
583
- onSubmit={methods.handleSubmit(onSubmit, onInvalid)}
584
- formId={EVENTLOOK_ORDER_FORM_ID}
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
- <Stack
587
- className="overview-card__event-info"
588
- display={{ md: 'none' }}
589
- sx={{
590
- mb: 2,
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
- <Typography variant="h3" component="h1">
594
- {event.name}
595
- </Typography>
596
- <Typography variant="h5" component="h2">
597
- {dayjs(event.startDate).format('DD.MM.YYYY HH:mm')}
598
- </Typography>
599
- <Typography variant="body2" mt={1}>
600
- {getPlaceAsString(event.place)}
601
- </Typography>
602
- {headerSlot ? <>{headerSlot}</> : null}
603
- </Stack>
604
- <Grid
605
- container
606
- spacing={2}
607
- sx={{
608
- pb: {
609
- xs: isPaymentOverviewDrawerOpen ? cartItemCount * 4 + 18 : 0,
610
- md: 0,
611
- },
612
- }}
613
- >
614
- <Grid size={{ xs: 12, md: 8 }}>
615
- <Stepper
616
- orientation="vertical"
617
- sx={(theme) => ({
618
- [theme.breakpoints.down('sm')]: {
619
- '& .MuiStepContent-root': {
620
- borderLeftWidth: 0,
621
- paddingLeft: 0,
622
- marginLeft: 0,
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
- '& .MuiStepConnector-line': { borderLeftWidth: 0 },
625
- },
626
- })}
627
- >
628
- {event.type === EventType.RECURRING && (
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.6.title')}</StepLabel>
729
+ <StepLabel>{t('event.tickets.stepper.1.title')}</StepLabel>
631
730
  <StepContent sx={{ pr: { xs: 0 } }}>
632
- <TimeslotSelection event={event} />
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
- <Step active>
637
- <StepLabel>{t('event.tickets.stepper.1.title')}</StepLabel>
638
- <StepContent sx={{ pr: { xs: 0 } }}>
639
- {event.mapId && seatingIframeUrl ? (
640
- <TicketSelectionMap event={event} />
641
- ) : event.hasMerchandise ? (
642
- <TicketWithMerchandiseSelection event={event} />
643
- ) : (
644
- <TicketSelection event={event} />
645
- )}
646
- </StepContent>
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.4.title')}</StepLabel>
753
+ <StepLabel>{t('event.tickets.stepper.8.title')}</StepLabel>
651
754
  <StepContent sx={{ pr: { xs: 0 } }}>
652
- <MerchandiseSelection
653
- eventProducts={eventProducts}
654
- eventId={event.id}
655
- isLoading={isLoading}
656
- />
755
+ <Services event={event} />
657
756
  </StepContent>
658
757
  </Step>
659
- )}
660
- <Step active>
661
- <StepLabel>{t('event.tickets.stepper.8.title')}</StepLabel>
662
- <StepContent sx={{ pr: { xs: 0 } }}>
663
- <Services event={event} />
664
- </StepContent>
665
- </Step>
666
- {event.children.length && (
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.7.title')}</StepLabel>
767
+ <StepLabel>{t('event.tickets.stepper.2.title')}</StepLabel>
669
768
  <StepContent sx={{ pr: { xs: 0 } }}>
670
- <ChildEventSection events={event.children} />
769
+ <ContactPerson event={event} />
671
770
  </StepContent>
672
771
  </Step>
673
- )}
674
- <Step active>
675
- <StepLabel>{t('event.tickets.stepper.2.title')}</StepLabel>
676
- <StepContent sx={{ pr: { xs: 0 } }}>
677
- <ContactPerson event={event} />
678
- </StepContent>
679
- </Step>
680
- {event.hasMerchandise && showShippingMethods() && (
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>{t('event.tickets.stepper.5.title')}</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
- <Shipping event={event} />
787
+ <Payment event={event} stripeReady={stripeReady} />
685
788
  </StepContent>
686
789
  </Step>
687
- )}
688
- <Step active>
689
- <StepLabel>
690
- {t(
691
- `event.tickets.stepper.3.${values.isPaymentVerify ? 'title_verify' : 'title'}`
692
- )}
693
- </StepLabel>
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="insuranceTermsAndConditions"
798
+ name="termsAndConditions"
727
799
  label={
728
800
  <>
729
801
  <Trans
730
- text="event.tickets.insurance.checkbox"
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
- </Stack>
753
- </Grid>
754
- <Grid size={12} sx={{ display: { xs: 'block', md: 'none' } }}>
755
- <Divider sx={{ borderStyle: 'dashed' }} />
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
- <Grid size={{ xs: 12, md: 4 }} sx={{ mt: { xs: 0, md: 0 } }}>
758
- <PaymentOverviewBox event={event} withoutPadding />
759
- </Grid>
760
- </Grid>
761
-
762
- {!isIframe && (
763
- <PaymentOverviewDrawer
764
- event={event}
765
- totalPrice={values.total}
766
- termsAndConditionsRef={termsAndConditionsRef}
767
- onOpenChange={setIsPaymentOverviewDrawerOpen}
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;
@@ -174,7 +174,7 @@ const TicketSelectionMobile: React.FC<Props> = ({
174
174
  key={release.id}
175
175
  sx={{
176
176
  pt: 1,
177
- pr: 0.5,
177
+ pr: 1.5,
178
178
  pb: 0.5,
179
179
  pl: 2,
180
180
  borderRadius: 1,