lbry-desktop/ui/page/settingsStripeCard/view.jsx
Anthony e140613faf
remove unused conditional
get stuff ready for merge

bugfix and cleanup

requested changes

fixing flow errors

fix last flow error and touchups

fiat and lbc tabs coming along

support setting currency as the default tab via query param

add wallet fiat balance

fixing naming

add fiat transactions

using es6 to populate data

should be fine but keeps crashing

transaction listing working

add no transactions thing

about to add a third tab

add third tab

add card last 4 to transaction history

some renaming

show payments successfully

show filler for subscriptions

display if no transactions or subs

working but in the wrong component

approaching something thats working

showing total tipped amount

about to add last couple features

cleanup

More touchups

adding last features

calculate the total amount of unique creators tipped

couple touchups

remove transaction listings from settings

add view transactions buttons

small optimization

add subscriptions section

fix lot of linting errors and make command more userful
2021-08-12 15:30:08 +02:00

440 lines
14 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 { STRIPE_PUBLIC_KEY } from 'config';
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';
let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key
// update the environment for the calls to the backend to indicate which environment to hit
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
stripeEnvironment = 'live';
}
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,
openModal: (string, {}) => void,
setAsConfirmingCard: () => 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: '',
};
}
componentDidMount() {
let that = this;
let doToast = this.props.doToast;
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/';
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
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,
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
});
// 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 () {
card.focus();
});
var email = that.props.email;
function submitForm(event) {
event.preventDefault();
// 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);
stripe
.confirmCardSetup(clientSecret, {
payment_method: {
card: card,
billing_details: { email: email },
},
})
.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,
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
});
});
changeLoadingState(false);
});
};
}, 0);
}
}
render() {
let that = this;
function setAsConfirmingCard() {
that.setState({
currentFlowStage: 'confirmingCard',
});
}
const { scriptFailedToLoad, openModal } = this.props;
const { currentFlowStage, pageTitle, userCardDetails, paymentMethodId } = this.state;
return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
<div>
{scriptFailedToLoad && (
<div className="error__text">There was an error connecting to Stripe. Please try again later.</div>
)}
</div>
{currentFlowStage === 'loading' && (
<div className="headerCard toConfirmCard">
<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="sr-payment-form card cardInput">
<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">
<Card
title={__('Card Details')}
body={
<>
<Plastic
type={userCardDetails.brand}
name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay}
expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear}
number={'____________' + userCardDetails.lastFour}
/>
<br />
<Button
button="secondary"
label={__('Remove Card')}
icon={ICONS.DELETE}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openModal(MODALS.CONFIRM_REMOVE_CARD, {
paymentMethodId: paymentMethodId,
setAsConfirmingCard: setAsConfirmingCard,
});
}}
/>
</>
}
actions={
<Button
button="primary"
label={__('View Transactions')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.WALLET}?tab=payment-history`}
/>
}
/>
<br />
</div>
)}
</Page>
);
}
}
export default SettingsStripeCard;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */