diff --git a/package.json b/package.json index e6765f310..eb99f3de0 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "build:dir": "yarn build -- --dir -c.compression=store -c.mac.identity=null", "crossenv": "./node_modules/cross-env/dist/bin/cross-env", "lint": "eslint 'ui/**/*.{js,jsx}' && eslint 'web/**/*.{js,jsx}' && eslint 'electron/**/*.js' && flow", - "lint-fix": "eslint --fix 'ui/**/*.{js,jsx}' && eslint --fix 'web/**/*.{js,jsx}' && eslint --fix 'electron/**/*.js' && flow", + "lint-fix": "eslint --fix --quiet 'ui/**/*.{js,jsx}' && eslint --fix --quiet 'web/**/*.{js,jsx}' && eslint --fix --quiet 'electron/**/*.js'", "format": "prettier 'src/**/*.{js,jsx,scss,json}' --write", "flow-defs": "flow-typed install", "precommit": "lint-staged", diff --git a/ui/component/common/tabs.jsx b/ui/component/common/tabs.jsx index 6e08f6ad9..edd225f4a 100644 --- a/ui/component/common/tabs.jsx +++ b/ui/component/common/tabs.jsx @@ -28,7 +28,7 @@ import { useRect } from '@reach/rect'; // </TabPanels> // </Tabs> // -// the base @reach/tabs components handle all the focus/accessibilty labels +// the base @reach/tabs components handle all the focus/accessibility labels // We're just adding some styling type TabsProps = { diff --git a/ui/component/walletFiatAccountHistory/index.js b/ui/component/walletFiatAccountHistory/index.js new file mode 100644 index 000000000..8fed953a8 --- /dev/null +++ b/ui/component/walletFiatAccountHistory/index.js @@ -0,0 +1,3 @@ +import FiatAccountHistory from './view'; + +export default FiatAccountHistory; diff --git a/ui/component/walletFiatAccountHistory/view.jsx b/ui/component/walletFiatAccountHistory/view.jsx new file mode 100644 index 000000000..49b1ff299 --- /dev/null +++ b/ui/component/walletFiatAccountHistory/view.jsx @@ -0,0 +1,88 @@ +// @flow +import React from 'react'; +import Button from 'component/button'; +import Card from 'component/common/card'; +import moment from 'moment'; + +type Props = { + accountDetails: any, + transactions: any, +}; + +const WalletBalance = (props: Props) => { + // receive transactions from parent component + const { transactions } = props; + + let accountTransactions; + + // reverse so most recent payments come first + if (transactions && transactions.length) { + accountTransactions = transactions.reverse(); + } + + // if there are more than 10 transactions, limit it to 10 for the frontend + if (accountTransactions && accountTransactions.length > 10) { + accountTransactions.length = 10; + } + + return ( + <><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>{__('Processing Fee')}</th> + <th>{__('Odysee Fee')}</th> + <th>{__('Received Amount')}</th> + </tr> + </thead> + <tbody> + {accountTransactions && + accountTransactions.map((transaction) => ( + <tr key={transaction.name + transaction.created_at}> + <td>{moment(transaction.created_at).format('LLL')}</td> + <td> + <Button + className="" + navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id} + label={transaction.channel_name} + button="link" + /> + </td> + <td> + <Button + className="" + navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id} + label={ + transaction.channel_claim_id === transaction.source_claim_id + ? 'Channel Page' + : 'Content Page' + } + button="link" + /> + </td> + <td>${transaction.tipped_amount / 100}</td> + <td>${transaction.transaction_fee / 100}</td> + <td>${transaction.application_fee / 100}</td> + <td>${transaction.received_amount / 100}</td> + </tr> + ))} + </tbody> + </table> + {!accountTransactions && <p style={{textAlign: 'center', marginTop: '20px', fontSize: '13px', color: 'rgb(171, 171, 171)'}}>No Transactions</p>} + </div> + </> + )} + /> + </> + ); +}; + +export default WalletBalance; diff --git a/ui/component/walletFiatBalance/index.js b/ui/component/walletFiatBalance/index.js new file mode 100644 index 000000000..b35b2d4af --- /dev/null +++ b/ui/component/walletFiatBalance/index.js @@ -0,0 +1,3 @@ +import WalletFiatBalance from './view'; + +export default WalletFiatBalance; diff --git a/ui/component/walletFiatBalance/view.jsx b/ui/component/walletFiatBalance/view.jsx new file mode 100644 index 000000000..9c71623ba --- /dev/null +++ b/ui/component/walletFiatBalance/view.jsx @@ -0,0 +1,94 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; +import React from 'react'; +import Button from 'component/button'; +import Card from 'component/common/card'; +import Icon from 'component/common/icon'; +import I18nMessage from 'component/i18nMessage'; + +type Props = { + accountDetails: any, +}; + +const WalletBalance = (props: Props) => { + const { + accountDetails, + } = props; + + return ( + <>{<Card + title={<><Icon size={18} icon={ICONS.FINANCE} />{(accountDetails && ((accountDetails.total_received_unpaid - accountDetails.total_paid_out) / 100)) || 0} USD</>} + subtitle={accountDetails && accountDetails.total_received_unpaid > 0 && + <I18nMessage> + This is your pending balance that will be automatically sent to your bank account + </I18nMessage> + } + actions={ + <> + <h2 className="section__title--small"> + ${(accountDetails && (accountDetails.total_received_unpaid / 100)) || 0} Total Received Tips + </h2> + + <h2 className="section__title--small"> + ${(accountDetails && (accountDetails.total_paid_out / 100)) || 0} Withdrawn + {/* <Button */} + {/* button="link" */} + {/* label={detailsExpanded ? __('View less') : __('View more')} */} + {/* iconRight={detailsExpanded ? ICONS.UP : ICONS.DOWN} */} + {/* onClick={() => setDetailsExpanded(!detailsExpanded)} */} + {/* /> */} + </h2> + + {/* view more section */} + {/* commenting out because not implemented, but could be used in the future */} + {/* {detailsExpanded && ( */} + {/* <div className="section__subtitle"> */} + {/* <dl> */} + {/* <dt> */} + {/* <span className="dt__text">{__('Earned from uploads')}</span> */} + {/* /!* <span className="help--dt">({__('Earned from channel page')})</span> *!/ */} + {/* </dt> */} + {/* <dd> */} + {/* <span className="dd__text"> */} + {/* {Boolean(1) && ( */} + {/* <Button */} + {/* button="link" */} + {/* className="dd__button" */} + {/* icon={ICONS.UNLOCK} */} + {/* /> */} + {/* )} */} + {/* <CreditAmount amount={1} precision={4} /> */} + {/* </span> */} + {/* </dd> */} + + {/* <dt> */} + {/* <span className="dt__text">{__('Earned from channel page')}</span> */} + {/* /!* <span className="help--dt">({__('Delete or edit past content to spend')})</span> *!/ */} + {/* </dt> */} + {/* <dd> */} + {/* <CreditAmount amount={1} precision={4} /> */} + {/* </dd> */} + + {/* /!* <dt> *!/ */} + {/* /!* <span className="dt__text">{__('...supporting content')}</span> *!/ */} + {/* /!* <span className="help--dt">({__('Delete supports to spend')})</span> *!/ */} + {/* /!* </dt> *!/ */} + {/* /!* <dd> *!/ */} + {/* /!* <CreditAmount amount={1} precision={4} /> *!/ */} + {/* /!* </dd> *!/ */} + {/* </dl> */} + {/* </div> */} + {/* )} */} + + <div className="section__actions"> + {/* <Button button="primary" label={__('Receive Payout')} icon={ICONS.SEND} /> */} + <Button button="secondary" label={__('Account Configuration')} icon={ICONS.SETTINGS} navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} /> + </div> + </> + } + />}</> + ); +}; + +export default WalletBalance; diff --git a/ui/component/walletFiatPaymentBalance/index.js b/ui/component/walletFiatPaymentBalance/index.js new file mode 100644 index 000000000..d40e374be --- /dev/null +++ b/ui/component/walletFiatPaymentBalance/index.js @@ -0,0 +1,3 @@ +import WalletFiatPaymentBalance from './view'; + +export default WalletFiatPaymentBalance; diff --git a/ui/component/walletFiatPaymentBalance/view.jsx b/ui/component/walletFiatPaymentBalance/view.jsx new file mode 100644 index 000000000..3b8032d3a --- /dev/null +++ b/ui/component/walletFiatPaymentBalance/view.jsx @@ -0,0 +1,76 @@ +// @flow +import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; +import React from 'react'; +import Button from 'component/button'; +import Card from 'component/common/card'; + +type Props = { + totalTippedAmount: number, + accountDetails: any, + transactions: any, +}; + +const WalletBalance = (props: Props) => { + const { + // accountDetails, + transactions, + } = props; + + // let cardDetails = { + // brand: card.brand, + // expiryYear: card.exp_year, + // expiryMonth: card.exp_month, + // lastFour: card.last4, + // topOfDisplay: topOfDisplay, + // bottomOfDisplay: bottomOfDisplay, + // }; + + // const [detailsExpanded, setDetailsExpanded] = React.useState(false); + const [totalCreatorsSupported, setTotalCreatorsSupported] = React.useState(false); + + // calculate how many unique users tipped + React.useEffect(() => { + if (transactions) { + let channelNames = []; + + for (const transaction of transactions) { + channelNames.push(transaction.channel_name); + } + + let unique = [...new Set(channelNames)]; + setTotalCreatorsSupported(unique.length); + } + }, [transactions]); + + return ( + <>{<Card + // TODO: implement hasActiveCard and show the current card the user would charge to + // subtitle={hasActiveCard && <h2>Hello</h2> + // // <Plastic + // // type={userCardDetails.brand} + // // name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay} + // // expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear} + // // number={'____________' + userCardDetails.lastFour} + // // /> + // } + actions={ + <> + <h2 className="section__title--small"> + {(transactions && transactions.length) || 0} Total Tips + </h2> + + <h2 className="section__title--small"> + {totalCreatorsSupported || 0} Creators Supported + </h2> + + <div className="section__actions"> + <Button button="secondary" label={__('Manage Cards')} icon={ICONS.SETTINGS} navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} /> + </div> + </> + } + />}</> + ); +}; + +export default WalletBalance; diff --git a/ui/component/walletFiatPaymentHistory/index.js b/ui/component/walletFiatPaymentHistory/index.js new file mode 100644 index 000000000..0d41b2e19 --- /dev/null +++ b/ui/component/walletFiatPaymentHistory/index.js @@ -0,0 +1,3 @@ +import WalletFiatPaymentHistory from './view'; + +export default WalletFiatPaymentHistory; diff --git a/ui/component/walletFiatPaymentHistory/view.jsx b/ui/component/walletFiatPaymentHistory/view.jsx new file mode 100644 index 000000000..42d0341f3 --- /dev/null +++ b/ui/component/walletFiatPaymentHistory/view.jsx @@ -0,0 +1,112 @@ +// @flow +import React from 'react'; +import Button from 'component/button'; +import Card from 'component/common/card'; +import { Lbryio } from 'lbryinc'; +import moment from 'moment'; +import { STRIPE_PUBLIC_KEY } from 'config'; + +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 = { + accountDetails: any, + transactions: any, +}; + +const WalletBalance = (props: Props) => { + // receive transactions from parent component + const { transactions: accountTransactions } = props; + + // const [accountStatusResponse, setAccountStatusResponse] = React.useState(); + + // const [subscriptions, setSubscriptions] = React.useState(); + + const [lastFour, setLastFour] = React.useState(); + + function getCustomerStatus() { + return Lbryio.call( + 'customer', + 'status', + { + environment: stripeEnvironment, + }, + 'post' + ); + } + + // TODO: this is actually incorrect, last4 should be populated based on the transaction not the current customer details + React.useEffect(() => { + (async function() { + const customerStatusResponse = await getCustomerStatus(); + + const lastFour = customerStatusResponse.PaymentMethods && customerStatusResponse.PaymentMethods.length && customerStatusResponse.PaymentMethods[0].card.last4; + + setLastFour(lastFour); + })(); + }, []); + + return ( + <> + <Card + title={__('Payment 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>{__('Card Last 4')}</th> + <th>{__('Anonymous')}</th> + </tr> + </thead> + <tbody> + {accountTransactions && + accountTransactions.map((transaction) => ( + <tr key={transaction.name + transaction.created_at}> + <td>{moment(transaction.created_at).format('LLL')}</td> + <td> + <Button + className="" + navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id} + label={transaction.channel_name} + button="link" + /> + </td> + <td> + <Button + className="" + navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id} + label={ + transaction.channel_claim_id === transaction.source_claim_id + ? 'Channel Page' + : 'Content Page' + } + button="link" + /> + </td> + <td>${transaction.tipped_amount / 100}</td> + <td>{lastFour}</td> + <td>{transaction.private_tip ? 'Yes' : 'No'}</td> + </tr> + ))} + </tbody> + </table> + {(!accountTransactions || accountTransactions.length === 0) && <p style={{textAlign: 'center', marginTop: '20px', fontSize: '13px', color: 'rgb(171, 171, 171)'}}>No Transactions</p>} + </div> + </> + } + /> + </> + ); +}; + +export default WalletBalance; diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index 034a2ee28..3f201373a 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -208,44 +208,6 @@ class SettingsPage extends React.PureComponent<Props, State> { }} className="card-stack" > - {/* @if TARGET='web' */} - {user && user.fiat_enabled && ( - <Card - title={__('Bank Accounts')} - subtitle={__('Connect a bank account to receive tips and compensation in your local currency')} - actions={ - <div className="section__actions"> - <Button - button="secondary" - label={__('Manage')} - icon={ICONS.SETTINGS} - navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} - /> - </div> - } - /> - )} - {/* @endif */} - - {/* @if TARGET='web' */} - {isAuthenticated && ( - <Card - title={__('Payment Methods')} - subtitle={__('Add a credit card to tip creators in their local currency')} - actions={ - <div className="section__actions"> - <Button - button="secondary" - label={__('Manage')} - icon={ICONS.SETTINGS} - navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} - /> - </div> - } - /> - )} - {/* @endif */} - <Card title={__('Language')} actions={<SettingLanguage />} /> {homepages && Object.keys(homepages).length > 1 && ( <Card title={__('Homepage')} actions={<HomepageSelector />} /> @@ -488,6 +450,44 @@ class SettingsPage extends React.PureComponent<Props, State> { /> {/* @endif */} + {/* @if TARGET='web' */} + {user && ( + <Card + title={__('Bank Accounts')} + subtitle={__('Connect a bank account to receive tips and compensation in your local currency')} + actions={ + <div className="section__actions"> + <Button + button="secondary" + label={__('Manage')} + icon={ICONS.SETTINGS} + navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} + /> + </div> + } + /> + )} + {/* @endif */} + + {/* @if TARGET='web' */} + {isAuthenticated && ( + <Card + title={__('Payment Methods')} + subtitle={__('Add a credit card to tip creators in their local currency')} + actions={ + <div className="section__actions"> + <Button + button="secondary" + label={__('Manage')} + icon={ICONS.SETTINGS} + navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} + /> + </div> + } + /> + )} + {/* @endif */} + {(isAuthenticated || !IS_WEB) && ( <> <Card diff --git a/ui/page/settingsStripeAccount/index.js b/ui/page/settingsStripeAccount/index.js index fd03b4f49..7df77d4e5 100644 --- a/ui/page/settingsStripeAccount/index.js +++ b/ui/page/settingsStripeAccount/index.js @@ -1,12 +1,9 @@ import { connect } from 'react-redux'; import { withRouter } from 'react-router'; import StripeAccountConnection from './view'; -import { selectUser } from 'redux/selectors/user'; import { doToast } from 'redux/actions/notifications'; -const select = (state) => ({ - user: selectUser(state), -}); +const select = (state) => ({}); const perform = (dispatch) => ({ doToast: (options) => dispatch(doToast(options)), diff --git a/ui/page/settingsStripeAccount/view.jsx b/ui/page/settingsStripeAccount/view.jsx index 2821b4423..63371ca86 100644 --- a/ui/page/settingsStripeAccount/view.jsx +++ b/ui/page/settingsStripeAccount/view.jsx @@ -1,12 +1,13 @@ // @flow import * as ICONS from 'constants/icons'; +import * as PAGES from 'constants/pages'; import React from 'react'; import Button from 'component/button'; import Card from 'component/common/card'; import Page from 'component/page'; + import { Lbryio } from 'lbryinc'; import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config'; -import moment from 'moment'; const isDev = process.env.NODE_ENV !== 'production'; @@ -30,7 +31,6 @@ if (isDev) { type Props = { source: string, - user: User, doOpenModal: (string, {}) => void, doToast: ({ message: string }) => void, }; @@ -40,13 +40,13 @@ type State = { loading: boolean, content: ?string, stripeConnectionUrl: string, - // alreadyUpdated: boolean, accountConfirmed: boolean, accountPendingConfirmation: boolean, accountNotConfirmedButReceivedTips: boolean, unpaidBalance: number, pageTitle: string, - accountTransactions: any, // define this type + stillRequiringVerification: boolean, + accountTransactions: any }; class StripeAccountConnection extends React.Component<Props, State> { @@ -62,19 +62,14 @@ class StripeAccountConnection extends React.Component<Props, State> { unpaidBalance: 0, stripeConnectionUrl: '', pageTitle: 'Add Payout Method', + stillRequiringVerification: false, accountTransactions: [], - // alreadyUpdated: false, }; } componentDidMount() { - const { user } = this.props; - let doToast = this.props.doToast; - // $FlowFixMe - this.experimentalUiEnabled = user && user.experimental_ui; - var that = this; function getAndSetAccountLink(stillNeedToConfirmAccount) { @@ -140,9 +135,22 @@ class StripeAccountConnection extends React.Component<Props, State> { if (accountStatusResponse.charges_enabled) { // account has already been confirmed - that.setState({ + const eventuallyDueInformation = accountStatusResponse.account_info.requirements.eventually_due; + + const currentlyDueInformation = accountStatusResponse.account_info.requirements.currently_due; + + let objectToUpdateState = { accountConfirmed: true, - }); + stillRequiringVerification: false, + }; + + if ((eventuallyDueInformation && eventuallyDueInformation.length) || (currentlyDueInformation && currentlyDueInformation)) { + objectToUpdateState.stillRequiringVerification = true; + getAndSetAccountLink(false); + } + + that.setState(objectToUpdateState); + // user has not confirmed an account but have received payments } else if (accountStatusResponse.total_received_unpaid > 0) { that.setState({ @@ -184,35 +192,72 @@ class StripeAccountConnection extends React.Component<Props, State> { unpaidBalance, accountNotConfirmedButReceivedTips, pageTitle, - accountTransactions, + stillRequiringVerification, } = this.state; - const { user } = this.props; - - if (user.fiat_enabled) { - return ( - <Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation> - <Card - title={<div className="table__header-text">{__('Connect a bank account')}</div>} - isBodyList - body={ - <div> - {/* show while waiting for account status */} - {!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && ( - <div className="card__body-actions"> + return ( + <Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation> + <Card + title={<div className="table__header-text">{__('Connect a bank account')}</div>} + isBodyList + body={ + <div> + {/* show while waiting for account status */} + {!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && ( + <div className="card__body-actions"> + <div> <div> - <div> - <h3>{__('Getting your bank account connection status...')}</h3> - </div> + <h3>{__('Getting your bank account connection status...')}</h3> </div> </div> - )} - {/* user has yet to complete their integration */} - {!accountConfirmed && accountPendingConfirmation && ( - <div className="card__body-actions"> + </div> + )} + {/* user has yet to complete their integration */} + {!accountConfirmed && accountPendingConfirmation && ( + <div className="card__body-actions"> + <div> <div> + <h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3> + </div> + <div className="section__actions"> + <a href={stripeConnectionUrl}> + <Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} /> + </a> + </div> + </div> + </div> + )} + {/* user has completed their integration */} + {accountConfirmed && ( + <div className="card__body-actions"> + <div> + <div> + <h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3> + {stillRequiringVerification && <><h3 style={{marginTop: '10px'}}>Although your account is connected it still requires verification to begin receiving tips.</h3> + <h3 style={{marginTop: '10px'}}>Please use the button below to complete your verification process and enable tipping for your account.</h3></> } + </div> + </div> + </div> + )} + {/* TODO: hopefully we won't be using this anymore and can remove it */} + {accountNotConfirmedButReceivedTips && ( + <div className="card__body-actions"> + <div> + <div> + <h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3> <div> - <h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3> + <br /> + <h3> + {__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })} + </h3> + </div> + <br /> + <div> + <h3> + {__( + 'Connect your bank account to be able to cash your pending balance out to your account.' + )} + </h3> </div> <div className="section__actions"> <a href={stripeConnectionUrl}> @@ -221,127 +266,29 @@ class StripeAccountConnection extends React.Component<Props, State> { </div> </div> </div> - )} - {/* user has completed their integration */} - {accountConfirmed && ( - <div className="card__body-actions"> - <div> - <div> - <h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3> - {unpaidBalance > 0 ? ( - <div> - <br /> - <h3> - {__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })} - </h3> - </div> - ) : ( - <div> - <br /> - <h3> - {__('Your account balance is $0 USD. When you receive a tip you will see it here.')} - </h3> - </div> - )} - </div> - </div> - </div> - )} - {accountNotConfirmedButReceivedTips && ( - <div className="card__body-actions"> - <div> - <div> - <h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3> - <div> - <br /> - <h3> - {__('Your pending account balance is $%balance% USD.', { balance: unpaidBalance / 100 })} - </h3> - </div> - <br /> - <div> - <h3> - {__( - 'Connect your bank account to be able to cash your pending balance out to your account.' - )} - </h3> - </div> - <div className="section__actions"> - <a href={stripeConnectionUrl}> - <Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} /> - </a> - </div> - </div> - </div> - </div> - )} - </div> - } - /> - <br /> - - {/* customer already has transactions */} - {accountTransactions && accountTransactions.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>{__('Processing Fee')}</th> - <th>{__('Odysee Fee')}</th> - <th>{__('Received Amount')}</th> - </tr> - </thead> - <tbody> - {accountTransactions && - accountTransactions.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.transaction_fee / 100}</td> - <td>${transaction.application_fee / 100}</td> - <td>${transaction.received_amount / 100}</td> - </tr> - ))} - </tbody> - </table> - </div> - </> - } - /> - )} - </Page> - ); - } else { - return <></>; // probably null; - } + </div> + )} + </div> + } + actions={ + <>{ stillRequiringVerification && <Button + button="primary" + label={__('Complete Verification')} + icon={ICONS.SETTINGS} + navigate={stripeConnectionUrl} + className="stripe__complete-verification-button" + /> } + <Button + button="secondary" + label={__('View Transactions')} + icon={ICONS.SETTINGS} + navigate={`/$/${PAGES.WALLET}?tab=fiat-payment-history`} + /></> + } + /> + <br /> + </Page> + ); } } diff --git a/ui/page/settingsStripeCard/view.jsx b/ui/page/settingsStripeCard/view.jsx index ba8763143..d3bef38ef 100644 --- a/ui/page/settingsStripeCard/view.jsx +++ b/ui/page/settingsStripeCard/view.jsx @@ -6,11 +6,11 @@ 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'; +import * as PAGES from 'constants/pages'; let stripeEnvironment = 'test'; // if the key contains pk_live it's a live key @@ -354,13 +354,13 @@ class SettingsStripeCard extends React.Component<Props, State> { const { scriptFailedToLoad, openModal } = this.props; - const { currentFlowStage, customerTransactions, pageTitle, userCardDetails, paymentMethodId } = this.state; + 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 className="error__text">{__('There was an error connecting to Stripe. Please try again later.')}</div> )} </div> @@ -418,72 +418,18 @@ class SettingsStripeCard extends React.Component<Props, State> { /> </> } + actions={ + <Button + button="primary" + label={__('View Transactions')} + icon={ICONS.SETTINGS} + navigate={`/$/${PAGES.WALLET}?tab=fiat-account-history`} + /> + } /> <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> ); } diff --git a/ui/page/wallet/view.jsx b/ui/page/wallet/view.jsx index ba284ffe1..38332f361 100644 --- a/ui/page/wallet/view.jsx +++ b/ui/page/wallet/view.jsx @@ -1,11 +1,34 @@ // @flow import React from 'react'; -import { withRouter } from 'react-router'; +import { useHistory } from 'react-router'; import WalletBalance from 'component/walletBalance'; +import WalletFiatBalance from 'component/walletFiatBalance'; +import WalletFiatPaymentBalance from 'component/walletFiatPaymentBalance'; +import WalletFiatAccountHistory from 'component/walletFiatAccountHistory'; +import WalletFiatPaymentHistory from 'component/walletFiatPaymentHistory'; import TxoList from 'component/txoList'; import Page from 'component/page'; +import * as PAGES from 'constants/pages'; import Spinner from 'component/spinner'; import YrblWalletEmpty from 'component/yrblWalletEmpty'; +import { Lbryio } from 'lbryinc'; +import { STRIPE_PUBLIC_KEY } from 'config'; +import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'component/common/tabs'; + +const TAB_QUERY = 'tab'; + +const TABS = { + LBRY_CREDITS_TAB: 'credits', + ACCOUNT_HISTORY: 'fiat-account-history', + PAYMENT_HISTORY: 'fiat-payment-history', +}; + +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 = { history: { action: string, push: (string) => void, replace: (string) => void }, @@ -14,32 +37,199 @@ type Props = { }; const WalletPage = (props: Props) => { - const { location, totalBalance } = props; - const { search } = location; + const { + location: { search }, + push, + } = useHistory(); + + // @if TARGET='web' + const urlParams = new URLSearchParams(search); + + const currentView = urlParams.get(TAB_QUERY) || TABS.LBRY_CREDITS_TAB; + + let tabIndex; + switch (currentView) { + case TABS.LBRY_CREDITS_TAB: + tabIndex = 0; + break; + case TABS.PAYMENT_HISTORY: + tabIndex = 1; + break; + case TABS.ACCOUNT_HISTORY: + tabIndex = 2; + break; + default: + tabIndex = 0; + break; + } + + function onTabChange(newTabIndex) { + let url = `/$/${PAGES.WALLET}?`; + + if (newTabIndex === 0) { + url += `${TAB_QUERY}=${TABS.LBRY_CREDITS_TAB}`; + } else if (newTabIndex === 1) { + url += `${TAB_QUERY}=${TABS.PAYMENT_HISTORY}`; + } else if (newTabIndex === 2) { + url += `${TAB_QUERY}=${TABS.ACCOUNT_HISTORY}`; + } else { + url += `${TAB_QUERY}=${TABS.LBRY_CREDITS_TAB}`; + } + push(url); + } + + const [accountStatusResponse, setAccountStatusResponse] = React.useState(); + const [accountTransactionResponse, setAccountTransactionResponse] = React.useState([]); + const [customerTransactions, setCustomerTransactions] = React.useState([]); + + function getPaymentHistory() { + return Lbryio.call( + 'customer', + 'list', + { + environment: stripeEnvironment, + }, + 'post' + ); + } + + function getAccountStatus() { + return Lbryio.call( + 'account', + 'status', + { + environment: stripeEnvironment, + }, + 'post' + ); + } + + function getAccountTransactionsa() { + return Lbryio.call( + 'account', + 'list', + { + environment: stripeEnvironment, + }, + 'post' + ); + } + + // calculate account transactions section + React.useEffect(() => { + (async function() { + try { + const response = await getAccountStatus(); + + setAccountStatusResponse(response); + + // TODO: some weird naming clash hence getAccountTransactionsa + const getAccountTransactions = await getAccountTransactionsa(); + + setAccountTransactionResponse(getAccountTransactions); + } catch (err) { + console.log(err); + } + })(); + }, []); + + // populate customer payment data + React.useEffect(() => { + (async function() { + try { + // get card payments customer has made + let customerTransactionResponse = await getPaymentHistory(); + + customerTransactionResponse.reverse(); + + setCustomerTransactions(customerTransactionResponse); + } catch (err) { + console.log(err); + } + })(); + }, []); + + // @endif + + const { totalBalance } = props; const showIntro = totalBalance === 0; const loading = totalBalance === undefined; return ( - <Page> - {loading && ( - <div className="main--empty"> - <Spinner delayed /> - </div> - )} - {!loading && ( - <> - {showIntro ? ( - <YrblWalletEmpty includeWalletLink /> - ) : ( - <div className="card-stack"> - <WalletBalance /> - <TxoList search={search} /> - </div> - )} - </> - )} - </Page> + <> + {/* @if TARGET='web' */} + <Page> + <Tabs onChange={onTabChange} index={tabIndex}> + <TabList className="tabs__list--collection-edit-page"> + <Tab>{__('LBRY Credits')}</Tab> + <Tab>{__('Account History')}</Tab> + <Tab>{__('Payment History')}</Tab> + </TabList> + <TabPanels> + <TabPanel> + <div className="section card-stack"> + <div className="lbc-transactions"> + {/* if the transactions are loading */} + {loading && ( + <div className="main--empty"> + <Spinner delayed /> + </div> + )} + {/* when the transactions are finished loading */} + {!loading && ( + <> + {showIntro ? ( + <YrblWalletEmpty includeWalletLink /> + ) : ( + <div className="card-stack"> + <WalletBalance /> + <TxoList search={search} /> + </div> + )} + </> + )} + </div> + </div> + </TabPanel> + <TabPanel> + <div className="section card-stack"> + <WalletFiatBalance accountDetails={accountStatusResponse} /> + <WalletFiatAccountHistory transactions={accountTransactionResponse} /> + </div> + </TabPanel> + <TabPanel> + <div className="section card-stack"> + <WalletFiatPaymentBalance transactions={customerTransactions} accountDetails={accountStatusResponse} /> + <WalletFiatPaymentHistory transactions={customerTransactions} /> + </div> + </TabPanel> + </TabPanels> + </Tabs> + </Page> + {/* @endif */} + {/* @if TARGET='app' */} + <Page> + {loading && ( + <div className="main--empty"> + <Spinner delayed /> + </div> + )} + {!loading && ( + <> + {showIntro ? ( + <YrblWalletEmpty includeWalletLink /> + ) : ( + <div className="card-stack"> + <WalletBalance /> + <TxoList search={search} /> + </div> + )} + </> + )} + </Page> + {/* @endif */} + </> ); }; -export default withRouter(WalletPage); +export default WalletPage; diff --git a/ui/scss/component/_stripe-card.scss b/ui/scss/component/_stripe-card.scss index 8b7892055..064dccc94 100644 --- a/ui/scss/component/_stripe-card.scss +++ b/ui/scss/component/_stripe-card.scss @@ -340,3 +340,7 @@ pre { //.successCard, .toConfirmCard { // max-width: 94% //} + +.stripe__complete-verification-button { + margin-right: 10px !important; +}