themer/web/src/CheckoutModal.js
2020-07-04 22:57:37 -06:00

148 lines
4.2 KiB
JavaScript

import React, { useContext, useEffect, useState } from 'react';
import { loadStripe } from "@stripe/stripe-js";
import {
Elements,
CardElement,
useStripe,
useElements
} from "@stripe/react-stripe-js";
import styles from './CheckoutModal.module.css';
import ThemeContext from './ThemeContext';
import Banner from './Banner';
import Button from './Button';
import { currencyOptions } from './PriceInput';
import Modal from './Modal';
const stripePromise = loadStripe(process.env.REACT_APP_STRIPE_PUBLISHABLE_KEY);
const CheckoutModal = ({ price, onClose, onComplete }) => {
const { getActiveColorOrFallback } = useContext(ThemeContext);
const currency = currencyOptions.find(({isoCode}) => isoCode === price.code);
const [succeeded, setSucceeded] = useState(false);
const [error, setError] = useState(null);
const [processing, setProcessing] = useState('');
const [disabled, setDisabled] = useState(true);
const [clientSecret, setClientSecret] = useState('');
const stripe = useStripe();
const elements = useElements();
useEffect(() => {
(async function fetchIntent() {
const response = await window
.fetch('/.netlify/functions/intent', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(price)
});
const data = await response.json();
if (response.status === 200) {
setClientSecret(data.clientSecret);
} else {
setError(data.message);
}
})();
}, [price]);
const cardStyle = {
style: {
base: {
color: getActiveColorOrFallback(['shade7']),
fontFamily: '"Fira Code", monospace',
fontSmoothing: "antialiased",
fontSize: "16px",
"::placeholder": {
color: getActiveColorOrFallback(['shade5'])
}
},
invalid: {
color: getActiveColorOrFallback(['accent0']),
iconColor: getActiveColorOrFallback(['accent0']),
},
complete: {
color: getActiveColorOrFallback(['accent3']),
iconColor: getActiveColorOrFallback(['accent3']),
},
}
};
const cardChange = async evt => {
setDisabled(evt.empty);
setError(evt.error ? evt.error.message : "");
};
const submit = async evt => {
evt.preventDefault();
setProcessing(true);
const payload = await stripe.confirmCardPayment(clientSecret, {
payment_method: {
card: elements.getElement(CardElement),
billing_details: {
name: evt.target.name.value
}
}
});
if (payload.error) {
setError(`Payment failed: ${payload.error.message}`);
setProcessing(false);
window.__ssa__log('payment failed', { message: payload.error.message });
} else {
setError(null);
setProcessing(false);
setSucceeded(true);
onComplete();
window.__ssa__log('payment complete');
}
};
return (
<form className={ styles.form } onSubmit={submit}>
<Modal onClose={ onClose } footer={ (
<>
<Button
type="button"
className={ styles.cancel }
secondary
onClick={ onClose }
>Cancel</Button>
<Button disabled={ disabled || processing || succeeded }>
{processing ? 'Processing...' : 'Pay & download' }
</Button>
</>
) }>
<div
className={ styles.price }
style={{ color: getActiveColorOrFallback(['shade7']) }}
>
Total:{' '}
<span className={ styles.symbol }>{currency.symbol}</span>
{currency.toDisplay(price.amount)}
</div>
<div
className={ styles.card }
style={{ borderColor: getActiveColorOrFallback(['shade7']) }}
>
<CardElement options={ cardStyle } onChange={ cardChange } />
</div>
{error ? (
<Banner
className={ styles.error }
color={ getActiveColorOrFallback(['accent0']) }
>
{error}
</Banner>
) : null}
</Modal>
</form>
);
}
export default props => (
<Elements stripe={ stripePromise }>
<CheckoutModal { ...props } />
</Elements>
);