// 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 moment from 'moment'; import Plastic from 'react-plastic'; import Button from 'component/button'; import * as ICONS from 'constants/icons'; import * as MODALS from 'constants/modal_types'; 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'; } // 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; console.log(this.props); 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]; console.log(customerStatusResponse.Customer); 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) => { console.log(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, }); console.log(customerTransactionsResponse); }); // 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 && 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 if (error === 'internal_apis_down') { var displayString = 'There was an error from the server, please let support know'; doToast({ message: displayString, isError: true }); } 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) { // $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, }); }); console.log(result); changeLoadingState(false); }); }; }, 0); } } render() { let that = this; function setAsConfirmingCard() { that.setState({ currentFlowStage: 'confirmingCard', }); } const { scriptFailedToLoad, openModal } = this.props; const { currentFlowStage, customerTransactions, 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, }); }} /> </> } /> <br /> {/* if a user has no transactions yet */} {(!customerTransactions || customerTransactions.length === 0) && ( <Card title={__('Tip History')} subtitle={__('You have not sent any tips yet. When you do they will appear here. ')} /> )} </div> )} {/* customer already has transactions */} {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>{__('Tip Location')}</th> <th>{__('Amount (USD)')} </th> <th>{__('Anonymous')}</th> </tr> </thead> <tbody> {customerTransactions && customerTransactions.reverse().map((transaction) => ( <tr key={transaction.name + transaction.created_at}> <td>{moment(transaction.created_at).format('LLL')}</td> <td> <Button className="stripe__card-link-text" navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id} label={transaction.channel_name} button="link" /> </td> <td> <Button className="stripe__card-link-text" navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id} label={ transaction.channel_claim_id === transaction.source_claim_id ? 'Channel Page' : 'File Page' } button="link" /> </td> <td>${transaction.tipped_amount / 100}</td> <td>{transaction.private_tip ? 'Yes' : 'No'}</td> </tr> ))} </tbody> </table> </div> </> } /> )} </Page> ); } } export default SettingsStripeCard; /* eslint-enable no-undef */ /* eslint-enable react/prop-types */