paymentForm.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. import {Fragment, useCallback, useEffect, useState} from 'react';
  2. import {addSuccessMessage} from 'sentry/actionCreators/indicator';
  3. import type {ModalRenderProps} from 'sentry/actionCreators/modal';
  4. import {t, tct} from 'sentry/locale';
  5. import type {Organization} from 'sentry/types/organization';
  6. import {decodeScalar} from 'sentry/utils/queryString';
  7. import useApi from 'sentry/utils/useApi';
  8. import {useLocation} from 'sentry/utils/useLocation';
  9. import type {SubmitData} from 'getsentry/components/creditCardForm';
  10. import CreditCardForm from 'getsentry/components/creditCardForm';
  11. import type {Invoice, PaymentCreateResponse} from 'getsentry/types';
  12. import trackGetsentryAnalytics from 'getsentry/utils/trackGetsentryAnalytics';
  13. import {displayPriceWithCents} from '../amCheckout/utils';
  14. type Props = Pick<ModalRenderProps, 'Header' | 'Body' | 'closeModal'> & {
  15. invoice: Invoice;
  16. organization: Organization;
  17. reloadInvoice: () => void;
  18. };
  19. function InvoiceDetailsPaymentForm({
  20. Header,
  21. Body,
  22. closeModal,
  23. organization,
  24. invoice,
  25. reloadInvoice,
  26. }: Props) {
  27. const api = useApi();
  28. const [validationError, setValidationError] = useState<string | undefined>();
  29. const [intentError, setIntentError] = useState<string | undefined>(undefined);
  30. const [intentData, setIntentData] = useState<PaymentCreateResponse | undefined>(
  31. undefined
  32. );
  33. const location = useLocation();
  34. const loadData = useCallback(async () => {
  35. setIntentError(undefined);
  36. try {
  37. const payload: PaymentCreateResponse = await api.requestPromise(
  38. `/organizations/${invoice.customer.slug}/payments/${invoice.id}/new/`
  39. );
  40. setIntentData(payload);
  41. } catch (e) {
  42. setIntentError(t('Unable to initialize payment, please try again later.'));
  43. }
  44. }, [api, invoice.customer.slug, invoice.id]);
  45. useEffect(() => {
  46. loadData();
  47. }, [loadData]);
  48. function handleSubmit({cardElement, stripe, validationErrors, onComplete}: SubmitData) {
  49. if (validationErrors.length) {
  50. onComplete();
  51. setValidationError(validationErrors[0]);
  52. return;
  53. }
  54. if (!intentData) {
  55. setValidationError(
  56. t('Cannot complete your payment at this time, please try again later.')
  57. );
  58. return;
  59. }
  60. stripe
  61. .confirmCardPayment(intentData.clientSecret, {
  62. payment_method: {card: cardElement},
  63. return_url: intentData.returnUrl,
  64. })
  65. .then((result: stripe.PaymentIntentResponse) => {
  66. if (result.error) {
  67. setIntentError(result.error.message);
  68. return;
  69. }
  70. trackGetsentryAnalytics('billing_failure.paid_now', {
  71. organization,
  72. referrer: decodeScalar(location?.query?.referrer),
  73. });
  74. addSuccessMessage(t('Payment sent successfully.'));
  75. reloadInvoice();
  76. closeModal();
  77. });
  78. }
  79. const error = validationError || intentError;
  80. const errorRetry = intentError ? () => loadData() : undefined;
  81. return (
  82. <Fragment>
  83. <Header>{t('Pay Invoice')}</Header>
  84. <Body>
  85. <p>
  86. {tct('Complete payment for [amount] USD', {
  87. amount: displayPriceWithCents({cents: invoice.amountBilled ?? 0}),
  88. })}
  89. </p>
  90. <CreditCardForm
  91. buttonText={t('Pay Now')}
  92. error={error}
  93. errorRetry={errorRetry}
  94. footerClassName="modal-footer"
  95. onCancel={() => closeModal()}
  96. onSubmit={handleSubmit}
  97. />
  98. </Body>
  99. </Fragment>
  100. );
  101. }
  102. export default InvoiceDetailsPaymentForm;