123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import {useEffect, useRef, useState} from 'react';
- import {css, useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import rubikFontPath from 'sentry/../fonts/rubik-regular.woff';
- import ButtonBar from 'sentry/components/buttonBar';
- import {Alert} from 'sentry/components/core/alert';
- import {Button} from 'sentry/components/core/button';
- import {Input} from 'sentry/components/core/input';
- import FieldGroup from 'sentry/components/forms/fieldGroup';
- import TextField from 'sentry/components/forms/fields/textField';
- import ExternalLink from 'sentry/components/links/externalLink';
- import LoadingIndicator from 'sentry/components/loadingIndicator';
- import {NODE_ENV} from 'sentry/constants';
- import {t, tct} from 'sentry/locale';
- import ConfigStore from 'sentry/stores/configStore';
- import {space} from 'sentry/styles/space';
- import {loadStripe} from 'getsentry/utils/stripe';
- export type SubmitData = {
-
- cardElement: stripe.elements.Element;
-
- onComplete: () => void;
-
- stripe: stripe.Stripe;
-
- validationErrors: string[];
- };
- type Props = {
-
- onSubmit: (data: SubmitData) => void;
-
- buttonText?: string;
-
- cancelButtonText?: string;
-
- className?: string;
-
- error?: string;
-
- errorRetry?: () => void;
-
- footerClassName?: string;
-
- onCancel?: () => void;
-
- referrer?: string;
- };
- function CreditCardForm({
- className,
- error,
- errorRetry,
- onCancel,
- onSubmit,
- buttonText = t('Save Changes'),
- cancelButtonText = t('Cancel'),
- footerClassName = 'form-actions',
- referrer,
- }: Props) {
- const theme = useTheme();
- const [postalCode, setPostalCode] = useState('');
- const [busy, setBusy] = useState(false);
- const [stripe, setStripe] = useState<stripe.Stripe>();
- const [cardElement, setCardElement] = useState<stripe.elements.Element>();
- const stripeMount = useRef<HTMLDivElement>(null);
-
-
- const defaultLoadState = NODE_ENV !== 'test';
- const [loading, setLoading] = useState(defaultLoadState);
- useEffect(() => {
- loadStripe(Stripe => {
- const apiKey = ConfigStore.get('getsentry.stripePublishKey');
- const instance = Stripe(apiKey);
- setStripe(instance);
- });
- }, []);
- useEffect(() => {
- if (!stripe || !stripeMount.current) {
- return;
- }
- const stripeElementStyles = {
- base: {
- backgroundColor: theme.background,
- color: theme.textColor,
- fontFamily: theme.text.family,
- fontWeight: 400,
- fontSize: theme.fontSizeLarge,
- '::placeholder': {
- color: theme.gray300,
- },
- },
- invalid: {
- color: theme.red300,
- iconColor: theme.red300,
- },
- };
- const stripeElements = stripe.elements({
- fonts: [{family: 'Rubik', src: `url(${rubikFontPath})`, weight: '400'}],
- });
- const stripeCardElement = stripeElements.create('card', {
- hidePostalCode: true,
- style: stripeElementStyles,
- });
- stripeCardElement.mount(stripeMount.current);
- stripeCardElement.on('ready', () => {
- setLoading(false);
- });
- setCardElement(stripeCardElement);
- }, [stripe, theme]);
- function onComplete() {
- setBusy(false);
- }
- function handleSubmit(e: React.FormEvent) {
- e.preventDefault();
- if (busy) {
- return;
- }
- setBusy(true);
- const validationErrors: string[] = [];
- if (postalCode === '') {
- validationErrors.push(t('Postal code is required.'));
- }
- if (!stripe || !cardElement) {
- return;
- }
- onSubmit({stripe, cardElement, onComplete, validationErrors});
- }
- function handleCancel(e: React.MouseEvent) {
- e.preventDefault();
- if (busy) {
- return;
- }
- onCancel?.();
- }
- function handlePostalCodeChange(value: string) {
- setPostalCode(value);
- if (cardElement) {
- cardElement.update({value: {postalCode: value}});
- }
- }
- function handlePostalCodeBlur(value: string) {
- setPostalCode(value);
- if (cardElement) {
- cardElement.update({value: {postalCode: value}});
- }
- }
- function handleErrorRetry(event: React.MouseEvent) {
- event.preventDefault();
- errorRetry?.();
- }
- const disabled = busy || loading;
- return (
- <form
- className={className}
- action="."
- method="POST"
- id="payment-form"
- onSubmit={handleSubmit}
- >
- {error && (
- <Alert.Container>
- <Alert type="error">
- <AlertContent>
- {error}
- {errorRetry && (
- <Button size="sm" onClick={handleErrorRetry}>
- {t('Retry')}
- </Button>
- )}
- </AlertContent>
- </Alert>
- </Alert.Container>
- )}
- {loading && <LoadingIndicator />}
- {referrer?.includes('billing-failure') && (
- <Alert.Container>
- <Alert type="warning">
- {t('Your credit card will be charged upon update.')}
- </Alert>
- </Alert.Container>
- )}
- <CreditCardInfoWrapper isLoading={loading}>
- <StyledField
- stacked
- flexibleControlStateSize
- inline={false}
- label={t('Card Details')}
- >
- <FormControl>
- <div ref={stripeMount} />
- </FormControl>
- </StyledField>
- <StyledTextField
- required
- stacked
- flexibleControlStateSize
- inline={false}
- maxLength={12}
- name="postal"
- label={t('Postal Code')}
- value={postalCode}
- onChange={handlePostalCodeChange}
- onBlur={handlePostalCodeBlur}
- />
- <Info>
- <small>
- {tct('Payments are processed securely through [stripe:Stripe].', {
- stripe: <ExternalLink href="https://stripe.com/" />,
- })}
- </small>
- </Info>
- <div className={footerClassName}>
- <StyledButtonBar gap={1}>
- {onCancel && (
- <Button
- data-test-id="cancel"
- priority="default"
- disabled={disabled}
- onClick={handleCancel}
- >
- {cancelButtonText}
- </Button>
- )}
- <Button
- data-test-id="submit"
- type="submit"
- priority="primary"
- disabled={disabled}
- onClick={handleSubmit}
- >
- {buttonText}
- </Button>
- </StyledButtonBar>
- </div>
- </CreditCardInfoWrapper>
- </form>
- );
- }
- const FormControl = Input.withComponent('div');
- const fieldCss = css`
- padding-right: 0;
- padding-left: 0;
- `;
- const StyledField = styled(FieldGroup)`
- ${fieldCss};
- padding-top: 0;
- `;
- const StyledTextField = styled(TextField)`
- ${fieldCss};
- `;
- const Info = styled('p')`
- ${fieldCss};
- margin-bottom: ${space(3)};
- margin-top: ${space(1)};
- `;
- const CreditCardInfoWrapper = styled('div')<{isLoading?: boolean}>`
- ${p => p.isLoading && 'display: none'};
- `;
- const StyledButtonBar = styled(ButtonBar)`
- max-width: fit-content;
- `;
- const AlertContent = styled('span')`
- display: flex;
- align-items: center;
- justify-content: space-between;
- `;
- export default CreditCardForm;
|