tip modal no fiat
This commit is contained in:
parent
73214a94ec
commit
c66cfb28b5
17 changed files with 135 additions and 1602 deletions
|
@ -2193,5 +2193,6 @@
|
||||||
"Trending for #Art": "Trending for #Art",
|
"Trending for #Art": "Trending for #Art",
|
||||||
"Trending for #Btc": "Trending for #Btc",
|
"Trending for #Btc": "Trending for #Btc",
|
||||||
"Trending for #Music": "Trending for #Music",
|
"Trending for #Music": "Trending for #Music",
|
||||||
|
"You sent %lbc% as a tip, Mahalo!": "You sent %lbc% as a tip, Mahalo!",
|
||||||
"--end--": "--end--"
|
"--end--": "--end--"
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,8 +56,6 @@ import RepostNew from 'page/repost';
|
||||||
import RewardsPage from 'page/rewards';
|
import RewardsPage from 'page/rewards';
|
||||||
import RewardsVerifyPage from 'page/rewardsVerify';
|
import RewardsVerifyPage from 'page/rewardsVerify';
|
||||||
import SearchPage from 'page/search';
|
import SearchPage from 'page/search';
|
||||||
import SettingsStripeCard from 'page/settingsStripeCard';
|
|
||||||
import SettingsStripeAccount from 'page/settingsStripeAccount';
|
|
||||||
|
|
||||||
import SettingsCreatorPage from 'page/settingsCreator';
|
import SettingsCreatorPage from 'page/settingsCreator';
|
||||||
import SettingsNotificationsPage from 'page/settingsNotifications';
|
import SettingsNotificationsPage from 'page/settingsNotifications';
|
||||||
|
@ -279,8 +277,6 @@ function AppRouter(props: Props) {
|
||||||
component={isAuthenticated || !IS_WEB ? ChannelsFollowingPage : DiscoverPage}
|
component={isAuthenticated || !IS_WEB ? ChannelsFollowingPage : DiscoverPage}
|
||||||
/>
|
/>
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} />
|
|
||||||
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_UPDATE_PWD}`} component={UpdatePasswordPage} />
|
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_UPDATE_PWD}`} component={UpdatePasswordPage} />
|
||||||
<PrivateRoute
|
<PrivateRoute
|
||||||
{...props}
|
{...props}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import Card from 'component/common/card';
|
||||||
import SettingsRow from 'component/settingsRow';
|
import SettingsRow from 'component/settingsRow';
|
||||||
import SyncToggle from 'component/syncToggle';
|
import SyncToggle from 'component/syncToggle';
|
||||||
import { getPasswordFromCookie } from 'util/saved-passwords';
|
import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
// --- select ---
|
// --- select ---
|
||||||
|
@ -21,7 +20,7 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SettingAccount(props: Props) {
|
export default function SettingAccount(props: Props) {
|
||||||
const { isAuthenticated, walletEncrypted, user, myChannels, doWalletStatus } = props;
|
const { isAuthenticated, walletEncrypted, myChannels, doWalletStatus } = props;
|
||||||
const [storedPassword, setStoredPassword] = React.useState(false);
|
const [storedPassword, setStoredPassword] = React.useState(false);
|
||||||
|
|
||||||
// Determine if password is stored.
|
// Determine if password is stored.
|
||||||
|
@ -62,38 +61,6 @@ export default function SettingAccount(props: Props) {
|
||||||
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
|
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
{user && getStripeEnvironment() && (
|
|
||||||
<SettingsRow
|
|
||||||
title={__('Bank Accounts')}
|
|
||||||
subtitle={__('Connect a bank account to receive tips and compensation in your local currency.')}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
button="inverse"
|
|
||||||
label={__('Manage')}
|
|
||||||
icon={ICONS.ARROW_RIGHT}
|
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
)}
|
|
||||||
{/* @endif */}
|
|
||||||
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
{isAuthenticated && getStripeEnvironment() && (
|
|
||||||
<SettingsRow
|
|
||||||
title={__('Payment Methods')}
|
|
||||||
subtitle={__('Add a credit card to tip creators in their local currency.')}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
button="inverse"
|
|
||||||
label={__('Manage')}
|
|
||||||
icon={ICONS.ARROW_RIGHT}
|
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
|
||||||
/>
|
|
||||||
</SettingsRow>
|
|
||||||
)}
|
|
||||||
{/* @endif */}
|
|
||||||
|
|
||||||
{myChannels && (
|
{myChannels && (
|
||||||
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -10,7 +10,6 @@ import Card from 'component/common/card';
|
||||||
import LbcSymbol from 'component/common/lbc-symbol';
|
import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import { formatNumberWithCommas } from 'util/number';
|
import { formatNumberWithCommas } from 'util/number';
|
||||||
import WalletFiatBalance from 'component/walletFiatBalance';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
balance: number,
|
balance: number,
|
||||||
|
@ -64,8 +63,6 @@ const WalletBalance = (props: Props) => {
|
||||||
}, [doFetchUtxoCounts, balance, detailsExpanded]);
|
}, [doFetchUtxoCounts, balance, detailsExpanded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={'columns'}>
|
|
||||||
<div className="column">
|
|
||||||
<Card
|
<Card
|
||||||
title={<LbcSymbol postfix={formatNumberWithCommas(totalBalance)} isTitle />}
|
title={<LbcSymbol postfix={formatNumberWithCommas(totalBalance)} isTitle />}
|
||||||
subtitle={
|
subtitle={
|
||||||
|
@ -149,21 +146,14 @@ const WalletBalance = (props: Props) => {
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="help--warning">
|
<p className="help--warning">
|
||||||
{__(
|
{__('Your wallet is not currently synced with lbry.tv. You are in control of backing up your wallet.')}
|
||||||
'Your wallet is not currently synced with lbry.tv. You are in control of backing up your wallet.'
|
|
||||||
)}
|
|
||||||
<HelpLink navigate={`/$/${PAGES.BACKUP}`} />
|
<HelpLink navigate={`/$/${PAGES.BACKUP}`} />
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{/* @endif */}
|
{/* @endif */}
|
||||||
<div className="section__actions">
|
<div className="section__actions">
|
||||||
<Button button="primary" label={__('Buy')} icon={ICONS.BUY} navigate={`/$/${PAGES.BUY}`} />
|
<Button button="primary" label={__('Buy')} icon={ICONS.BUY} navigate={`/$/${PAGES.BUY}`} />
|
||||||
<Button
|
<Button button="secondary" label={__('Receive')} icon={ICONS.RECEIVE} navigate={`/$/${PAGES.RECEIVE}`} />
|
||||||
button="secondary"
|
|
||||||
label={__('Receive')}
|
|
||||||
icon={ICONS.RECEIVE}
|
|
||||||
navigate={`/$/${PAGES.RECEIVE}`}
|
|
||||||
/>
|
|
||||||
<Button button="secondary" label={__('Send')} icon={ICONS.SEND} navigate={`/$/${PAGES.SEND}`} />
|
<Button button="secondary" label={__('Send')} icon={ICONS.SEND} navigate={`/$/${PAGES.SEND}`} />
|
||||||
</div>
|
</div>
|
||||||
{(otherCount > WALLET_CONSOLIDATE_UTXOS || consolidateIsPending || consolidatingUtxos) && (
|
{(otherCount > WALLET_CONSOLIDATE_UTXOS || consolidateIsPending || consolidatingUtxos) && (
|
||||||
|
@ -183,20 +173,14 @@ const WalletBalance = (props: Props) => {
|
||||||
help: <HelpLink href="https://lbry.com/faq/transaction-types" />,
|
help: <HelpLink href="https://lbry.com/faq/transaction-types" />,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Your wallet has a lot of change lying around. Consolidating will speed up your transactions. This
|
Your wallet has a lot of change lying around. Consolidating will speed up your transactions. This could
|
||||||
could take some time. %now%%help%
|
take some time. %now%%help%
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="column">
|
|
||||||
{/* fiat balance card */}
|
|
||||||
<WalletFiatBalance />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
import FiatAccountHistory from './view';
|
|
||||||
|
|
||||||
export default FiatAccountHistory;
|
|
|
@ -1,77 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
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 (
|
|
||||||
<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 className="wallet__fiat-transactions">No Transactions</p>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WalletBalance;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import WalletFiatBalance from './view';
|
|
||||||
|
|
||||||
export default WalletFiatBalance;
|
|
|
@ -1,97 +0,0 @@
|
||||||
// @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';
|
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
|
||||||
|
|
||||||
const WalletBalance = () => {
|
|
||||||
const [accountStatusResponse, setAccountStatusResponse] = React.useState();
|
|
||||||
|
|
||||||
function getAccountStatus() {
|
|
||||||
return Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'status',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculate account transactions section
|
|
||||||
React.useEffect(() => {
|
|
||||||
(async function () {
|
|
||||||
try {
|
|
||||||
if (!stripeEnvironment) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await getAccountStatus();
|
|
||||||
|
|
||||||
setAccountStatusResponse(response);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}, [stripeEnvironment]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
<Card
|
|
||||||
title={
|
|
||||||
<>
|
|
||||||
<Icon size={18} icon={ICONS.FINANCE} />
|
|
||||||
{(accountStatusResponse &&
|
|
||||||
(accountStatusResponse.total_received_unpaid - accountStatusResponse.total_paid_out) / 100) ||
|
|
||||||
0}{' '}
|
|
||||||
USD
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
subtitle={
|
|
||||||
accountStatusResponse && accountStatusResponse.total_received_unpaid > 0 ? (
|
|
||||||
<I18nMessage>
|
|
||||||
This is your pending balance that will be automatically sent to your bank account.
|
|
||||||
</I18nMessage>
|
|
||||||
) : (
|
|
||||||
<I18nMessage>When you begin to receive tips your balance will be updated here.</I18nMessage>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<h2 className="section__title--small">
|
|
||||||
${(accountStatusResponse && accountStatusResponse.total_received_unpaid / 100) || 0} {__('Total Received Tips')}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<h2 className="section__title--small">
|
|
||||||
${(accountStatusResponse && accountStatusResponse.total_paid_out / 100) || 0} {__('Withdrawn')}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="section__actions">
|
|
||||||
<Button
|
|
||||||
button="secondary"
|
|
||||||
label={__('Bank Accounts')}
|
|
||||||
icon={ICONS.SETTINGS}
|
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
button="secondary"
|
|
||||||
label={__('Payment Methods')}
|
|
||||||
icon={ICONS.SETTINGS}
|
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WalletBalance;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import WalletFiatPaymentBalance from './view';
|
|
||||||
|
|
||||||
export default WalletFiatPaymentBalance;
|
|
|
@ -1,76 +0,0 @@
|
||||||
// @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;
|
|
|
@ -1,3 +0,0 @@
|
||||||
import WalletFiatPaymentHistory from './view';
|
|
||||||
|
|
||||||
export default WalletFiatPaymentHistory;
|
|
|
@ -1,109 +0,0 @@
|
||||||
// @flow
|
|
||||||
import React from 'react';
|
|
||||||
import Button from 'component/button';
|
|
||||||
import { Lbryio } from 'lbryinc';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
accountDetails: any,
|
|
||||||
transactions: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
const WalletBalance = (props: Props) => {
|
|
||||||
// receive transactions from parent component
|
|
||||||
const { transactions: accountTransactions } = props;
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<div className="section card-stack">
|
|
||||||
<div className="table__wrapper">
|
|
||||||
<table className="table table--transactions">
|
|
||||||
{/* table header */}
|
|
||||||
<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>
|
|
||||||
{/* list data for transactions */}
|
|
||||||
<tbody>
|
|
||||||
{accountTransactions &&
|
|
||||||
accountTransactions.map((transaction) => (
|
|
||||||
<tr key={transaction.name + transaction.created_at}>
|
|
||||||
{/* date */}
|
|
||||||
<td>{moment(transaction.created_at).format('LLL')}</td>
|
|
||||||
{/* receiving channel name */}
|
|
||||||
<td>
|
|
||||||
<Button
|
|
||||||
className=""
|
|
||||||
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
|
|
||||||
label={transaction.channel_name}
|
|
||||||
button="link"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
{/* link to content or channel */}
|
|
||||||
<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>
|
|
||||||
{/* how much tipped */}
|
|
||||||
<td>${transaction.tipped_amount / 100}</td>
|
|
||||||
{/* TODO: this is incorrect need it per transactions not per user */}
|
|
||||||
{/* last four of credit card */}
|
|
||||||
<td>{lastFour}</td>
|
|
||||||
{/* whether tip is anonymous or not */}
|
|
||||||
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{/* show some markup if there's no transactions */}
|
|
||||||
{(!accountTransactions || accountTransactions.length === 0) && (
|
|
||||||
<p className="wallet__fiat-transactions">No Transactions</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WalletBalance;
|
|
|
@ -15,17 +15,10 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
||||||
import { parseURI } from 'util/lbryURI';
|
import { parseURI } from 'util/lbryURI';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
|
||||||
|
|
||||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||||
const MINIMUM_FIAT_TIP = 1;
|
|
||||||
const MAXIMUM_FIAT_TIP = 1000;
|
|
||||||
|
|
||||||
const DEFAULT_TIP_ERROR = __('Sorry, there was an error in processing your payment!');
|
|
||||||
|
|
||||||
const TAB_BOOST = 'TabBoost';
|
const TAB_BOOST = 'TabBoost';
|
||||||
const TAB_FIAT = 'TabFiat';
|
|
||||||
const TAB_LBC = 'TabLBC';
|
const TAB_LBC = 'TabLBC';
|
||||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
||||||
|
|
||||||
|
@ -63,8 +56,6 @@ function WalletSendTip(props: Props) {
|
||||||
fetchingChannels,
|
fetchingChannels,
|
||||||
incognito,
|
incognito,
|
||||||
activeChannelClaim,
|
activeChannelClaim,
|
||||||
doToast,
|
|
||||||
isAuthenticated,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
/** REACT STATE **/
|
/** REACT STATE **/
|
||||||
|
@ -73,12 +64,6 @@ function WalletSendTip(props: Props) {
|
||||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||||
const [isConfirming, setIsConfirming] = React.useState(false);
|
const [isConfirming, setIsConfirming] = React.useState(false);
|
||||||
|
|
||||||
// only allow certain creators to receive tips
|
|
||||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
|
||||||
|
|
||||||
// show things conditionally based on if a user has a card already
|
|
||||||
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
|
|
||||||
|
|
||||||
// show the tip error on the frontend
|
// show the tip error on the frontend
|
||||||
const [tipError, setTipError] = React.useState();
|
const [tipError, setTipError] = React.useState();
|
||||||
|
|
||||||
|
@ -104,46 +89,6 @@ function WalletSendTip(props: Props) {
|
||||||
// channel name used in url
|
// channel name used in url
|
||||||
const { channelName } = parseURI(uri);
|
const { channelName } = parseURI(uri);
|
||||||
|
|
||||||
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
|
|
||||||
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
|
|
||||||
|
|
||||||
// setup variables for backend tip API
|
|
||||||
let channelClaimId, tipChannelName;
|
|
||||||
// if there is a signing channel it's on a file
|
|
||||||
if (claim.signing_channel) {
|
|
||||||
channelClaimId = claim.signing_channel.claim_id;
|
|
||||||
tipChannelName = claim.signing_channel.name;
|
|
||||||
|
|
||||||
// otherwise it's on the channel page
|
|
||||||
} else {
|
|
||||||
channelClaimId = claim.claim_id;
|
|
||||||
tipChannelName = claim.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceClaimId = claim.claim_id;
|
|
||||||
|
|
||||||
// check if creator has a payment method saved
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (channelClaimId && isAuthenticated && stripeEnvironment) {
|
|
||||||
Lbryio.call(
|
|
||||||
'customer',
|
|
||||||
'status',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((customerStatusResponse) => {
|
|
||||||
const defaultPaymentMethodId =
|
|
||||||
customerStatusResponse.Customer &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
|
||||||
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
|
||||||
|
|
||||||
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [channelClaimId, isAuthenticated, stripeEnvironment]);
|
|
||||||
|
|
||||||
// focus tip element if it exists
|
// focus tip element if it exists
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const tipInputElement = document.getElementById('tip-input');
|
const tipInputElement = document.getElementById('tip-input');
|
||||||
|
@ -152,32 +97,6 @@ function WalletSendTip(props: Props) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// check if user can receive tips
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (channelClaimId && stripeEnvironment) {
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'check',
|
|
||||||
{
|
|
||||||
channel_claim_id: channelClaimId,
|
|
||||||
channel_name: tipChannelName,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((accountCheckResponse) => {
|
|
||||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
|
||||||
setCanReceiveFiatTip(true);
|
|
||||||
} else {
|
|
||||||
setCanReceiveFiatTip(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
// console.log(error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [channelClaimId, stripeEnvironment]);
|
|
||||||
|
|
||||||
// if user has no balance, used to show conditional frontend
|
// if user has no balance, used to show conditional frontend
|
||||||
const noBalance = balance === 0;
|
const noBalance = balance === 0;
|
||||||
|
|
||||||
|
@ -208,12 +127,6 @@ function WalletSendTip(props: Props) {
|
||||||
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {
|
||||||
claimTypeText,
|
claimTypeText,
|
||||||
});
|
});
|
||||||
} else if (activeTab === TAB_FIAT) {
|
|
||||||
iconToUse = ICONS.FINANCE;
|
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation in USD.');
|
|
||||||
// if (!hasCardSaved) {
|
|
||||||
// explainerText += __('You must add a card to use this functionality.');
|
|
||||||
// }
|
|
||||||
} else if (activeTab === TAB_LBC) {
|
} else if (activeTab === TAB_LBC) {
|
||||||
iconToUse = ICONS.LBC;
|
iconToUse = ICONS.LBC;
|
||||||
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
explainerText = __('Show this channel your appreciation by sending a donation of Credits.');
|
||||||
|
@ -233,7 +146,7 @@ function WalletSendTip(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's not fiat, aka it's boost or lbc tip
|
// if it's not fiat, aka it's boost or lbc tip
|
||||||
else if (activeTab !== TAB_FIAT) {
|
else {
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||||
const validTipInput = regexp.test(String(tipAmount));
|
const validTipInput = regexp.test(String(tipAmount));
|
||||||
|
|
||||||
|
@ -249,17 +162,6 @@ function WalletSendTip(props: Props) {
|
||||||
tipError = __('Amount must be higher');
|
tipError = __('Amount must be higher');
|
||||||
}
|
}
|
||||||
// if tip fiat tab
|
// if tip fiat tab
|
||||||
} else {
|
|
||||||
regexp = RegExp(/^(\d*([.]\d{0,2})?)$/);
|
|
||||||
const validTipInput = regexp.test(String(tipAmount));
|
|
||||||
|
|
||||||
if (!validTipInput) {
|
|
||||||
tipError = __('Amount must have no more than 2 decimal places');
|
|
||||||
} else if (tipAmount < MINIMUM_FIAT_TIP) {
|
|
||||||
tipError = __('Amount must be at least one dollar');
|
|
||||||
} else if (tipAmount > MAXIMUM_FIAT_TIP) {
|
|
||||||
tipError = __('Amount cannot be over 1000 dollars');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTipError(tipError);
|
setTipError(tipError);
|
||||||
|
@ -289,7 +191,7 @@ function WalletSendTip(props: Props) {
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
if (tipAmount && claimId) {
|
if (tipAmount && claimId) {
|
||||||
// send an instant tip (no need to go to an exchange first)
|
// send an instant tip (no need to go to an exchange first)
|
||||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
if (instantTipEnabled) {
|
||||||
if (instantTipMax.currency === 'LBC') {
|
if (instantTipMax.currency === 'LBC') {
|
||||||
sendSupportOrConfirm(instantTipMax.amount);
|
sendSupportOrConfirm(instantTipMax.amount);
|
||||||
} else {
|
} else {
|
||||||
|
@ -299,106 +201,17 @@ function WalletSendTip(props: Props) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// sending fiat tip
|
// sending fiat tip
|
||||||
} else if (activeTab === TAB_FIAT) {
|
|
||||||
if (!isConfirming) {
|
|
||||||
setIsConfirming(true);
|
|
||||||
} else if (isConfirming) {
|
|
||||||
let sendAnonymously = !activeChannelClaim || incognito;
|
|
||||||
|
|
||||||
// hit backend to send tip
|
|
||||||
Lbryio.call(
|
|
||||||
'customer',
|
|
||||||
'tip',
|
|
||||||
{
|
|
||||||
// round to fix issues with floating point numbers
|
|
||||||
amount: Math.round(100 * tipAmount), // convert from dollars to cents
|
|
||||||
creator_channel_name: tipChannelName, // creator_channel_name
|
|
||||||
creator_channel_claim_id: channelClaimId,
|
|
||||||
tipper_channel_name: sendAnonymously ? '' : activeChannelName,
|
|
||||||
tipper_channel_claim_id: sendAnonymously ? '' : activeChannelId,
|
|
||||||
currency: 'USD',
|
|
||||||
anonymous: sendAnonymously,
|
|
||||||
source_claim_id: sourceClaimId,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((customerTipResponse) => {
|
|
||||||
doToast({
|
|
||||||
message: __("You sent $%amount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
|
|
||||||
amount: tipAmount,
|
|
||||||
tipChannelName,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
// show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
|
|
||||||
let displayError;
|
|
||||||
if (error.message) {
|
|
||||||
displayError = error.message;
|
|
||||||
} else {
|
|
||||||
displayError = DEFAULT_TIP_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
doToast({ message: displayError, isError: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
closeModal();
|
|
||||||
}
|
|
||||||
// if it's a boost (?)
|
|
||||||
} else {
|
} else {
|
||||||
sendSupportOrConfirm();
|
sendSupportOrConfirm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const countDecimals = function (value) {
|
|
||||||
const text = value.toString();
|
|
||||||
const index = text.indexOf('.');
|
|
||||||
return text.length - index - 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||||
let tipAmountAsString = event.target.value;
|
let tipAmountAsString = event.target.value;
|
||||||
|
|
||||||
let tipAmount = parseFloat(tipAmountAsString);
|
let tipAmount = parseFloat(tipAmountAsString);
|
||||||
|
|
||||||
const howManyDecimals = countDecimals(tipAmountAsString);
|
|
||||||
|
|
||||||
// fiat tip input
|
|
||||||
if (activeTab === TAB_FIAT) {
|
|
||||||
if (Number.isNaN(tipAmount)) {
|
|
||||||
setCustomTipAmount('');
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow maximum of two decimal places
|
|
||||||
if (howManyDecimals > 2) {
|
|
||||||
tipAmount = Math.floor(tipAmount * 100) / 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove decimals, and then get number of digits
|
|
||||||
const howManyDigits = Math.trunc(tipAmount).toString().length;
|
|
||||||
|
|
||||||
if (howManyDigits > 4 && tipAmount !== 1000) {
|
|
||||||
setTipError('Amount cannot be over 1000 dollars');
|
|
||||||
setCustomTipAmount(tipAmount);
|
setCustomTipAmount(tipAmount);
|
||||||
} else if (tipAmount > 1000) {
|
|
||||||
setTipError('Amount cannot be over 1000 dollars');
|
|
||||||
setCustomTipAmount(tipAmount);
|
|
||||||
} else {
|
|
||||||
setCustomTipAmount(tipAmount);
|
|
||||||
}
|
|
||||||
// LBC tip input
|
|
||||||
} else {
|
|
||||||
// TODO: this is a bit buggy, needs a touchup
|
|
||||||
// if (howManyDecimals > 9) {
|
|
||||||
// // only allows up to 8 decimal places
|
|
||||||
// tipAmount = Number(tipAmount.toString().match(/^-?\d+(?:\.\d{0,8})?/)[0]);
|
|
||||||
//
|
|
||||||
// setTipError('Please only use up to 8 decimals');
|
|
||||||
// }
|
|
||||||
setCustomTipAmount(tipAmount);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildButtonText() {
|
function buildButtonText() {
|
||||||
|
@ -407,28 +220,17 @@ function WalletSendTip(props: Props) {
|
||||||
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
||||||
// also sometimes it's returned as a string
|
// also sometimes it's returned as a string
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
if (tipAmount !== tipAmount || tipAmount === 'NaN') {
|
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertToTwoDecimals(number) {
|
|
||||||
return (Math.round(number * 100) / 100).toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
const amountToShow = activeTab === TAB_FIAT ? convertToTwoDecimals(tipAmount) : tipAmount;
|
|
||||||
|
|
||||||
// if it's a valid number display it, otherwise do an empty string
|
// if it's a valid number display it, otherwise do an empty string
|
||||||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||||
|
|
||||||
// build button text based on tab
|
// build button text based on tab
|
||||||
if (activeTab === TAB_BOOST) {
|
if (activeTab === TAB_BOOST) {
|
||||||
return claimIsMine
|
return claimIsMine
|
||||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||||
} else if (activeTab === TAB_FIAT) {
|
|
||||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
|
||||||
} else if (activeTab === TAB_LBC) {
|
} else if (activeTab === TAB_LBC) {
|
||||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||||
}
|
}
|
||||||
|
@ -436,17 +238,13 @@ function WalletSendTip(props: Props) {
|
||||||
|
|
||||||
// dont allow user to click send button
|
// dont allow user to click send button
|
||||||
function shouldDisableAmountSelector(amount) {
|
function shouldDisableAmountSelector(amount) {
|
||||||
return (
|
return amount > balance;
|
||||||
(amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// showed on confirm page above amount
|
// showed on confirm page above amount
|
||||||
function setConfirmLabel() {
|
function setConfirmLabel() {
|
||||||
if (activeTab === TAB_LBC) {
|
if (activeTab === TAB_LBC) {
|
||||||
return __('Tipping Credit');
|
return __('Tipping Credit');
|
||||||
} else if (activeTab === TAB_FIAT) {
|
|
||||||
return __('Tipping Fiat (USD)');
|
|
||||||
} else if (activeTab === TAB_BOOST) {
|
} else if (activeTab === TAB_BOOST) {
|
||||||
return __('Boosting');
|
return __('Boosting');
|
||||||
}
|
}
|
||||||
|
@ -488,27 +286,6 @@ function WalletSendTip(props: Props) {
|
||||||
}}
|
}}
|
||||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||||
/>
|
/>
|
||||||
{/* tip fiat tab button */}
|
|
||||||
{/* @if TARGET='web' */}
|
|
||||||
{stripeEnvironment && (
|
|
||||||
<Button
|
|
||||||
key="tip-fiat"
|
|
||||||
icon={ICONS.FINANCE}
|
|
||||||
label={__('Tip')}
|
|
||||||
button="alt"
|
|
||||||
onClick={() => {
|
|
||||||
const tipInputElement = document.getElementById('tip-input');
|
|
||||||
if (tipInputElement) {
|
|
||||||
tipInputElement.focus();
|
|
||||||
}
|
|
||||||
if (!isConfirming) {
|
|
||||||
setActiveTab(TAB_FIAT);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_FIAT })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{/* @endif */}
|
|
||||||
{/* tip LBC tab button */}
|
{/* tip LBC tab button */}
|
||||||
<Button
|
<Button
|
||||||
key="boost"
|
key="boost"
|
||||||
|
@ -551,11 +328,7 @@ function WalletSendTip(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||||
<div className="confirm__value">
|
<div className="confirm__value">
|
||||||
{activeTab === TAB_FIAT ? (
|
|
||||||
<p>$ {(Math.round(tipAmount * 100) / 100).toFixed(2)}</p>
|
|
||||||
) : (
|
|
||||||
<LbcSymbol postfix={tipAmount} size={22} />
|
<LbcSymbol postfix={tipAmount} size={22} />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -570,14 +343,6 @@ function WalletSendTip(props: Props) {
|
||||||
<ChannelSelector />
|
<ChannelSelector />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* prompt to save a card */}
|
|
||||||
{activeTab === TAB_FIAT && !hasCardSaved && (
|
|
||||||
<h3 className="add-card-prompt">
|
|
||||||
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />{' '}
|
|
||||||
{__('To Tip Creators')}
|
|
||||||
</h3>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* section to pick tip/boost amount */}
|
{/* section to pick tip/boost amount */}
|
||||||
<div className="section">
|
<div className="section">
|
||||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||||
|
@ -607,10 +372,9 @@ function WalletSendTip(props: Props) {
|
||||||
label={__('Custom')}
|
label={__('Custom')}
|
||||||
onClick={() => setUseCustomTip(true)}
|
onClick={() => setUseCustomTip(true)}
|
||||||
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
// disabled if it's receive fiat and there is no card or creator can't receive tips
|
||||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && activeTab !== TAB_FIAT && (
|
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
||||||
<Button
|
<Button
|
||||||
button="secondary"
|
button="secondary"
|
||||||
className="button-toggle-group-action"
|
className="button-toggle-group-action"
|
||||||
|
@ -629,15 +393,11 @@ function WalletSendTip(props: Props) {
|
||||||
label={
|
label={
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
{__('Custom support amount')}{' '}
|
{__('Custom support amount')}{' '}
|
||||||
{activeTab !== TAB_FIAT ? (
|
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||||
>
|
>
|
||||||
(%lbc_balance% Credits available)
|
(%lbc_balance% Credits available)
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
) : (
|
|
||||||
'in USD'
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
}
|
}
|
||||||
error={tipError}
|
error={tipError}
|
||||||
|
@ -645,7 +405,7 @@ function WalletSendTip(props: Props) {
|
||||||
step="any"
|
step="any"
|
||||||
type="number"
|
type="number"
|
||||||
style={{
|
style={{
|
||||||
width: activeTab === TAB_FIAT ? '99px' : '160px',
|
width: '160px',
|
||||||
}}
|
}}
|
||||||
placeholder="1.23"
|
placeholder="1.23"
|
||||||
value={customTipAmount}
|
value={customTipAmount}
|
||||||
|
@ -661,24 +421,12 @@ function WalletSendTip(props: Props) {
|
||||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||||
button="primary"
|
button="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={
|
disabled={fetchingChannels || isPending || tipError || !tipAmount}
|
||||||
fetchingChannels ||
|
|
||||||
isPending ||
|
|
||||||
tipError ||
|
|
||||||
!tipAmount ||
|
|
||||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
|
||||||
}
|
|
||||||
label={buildButtonText()}
|
label={buildButtonText()}
|
||||||
/>
|
/>
|
||||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||||
</div>
|
</div>
|
||||||
{activeTab !== TAB_FIAT ? (
|
|
||||||
<WalletSpendableBalanceHelp />
|
<WalletSpendableBalanceHelp />
|
||||||
) : !canReceiveFiatTip ? (
|
|
||||||
<div className="help">{__('Only creators that verify cash accounts can receive tips')}</div>
|
|
||||||
) : (
|
|
||||||
<div className="help">{__('The payment will be made from your saved card')}</div>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
// if it's LBC and there is no balance, you can prompt to purchase LBC
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import StripeAccountConnection from './view';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
|
|
||||||
const select = (state) => ({});
|
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default withRouter(connect(select, perform)(StripeAccountConnection));
|
|
|
@ -1,313 +0,0 @@
|
||||||
// @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 } from 'config';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
|
|
||||||
const isDev = process.env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
|
||||||
let successStripeRedirectUrl, failureStripeRedirectUrl;
|
|
||||||
let successEndpoint = '/$/settings/tip_account';
|
|
||||||
let failureEndpoint = '/$/settings/tip_account';
|
|
||||||
if (isDev) {
|
|
||||||
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
|
|
||||||
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
|
|
||||||
} else {
|
|
||||||
successStripeRedirectUrl = URL + successEndpoint;
|
|
||||||
failureStripeRedirectUrl = URL + failureEndpoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
source: string,
|
|
||||||
doOpenModal: (string, {}) => void,
|
|
||||||
doToast: ({ message: string }) => void,
|
|
||||||
};
|
|
||||||
|
|
||||||
type State = {
|
|
||||||
error: boolean,
|
|
||||||
loading: boolean,
|
|
||||||
content: ?string,
|
|
||||||
stripeConnectionUrl: string,
|
|
||||||
accountConfirmed: boolean,
|
|
||||||
accountPendingConfirmation: boolean,
|
|
||||||
accountNotConfirmedButReceivedTips: boolean,
|
|
||||||
unpaidBalance: number,
|
|
||||||
pageTitle: string,
|
|
||||||
stillRequiringVerification: boolean,
|
|
||||||
accountTransactions: any,
|
|
||||||
};
|
|
||||||
|
|
||||||
class StripeAccountConnection extends React.Component<Props, State> {
|
|
||||||
constructor(props: Props) {
|
|
||||||
super(props);
|
|
||||||
this.state = {
|
|
||||||
error: false,
|
|
||||||
content: null,
|
|
||||||
loading: true,
|
|
||||||
accountConfirmed: false,
|
|
||||||
accountPendingConfirmation: false,
|
|
||||||
accountNotConfirmedButReceivedTips: false,
|
|
||||||
unpaidBalance: 0,
|
|
||||||
stripeConnectionUrl: '',
|
|
||||||
pageTitle: 'Add Payout Method',
|
|
||||||
stillRequiringVerification: false,
|
|
||||||
accountTransactions: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
let doToast = this.props.doToast;
|
|
||||||
|
|
||||||
var that = this;
|
|
||||||
|
|
||||||
function getAndSetAccountLink(stillNeedToConfirmAccount) {
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'link',
|
|
||||||
{
|
|
||||||
return_url: successStripeRedirectUrl,
|
|
||||||
refresh_url: failureStripeRedirectUrl,
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((accountLinkResponse) => {
|
|
||||||
// stripe link for user to navigate to and confirm account
|
|
||||||
const stripeConnectionUrl = accountLinkResponse.url;
|
|
||||||
|
|
||||||
// set connection url on frontend
|
|
||||||
that.setState({
|
|
||||||
stripeConnectionUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
// show the account confirmation link if not created already
|
|
||||||
if (stillNeedToConfirmAccount) {
|
|
||||||
that.setState({
|
|
||||||
accountPendingConfirmation: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// call the account status endpoint
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'status',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
)
|
|
||||||
.then((accountStatusResponse) => {
|
|
||||||
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
|
|
||||||
if (yetToBeCashedOutBalance) {
|
|
||||||
that.setState({
|
|
||||||
unpaidBalance: yetToBeCashedOutBalance,
|
|
||||||
});
|
|
||||||
|
|
||||||
Lbryio.call(
|
|
||||||
'account',
|
|
||||||
'list',
|
|
||||||
{
|
|
||||||
environment: stripeEnvironment,
|
|
||||||
},
|
|
||||||
'post'
|
|
||||||
).then((accountListResponse: any) => {
|
|
||||||
// TODO type this
|
|
||||||
that.setState({
|
|
||||||
accountTransactions: accountListResponse.reverse(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// if charges already enabled, no need to generate an account link
|
|
||||||
if (accountStatusResponse.charges_enabled) {
|
|
||||||
// account has already been confirmed
|
|
||||||
|
|
||||||
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.length)
|
|
||||||
) {
|
|
||||||
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({
|
|
||||||
accountNotConfirmedButReceivedTips: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
getAndSetAccountLink();
|
|
||||||
|
|
||||||
// user has not received any amount or confirmed an account
|
|
||||||
} else {
|
|
||||||
// get stripe link and set it on the frontend
|
|
||||||
// pass true so it updates the frontend
|
|
||||||
getAndSetAccountLink(true);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function (error) {
|
|
||||||
// errorString passed from the API (with a 403 error)
|
|
||||||
const errorString = 'account not linked to user, please link first';
|
|
||||||
|
|
||||||
// if it's beamer's error indicating the account is not linked yet
|
|
||||||
if (error.message.indexOf(errorString) > -1) {
|
|
||||||
// get stripe link and set it on the frontend
|
|
||||||
getAndSetAccountLink(true);
|
|
||||||
} else {
|
|
||||||
// probably an error from stripe
|
|
||||||
const displayString = __('There was an error getting your account setup, please try again later');
|
|
||||||
doToast({ message: displayString, isError: true });
|
|
||||||
// not an error from Beamer, throw it
|
|
||||||
throw new Error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {
|
|
||||||
stripeConnectionUrl,
|
|
||||||
accountConfirmed,
|
|
||||||
accountPendingConfirmation,
|
|
||||||
unpaidBalance,
|
|
||||||
accountNotConfirmedButReceivedTips,
|
|
||||||
pageTitle,
|
|
||||||
stillRequiringVerification,
|
|
||||||
} = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
noFooter
|
|
||||||
noSideNavigation
|
|
||||||
settingsPage
|
|
||||||
className="card-stack"
|
|
||||||
backout={{ title: pageTitle, backLabel: __('Back') }}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<h3>{__('Getting your bank account connection status...')}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
}
|
|
||||||
// only show additional buttons if its for additional verification or to show transaction page
|
|
||||||
actions={(stillRequiringVerification || accountConfirmed) &&
|
|
||||||
<>
|
|
||||||
{stillRequiringVerification && (
|
|
||||||
<Button
|
|
||||||
button="primary"
|
|
||||||
label={__('Complete Verification')}
|
|
||||||
icon={ICONS.SETTINGS}
|
|
||||||
navigate={stripeConnectionUrl}
|
|
||||||
className="stripe__complete-verification-button"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{accountConfirmed && (
|
|
||||||
<Button
|
|
||||||
button="secondary"
|
|
||||||
label={__('View Transactions')}
|
|
||||||
icon={ICONS.SETTINGS}
|
|
||||||
navigate={`/$/${PAGES.WALLET}?fiatType=incoming&tab=fiat-payment-history¤cy=fiat`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default StripeAccountConnection;
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { doSetClientSetting } from 'redux/actions/settings';
|
|
||||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
|
||||||
import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user';
|
|
||||||
import { doOpenModal } from 'redux/actions/app';
|
|
||||||
import { doToast } from 'redux/actions/notifications';
|
|
||||||
|
|
||||||
import SettingsStripeCard from './view';
|
|
||||||
|
|
||||||
const select = (state) => ({
|
|
||||||
osNotificationsEnabled: selectosNotificationsEnabled(state),
|
|
||||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
|
||||||
email: selectUserEmail(state),
|
|
||||||
});
|
|
||||||
|
|
||||||
const perform = (dispatch) => ({
|
|
||||||
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
|
|
||||||
doOpenModal,
|
|
||||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
|
||||||
doToast: (options) => dispatch(doToast(options)),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(select, perform)(SettingsStripeCard);
|
|
|
@ -1,444 +0,0 @@
|
||||||
// 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 { STRIPE_PUBLIC_KEY } from 'config';
|
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
|
||||||
|
|
||||||
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
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
|
||||||
noFooter
|
|
||||||
noSideNavigation
|
|
||||||
settingsPage
|
|
||||||
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="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="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¤cy=fiat`}
|
|
||||||
style={{marginLeft: '10px'}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Page>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default SettingsStripeCard;
|
|
||||||
/* eslint-enable no-undef */
|
|
||||||
/* eslint-enable react/prop-types */
|
|
Loading…
Reference in a new issue