addPaymentMethod.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. import {useState} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Button} from 'sentry/components/button';
  5. import {Radio} from 'sentry/components/core/radio';
  6. import Panel from 'sentry/components/panels/panel';
  7. import PanelBody from 'sentry/components/panels/panelBody';
  8. import PanelFooter from 'sentry/components/panels/panelFooter';
  9. import PanelItem from 'sentry/components/panels/panelItem';
  10. import {t, tct} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import TextBlock from 'sentry/views/settings/components/text/textBlock';
  13. import CreditCardSetup from 'getsentry/components/creditCardSetup';
  14. import SubscriptionStore from 'getsentry/stores/subscriptionStore';
  15. import StepHeader from 'getsentry/views/amCheckout/steps/stepHeader';
  16. import type {StepProps} from 'getsentry/views/amCheckout/types';
  17. type Props = StepProps;
  18. function AddPaymentMethod({
  19. subscription,
  20. organization,
  21. isActive,
  22. stepNumber,
  23. isCompleted,
  24. onEdit,
  25. onCompleteStep,
  26. prevStepCompleted,
  27. }: Props) {
  28. const [useExisting, setUseExisting] = useState(true);
  29. const title = t('Payment Method');
  30. const hasPayment = subscription.paymentSource && !!subscription.paymentSource.last4;
  31. const onPaymentAccepted = (data: any) => {
  32. onCompleteStep(stepNumber);
  33. SubscriptionStore.set(data.slug, data);
  34. setUseExisting(true);
  35. };
  36. function renderBody() {
  37. const cardComponent = (
  38. <AddCardSetup
  39. organization={organization}
  40. onSuccess={onPaymentAccepted}
  41. buttonText="Continue"
  42. />
  43. );
  44. if (!hasPayment) {
  45. return <PanelBody data-test-id="body-payment-method">{cardComponent}</PanelBody>;
  46. }
  47. return (
  48. <PanelBody data-test-id="body-payment-method">
  49. <CreditCardOption
  50. isSelected={useExisting}
  51. onClick={() => setUseExisting(true)}
  52. data-test-id="existing-card"
  53. >
  54. <Label>
  55. <StyledRadio readOnly name="existing-card" checked={useExisting} />
  56. <CardDetails>
  57. {tct('Existing card on file ending in [last4]', {
  58. last4: subscription.paymentSource?.last4,
  59. })}
  60. {!!subscription.paymentSource?.zipCode && (
  61. <Description>
  62. {tct('Postal code [zip]', {
  63. zip: subscription.paymentSource?.zipCode,
  64. })}
  65. </Description>
  66. )}
  67. </CardDetails>
  68. </Label>
  69. </CreditCardOption>
  70. <CreditCardOption
  71. isSelected={!useExisting}
  72. onClick={() => setUseExisting(false)}
  73. data-test-id="new-card"
  74. >
  75. <Label>
  76. <StyledRadio readOnly name="new-card" checked={!useExisting} />
  77. <CardDetails>{t('Add new card')}</CardDetails>
  78. </Label>
  79. </CreditCardOption>
  80. {!useExisting && cardComponent}
  81. </PanelBody>
  82. );
  83. }
  84. return (
  85. <Panel>
  86. <StepHeader
  87. canSkip={prevStepCompleted}
  88. title={title}
  89. isActive={isActive}
  90. stepNumber={stepNumber}
  91. isCompleted={isCompleted}
  92. onEdit={onEdit}
  93. />
  94. {isActive && renderBody()}
  95. {isActive && hasPayment && useExisting && (
  96. <StepFooter data-test-id="footer-payment-method">
  97. <Button priority="primary" onClick={() => onCompleteStep(stepNumber)}>
  98. {t('Continue')}
  99. </Button>
  100. </StepFooter>
  101. )}
  102. </Panel>
  103. );
  104. }
  105. const CreditCardOption = styled(PanelItem)<{isSelected?: boolean}>`
  106. padding: 0;
  107. border-bottom: 1px solid ${p => p.theme.innerBorder};
  108. ${p =>
  109. p.isSelected &&
  110. css`
  111. background: ${p.theme.backgroundSecondary};
  112. color: ${p.theme.textColor};
  113. `}
  114. `;
  115. const Label = styled('label')`
  116. display: grid;
  117. grid-template-columns: max-content auto;
  118. gap: ${space(1.5)};
  119. padding: ${space(2)};
  120. color: ${p => p.theme.textColor};
  121. font-weight: normal;
  122. width: 100%;
  123. margin: 0;
  124. `;
  125. const StyledRadio = styled(Radio)`
  126. background: ${p => p.theme.background};
  127. `;
  128. const CardDetails = styled('div')`
  129. display: inline-grid;
  130. gap: ${space(0.75)};
  131. font-size: ${p => p.theme.fontSizeExtraLarge};
  132. color: ${p => p.theme.textColor};
  133. font-weight: 600;
  134. `;
  135. const Description = styled(TextBlock)`
  136. font-size: ${p => p.theme.fontSizeMedium};
  137. color: ${p => p.theme.subText};
  138. margin: 0;
  139. font-weight: normal;
  140. `;
  141. const AddCardSetup = styled(CreditCardSetup)`
  142. padding: ${space(2)} ${space(2)} 0;
  143. color: ${p => p.theme.textColor};
  144. .form-actions {
  145. display: grid;
  146. justify-items: end;
  147. border-top: 1px solid ${p => p.theme.border};
  148. padding: ${space(2)};
  149. margin: -${space(0.5)} -${space(2)} 0;
  150. }
  151. `;
  152. const StepFooter = styled(PanelFooter)`
  153. padding: ${space(2)};
  154. display: grid;
  155. align-items: center;
  156. justify-content: end;
  157. `;
  158. export default AddPaymentMethod;