lbry-desktop/ui/page/settingsStripeCard/view.jsx
mayeaux e6a563443e
Allow capture of name during adding of card (#1657)
* allow someone to save name during card signup

* regexp to not allow numbers or special characters

* add i8n string

* various touchups
2022-06-14 09:21:47 -04:00

580 lines
19 KiB
JavaScript

// restore flow
/* eslint-disable no-undef */
/* eslint-disable react/prop-types */
import React from 'react';
import Page from 'component/page';
import Card from 'component/common/card';
import { Lbryio } from 'lbryinc';
import Plastic from 'react-plastic';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
import * as PAGES from 'constants/pages';
import { FormField } from 'component/common/form';
import { STRIPE_PUBLIC_KEY } from 'config';
import { getStripeEnvironment } from 'util/stripe';
let stripeEnvironment = getStripeEnvironment();
const STRIPE_PLUGIN_SRC = 'https://js.stripe.com/v3/';
const APIS_DOWN_ERROR_RESPONSE = __('There was an error from the server, please try again later');
const CARD_SETUP_ERROR_RESPONSE = __('There was an error getting your card setup, please try again later');
// eslint-disable-next-line flowtype/no-types-missing-file-annotation
type Props = {
disabled: boolean,
label: ?string,
email: ?string,
scriptFailedToLoad: boolean,
doOpenModal: (string, {}) => void,
doToast: ({}) => void,
openModal: (string, {}) => void,
setAsConfirmingCard: () => void,
locale: ?any,
preferredCurrency: string,
setPreferredCurrency: (string) => void,
};
// type State = {
// open: boolean,
// currentFlowStage: string,
// customerTransactions: Array<any>,
// pageTitle: string,
// userCardDetails: any, // fill this out
// scriptFailedToLoad: boolean,
// };
class SettingsStripeCard extends React.Component<Props, State> {
constructor(props) {
// :Props
super(props);
this.state = {
open: false,
scriptFailedToLoad: false,
currentFlowStage: 'loading', // loading, confirmingCard, cardConfirmed
customerTransactions: [],
pageTitle: 'Add Card',
userCardDetails: {},
paymentMethodId: '',
preferredCurrency: 'USD',
};
}
componentDidMount() {
let that = this;
that.setState({
cardNameValue: '',
});
const { preferredCurrency, locale } = this.props;
// use preferredCurrency if it's set on client, otherwise use USD, unless in Europe then use EUR
if (preferredCurrency) {
that.setState({
preferredCurrency: preferredCurrency,
});
} else if (locale) {
if (locale.continent === 'EU') {
that.setState({
preferredCurrency: 'EUR',
});
}
}
let doToast = this.props.doToast;
// only add script if it doesn't already exist
const stripeScriptExists = document.querySelectorAll(`script[src="${STRIPE_PLUGIN_SRC}"]`).length > 0;
if (!stripeScriptExists) {
const script = document.createElement('script');
script.src = STRIPE_PLUGIN_SRC;
script.async = true;
// $FlowFixMe
document.body.appendChild(script);
}
// public key of the stripe account
let publicKey = STRIPE_PUBLIC_KEY;
// client secret of the SetupIntent (don't share with anyone but customer)
let clientSecret = '';
// setting a timeout to let the client secret populate
// TODO: fix this, should be a cleaner way
setTimeout(function () {
// check if customer has card setup already
if (stripeEnvironment) {
Lbryio.call(
'customer',
'status',
{
environment: stripeEnvironment,
},
'post'
)
.then((customerStatusResponse) => {
// user has a card saved if their defaultPaymentMethod has an id
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
let userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
// show different frontend if user already has card
if (userHasAlreadySetupPayment) {
let card = customerStatusResponse.PaymentMethods[0].card;
let customer = customerStatusResponse.Customer;
let topOfDisplay = customer.email.split('@')[0];
let bottomOfDisplay = '@' + customer.email.split('@')[1];
let cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4,
topOfDisplay: topOfDisplay,
bottomOfDisplay: bottomOfDisplay,
};
const formattedEmailName = cardDetails.topOfDisplay + ' ' + cardDetails.bottomOfDisplay;
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Payment Methods',
userCardDetails: cardDetails,
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
cardName: customerStatusResponse.PaymentMethods[0].billing_details.name || formattedEmailName,
});
// otherwise, prompt them to save a card
} else {
that.setState({
currentFlowStage: 'confirmingCard',
});
// get a payment method secret for frontend
Lbryio.call(
'customer',
'setup',
{
environment: stripeEnvironment,
},
'post'
).then((customerSetupResponse) => {
clientSecret = customerSetupResponse.client_secret;
// instantiate stripe elements
setupStripe();
});
}
// get customer transactions
Lbryio.call(
'customer',
'list',
{
environment: stripeEnvironment,
},
'post'
).then((customerTransactionsResponse) => {
that.setState({
customerTransactions: customerTransactionsResponse,
});
});
// if the status call fails, either an actual error or need to run setup first
})
.catch(function (error) {
// errorString passed from the API (with a 403 error)
const errorString = 'user as customer is not setup yet';
// if it's beamer's error indicating the account is not linked yet
if (error.message && error.message.indexOf(errorString) > -1) {
// send them to save a card
that.setState({
currentFlowStage: 'confirmingCard',
});
// get a payment method secret for frontend
Lbryio.call(
'customer',
'setup',
{
environment: stripeEnvironment,
},
'post'
).then((customerSetupResponse) => {
clientSecret = customerSetupResponse.client_secret;
// instantiate stripe elements
setupStripe();
});
// 500 error from the backend being down
} else if (error === 'internal_apis_down') {
doToast({ message: APIS_DOWN_ERROR_RESPONSE, isError: true });
} else {
// probably an error from stripe
doToast({ message: CARD_SETUP_ERROR_RESPONSE, isError: true });
}
});
}
}, 250);
function setupStripe() {
// TODO: have to fix this, using so that the script is available
setTimeout(function () {
var stripeElements = function (publicKey, setupIntent) {
var stripe = Stripe(publicKey);
var elements = stripe.elements();
// Element styles
var style = {
base: {
fontSize: '16px',
color: '#32325d',
fontFamily: '-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
fontSmoothing: 'antialiased',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
};
var card = elements.create('card', { style: style });
card.mount('#card-element');
// Element focus ring
card.on('focus', function () {
var el = document.getElementById('card-element');
el.classList.add('focused');
});
card.on('blur', function () {
var el = document.getElementById('card-element');
el.classList.remove('focused');
});
card.on('ready', function () {
// focus on the name input
document.querySelector('#card-name').focus();
});
var email = that.props.email;
function submitForm(event) {
event.preventDefault();
const cardUserName = document.querySelector('#card-name').value;
if (!cardUserName) {
return (document.querySelector('.sr-field-error').innerHTML = __('Please enter the name on the card'));
}
// if client secret wasn't loaded properly
if (!clientSecret) {
var displayErrorText = 'There was an error in generating your payment method. Please contact a developer';
var displayError = document.getElementById('card-errors');
displayError.textContent = displayErrorText;
return;
}
changeLoadingState(true);
const name = document.querySelector('#card-name').value;
stripe
.confirmCardSetup(clientSecret, {
payment_method: {
card: card,
billing_details: {
email,
name,
},
},
})
.then(function (result) {
if (result.error) {
changeLoadingState(false);
var displayError = document.getElementById('card-errors');
displayError.textContent = result.error.message;
} else {
// The PaymentMethod was successfully set up
// hide and show the proper divs
orderComplete(stripe, clientSecret);
}
});
}
// Handle payment submission when user clicks the pay button.
var button = document.getElementById('submit');
button.addEventListener('click', function (event) {
submitForm(event);
});
// currently doesn't work because the iframe javascript context is different
// would be nice though if it's even technically possible
// window.addEventListener('keyup', function(event) {
// if (event.keyCode === 13) {
// submitForm(event);
// }
// }, false);
};
// TODO: possible bug here where clientSecret isn't done
stripeElements(publicKey, clientSecret);
// Show a spinner on payment submission
var changeLoadingState = function (isLoading) {
if (isLoading) {
// $FlowFixMe
document.querySelector('button').disabled = true;
// $FlowFixMe
document.querySelector('#stripe-spinner').classList.remove('hidden');
// $FlowFixMe
document.querySelector('#button-text').classList.add('hidden');
} else {
// $FlowFixMe
document.querySelector('button').disabled = false;
// $FlowFixMe
document.querySelector('#stripe-spinner').classList.add('hidden');
// $FlowFixMe
document.querySelector('#button-text').classList.remove('hidden');
}
};
// shows a success / error message when the payment is complete
var orderComplete = function (stripe, clientSecret) {
stripe.retrieveSetupIntent(clientSecret).then(function (result) {
Lbryio.call(
'customer',
'status',
{
environment: stripeEnvironment,
},
'post'
).then((customerStatusResponse) => {
let card = customerStatusResponse.PaymentMethods[0].card;
let customer = customerStatusResponse.Customer;
let topOfDisplay = customer.email.split('@')[0];
let bottomOfDisplay = '@' + customer.email.split('@')[1];
let cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4,
topOfDisplay,
bottomOfDisplay,
};
const formattedEmailName = cardDetails.topOfDisplay + ' ' + cardDetails.bottomOfDisplay;
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Payment Methods',
userCardDetails: cardDetails,
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
cardName: customerStatusResponse.PaymentMethods[0].billing_details.name || formattedEmailName,
});
});
changeLoadingState(false);
});
};
}, 0);
}
}
render() {
let that = this;
const returnToValue = new URLSearchParams(location.search).get('returnTo');
let shouldShowBackToMembershipButton = returnToValue === 'premium';
function setAsConfirmingCard() {
that.setState({
currentFlowStage: 'confirmingCard',
});
}
const { setPreferredCurrency } = this.props;
function clearErrorMessage() {
const errorElement = document.querySelector('.sr-field-error');
errorElement.innerHTML = '';
}
// when user changes currency in selector
function onCurrencyChange(event) {
const { value } = event.target;
// update preferred currency in frontend
that.setState({
preferredCurrency: value,
});
// update client settings
setPreferredCurrency(value);
}
function onChangeCardName(event) {
const { value } = event.target;
const numberOrSpecialCharacter = /[0-9!@#$%^&*()_+=[\]{};:"\\|,<>?~]/;
const errorElement = document.querySelector('.sr-field-error');
if (numberOrSpecialCharacter.test(value)) {
errorElement.innerHTML = __('Special characters and numbers are not allowed');
} else if (value.length > 48) {
errorElement.innerHTML = __('Name must be less than 48 characters long');
} else {
errorElement.innerHTML = '';
that.setState({
cardNameValue: value,
});
}
}
const { scriptFailedToLoad, openModal } = this.props;
const {
currentFlowStage,
pageTitle,
userCardDetails,
paymentMethodId,
preferredCurrency,
cardName,
cardNameValue,
} = this.state;
return (
<Page noFooter noSideNavigation className="card-stack" backout={{ title: __(pageTitle), backLabel: __('Back') }}>
{/* if Stripe javascript didn't load */}
<div>
{scriptFailedToLoad && (
<div className="error__text">{__('There was an error connecting to Stripe. Please try again later.')}</div>
)}
</div>
{/* initial markup to show while getting information */}
{currentFlowStage === 'loading' && (
<div className="getting-card-status">
<Card title={__('Connect your card with Odysee')} subtitle={__('Getting your card connection status...')} />
</div>
)}
{/* customer has not added a card yet */}
{currentFlowStage === 'confirmingCard' && (
<div className="sr-root">
<div className="sr-main">
<div className="">
<div className="sr-form-row">
<label className="payment-details">{__('Name on card')}</label>
<input
type="text"
id="card-name"
onChange={onChangeCardName}
value={cardNameValue}
onBlur={clearErrorMessage}
/>
</div>
<div className="sr-form-row">
<label className="payment-details">{__('Card details')}</label>
<div className="sr-input sr-element sr-card-element" id="card-element" />
</div>
<div className="sr-field-error" id="card-errors" role="alert" />
<button className="linkButton" id="submit">
<div className="stripe__spinner hidden" id="stripe-spinner" />
<span id="button-text">{__('Add Card')}</span>
</button>
</div>
</div>
</div>
)}
{/* if the user has already confirmed their card */}
{currentFlowStage === 'cardConfirmed' && (
<div className="successCard">
{/* back to membership button */}
{shouldShowBackToMembershipButton && (
<Button
button="primary"
label={__('Back To Odysee Premium')}
icon={ICONS.UPGRADE}
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
style={{ marginBottom: '20px' }}
/>
)}
<Card
title={__('Card Details')}
className="add-payment-card-div"
body={
<>
<Plastic
type={userCardDetails.brand}
name={cardName}
expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear}
number={'____________' + userCardDetails.lastFour}
/>
<br />
<Button
button="primary"
label={__('Remove Card')}
icon={ICONS.DELETE}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openModal(MODALS.CONFIRM_REMOVE_CARD, {
paymentMethodId: paymentMethodId,
setAsConfirmingCard: setAsConfirmingCard,
});
}}
/>
<Button
button="secondary"
label={__('View Transactions')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.WALLET}?fiatType=outgoing&tab=fiat-payment-history&currency=fiat`}
style={{ marginLeft: '10px' }}
/>
</>
}
/>
<br />
<div className="currency-to-use-div">
<h1 className="currency-to-use-header">{__('Currency To Use')}:</h1>
<fieldset-section>
<FormField
className="currency-to-use-selector"
name="currency_selector"
type="select"
onChange={onCurrencyChange}
value={preferredCurrency}
>
{['USD', 'EUR'].map((currency) => (
<option key={currency} value={currency}>
{currency}
</option>
))}
</FormField>
</fieldset-section>
</div>
</div>
)}
</Page>
);
}
}
export default SettingsStripeCard;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */