lbry-desktop/ui/page/settingsStripeCard/view.jsx
zeppi bec50829c1 updated code
about to test something

generate programatically

beginning of the frontend

stripe integration page seems to be working

add user

put functionality behind conditional tag

connect frontend working well

adding environment variables to save success and failure url

bugfix

bugfix

final clean up

adding credit card page

seems to be coming along

calls successfully coming from the frontend

fixing up frontend

cleaning up

frontend coming along

client secret working

basic frontend in place

adding tip page

adding more to the tip frontend

frontend almost done

tabs coming along

one last thing to do for frontend

adding explainer text as custom function

putting finishing touches on tabs

support tabs working well

disable fiat toggle when card not connected

fix frontend gui bug

bugfix and pull out label function

fix symbol for tip gui

modal when card is not yet saved

fix fiat disabled bug

knowing whether card is added programatically

sending tip with frontend

tip functionality working

show unpaid balance

add frontend for card add section

update frontend

update frontend

bugfix

change to use react instead of css

update how stripe is instantiated

fix bug

use customer setup

coming along

working but needs optimization

persist if card is saved

adding anonymous tip functionality

fix nan bug

build stripe endpoints programatically

show for all users for time being

allow the stripe key to automatically switch to live environment

bugfix

bugfix

fix jslint

fix channel page support button

better docs

show customer transactions on frontend

basic table in place

various page updates per jeremys notes

showing card details

nicer tip history table

add better prompt to add card on file viewer page

some linting

time

put connect account behind fiat enabled

no persist fiat mode

wallet calls

tip stuff
2021-07-03 14:42:37 -04:00

428 lines
14 KiB
JavaScript

// @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 { SETTINGS } from 'lbry-redux';
import { Lbryio } from 'lbryinc';
import { STRIPE_PUBLIC_KEY } from 'config';
import classnames from 'classnames';
import moment from 'moment';
let scriptLoading = false;
let scriptLoaded = false;
let scriptDidError = false;
const dateFormat = {
month: 'short',
day: 'numeric',
year: 'numeric',
};
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';
}
type Props = {
disabled: boolean,
label: ?string,
email: ?string,
}
class CardVerify 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: {},
};
}
componentDidMount() {
var that = this;
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
var publicKey = STRIPE_PUBLIC_KEY;
// client secret of the SetupIntent (don't share with anyone but customer)
var 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;
var userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
// show different frontend if user already has card
if (userHasAlreadySetupPayment) {
var card = customerStatusResponse.PaymentMethods[0].card;
var cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
});
// get customer transactions
Lbryio.call('customer', 'list', {
environment: stripeEnvironment,
}, 'post').then(customerTransactionsResponse => {
that.setState({
customerTransactions: customerTransactionsResponse,
})
console.log(customerTransactionsResponse);
});
// 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 => {
console.log(customerSetupResponse);
clientSecret = customerSetupResponse.client_secret;
// instantiate stripe elements
setupStripe();
});
}
// if the status call fails, either an actual error or need to run setup first
}).catch(function(error) {
console.log(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.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 => {
console.log(customerSetupResponse);
clientSecret = customerSetupResponse.client_secret;
// instantiate stripe elements
setupStripe();
});
} else {
console.log('Unseen before error');
}
});
}, 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) {
console.log(result);
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) {
document.querySelector('button').disabled = true;
document.querySelector('#spinner').classList.remove('hidden');
document.querySelector('#button-text').classList.add('hidden');
} else {
document.querySelector('button').disabled = false;
document.querySelector('#spinner').classList.add('hidden');
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 => {
var card = customerStatusResponse.PaymentMethods[0].card;
var cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4,
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
});
});
console.log(result);
changeLoadingState(false);
});
};
}, 0);
}
}
componentDidUpdate() {
if (!scriptLoading) {
this.updateStripeHandler();
}
}
componentWillUnmount() {
if (this.loadPromise) {
this.loadPromise.reject();
}
if (CardVerify.stripeHandler && this.state.open) {
CardVerify.stripeHandler.close();
}
}
onScriptLoaded = () => {
// if (!CardVerify.stripeHandler) {
// CardVerify.stripeHandler = StripeCheckout.configure({
// key: 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo',
// });
//
// if (this.hasPendingClick) {
// this.showStripeDialog();
// }
// }
};
onScriptError = (...args) => {
this.setState({ scriptFailedToLoad: true });
};
onClosed = () => {
this.setState({ open: false });
};
updateStripeHandler() {
// if (!CardVerify.stripeHandler) {
// CardVerify.stripeHandler = StripeCheckout.configure({
// key: this.props.stripeKey,
// });
// }
}
render() {
const { scriptFailedToLoad } = this.props;
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails } = 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>}
{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="spinner hidden" id="spinner" />
<span id="button-text">Add Card</span>
</button>
</div>
</div>
</div>}
{currentFlowStage === 'cardConfirmed' && <div className="successCard">
<Card
title={__('Card Details')}
body={<>
<h4 className="grey-text">Brand: {userCardDetails.brand.toUpperCase()} &nbsp;
Last 4: {userCardDetails.lastFour} &nbsp;
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear} &nbsp;
</h4>
</>}
/>
<br />
{(!customerTransactions || customerTransactions.length === 0) && <Card
title={__('Tip History')}
subtitle={__('You have not sent any tips yet. When you do they will appear here. ')}
/>}
{customerTransactions && customerTransactions.length > 0 && <Card
title={__('Tip History')}
body={<><div className="table__wrapper">
<table className="table table--transactions">
<thead>
<tr>
<th className="date-header">{__('Date')}</th>
<th>{<>{__('Receiving Channel Name')}</>}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Anonymous')}</th>
</tr>
</thead>
<tbody>
{customerTransactions &&
customerTransactions.map((transaction) => (
<tr key={transaction.name + transaction.created_at}>
<td>{moment(transaction.created_at).format('LLL')}</td>
<td>{transaction.channel_name}</td>
<td>${transaction.tipped_amount / 100}</td>
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
</tr>
))}
</tbody>
</table>
</div></>}
/>}
</div>}
</Page>
);
}
}
export default CardVerify;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */