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 #Btc": "Trending for #Btc",
|
||||
"Trending for #Music": "Trending for #Music",
|
||||
"You sent %lbc% as a tip, Mahalo!": "You sent %lbc% as a tip, Mahalo!",
|
||||
"--end--": "--end--"
|
||||
}
|
||||
|
|
|
@ -56,8 +56,6 @@ import RepostNew from 'page/repost';
|
|||
import RewardsPage from 'page/rewards';
|
||||
import RewardsVerifyPage from 'page/rewardsVerify';
|
||||
import SearchPage from 'page/search';
|
||||
import SettingsStripeCard from 'page/settingsStripeCard';
|
||||
import SettingsStripeAccount from 'page/settingsStripeAccount';
|
||||
|
||||
import SettingsCreatorPage from 'page/settingsCreator';
|
||||
import SettingsNotificationsPage from 'page/settingsNotifications';
|
||||
|
@ -279,8 +277,6 @@ function AppRouter(props: Props) {
|
|||
component={isAuthenticated || !IS_WEB ? ChannelsFollowingPage : DiscoverPage}
|
||||
/>
|
||||
<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}
|
||||
|
|
|
@ -8,7 +8,6 @@ import Card from 'component/common/card';
|
|||
import SettingsRow from 'component/settingsRow';
|
||||
import SyncToggle from 'component/syncToggle';
|
||||
import { getPasswordFromCookie } from 'util/saved-passwords';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
|
||||
type Props = {
|
||||
// --- select ---
|
||||
|
@ -21,7 +20,7 @@ type 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);
|
||||
|
||||
// Determine if password is stored.
|
||||
|
@ -62,38 +61,6 @@ export default function SettingAccount(props: Props) {
|
|||
<SyncToggle disabled={walletEncrypted && !storedPassword && storedPassword !== ''} />
|
||||
{/* @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 && (
|
||||
<SettingsRow title={__('Comments')} subtitle={__('View your past comments.')}>
|
||||
<Button
|
||||
|
|
|
@ -10,7 +10,6 @@ import Card from 'component/common/card';
|
|||
import LbcSymbol from 'component/common/lbc-symbol';
|
||||
import I18nMessage from 'component/i18nMessage';
|
||||
import { formatNumberWithCommas } from 'util/number';
|
||||
import WalletFiatBalance from 'component/walletFiatBalance';
|
||||
|
||||
type Props = {
|
||||
balance: number,
|
||||
|
@ -64,8 +63,6 @@ const WalletBalance = (props: Props) => {
|
|||
}, [doFetchUtxoCounts, balance, detailsExpanded]);
|
||||
|
||||
return (
|
||||
<div className={'columns'}>
|
||||
<div className="column">
|
||||
<Card
|
||||
title={<LbcSymbol postfix={formatNumberWithCommas(totalBalance)} isTitle />}
|
||||
subtitle={
|
||||
|
@ -149,21 +146,14 @@ const WalletBalance = (props: Props) => {
|
|||
</p>
|
||||
) : (
|
||||
<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}`} />
|
||||
</p>
|
||||
)}
|
||||
{/* @endif */}
|
||||
<div className="section__actions">
|
||||
<Button button="primary" label={__('Buy')} icon={ICONS.BUY} navigate={`/$/${PAGES.BUY}`} />
|
||||
<Button
|
||||
button="secondary"
|
||||
label={__('Receive')}
|
||||
icon={ICONS.RECEIVE}
|
||||
navigate={`/$/${PAGES.RECEIVE}`}
|
||||
/>
|
||||
<Button button="secondary" label={__('Receive')} icon={ICONS.RECEIVE} navigate={`/$/${PAGES.RECEIVE}`} />
|
||||
<Button button="secondary" label={__('Send')} icon={ICONS.SEND} navigate={`/$/${PAGES.SEND}`} />
|
||||
</div>
|
||||
{(otherCount > WALLET_CONSOLIDATE_UTXOS || consolidateIsPending || consolidatingUtxos) && (
|
||||
|
@ -183,20 +173,14 @@ const WalletBalance = (props: Props) => {
|
|||
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
|
||||
could take some time. %now%%help%
|
||||
Your wallet has a lot of change lying around. Consolidating will speed up your transactions. This could
|
||||
take some time. %now%%help%
|
||||
</I18nMessage>
|
||||
</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 usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import { getStripeEnvironment } from 'util/stripe';
|
||||
let stripeEnvironment = getStripeEnvironment();
|
||||
|
||||
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_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
||||
|
||||
|
@ -63,8 +56,6 @@ function WalletSendTip(props: Props) {
|
|||
fetchingChannels,
|
||||
incognito,
|
||||
activeChannelClaim,
|
||||
doToast,
|
||||
isAuthenticated,
|
||||
} = props;
|
||||
|
||||
/** REACT STATE **/
|
||||
|
@ -73,12 +64,6 @@ function WalletSendTip(props: Props) {
|
|||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', 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
|
||||
const [tipError, setTipError] = React.useState();
|
||||
|
||||
|
@ -104,46 +89,6 @@ function WalletSendTip(props: Props) {
|
|||
// channel name used in url
|
||||
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
|
||||
React.useEffect(() => {
|
||||
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
|
||||
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.', {
|
||||
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) {
|
||||
iconToUse = ICONS.LBC;
|
||||
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
|
||||
else if (activeTab !== TAB_FIAT) {
|
||||
else {
|
||||
regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(tipAmount));
|
||||
|
||||
|
@ -249,17 +162,6 @@ function WalletSendTip(props: Props) {
|
|||
tipError = __('Amount must be higher');
|
||||
}
|
||||
// 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);
|
||||
|
@ -289,7 +191,7 @@ function WalletSendTip(props: Props) {
|
|||
function handleSubmit() {
|
||||
if (tipAmount && claimId) {
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipEnabled) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
|
@ -299,106 +201,17 @@ function WalletSendTip(props: Props) {
|
|||
});
|
||||
}
|
||||
// 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 {
|
||||
sendSupportOrConfirm();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const countDecimals = function (value) {
|
||||
const text = value.toString();
|
||||
const index = text.indexOf('.');
|
||||
return text.length - index - 1;
|
||||
};
|
||||
|
||||
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
|
||||
let tipAmountAsString = event.target.value;
|
||||
|
||||
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);
|
||||
} 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() {
|
||||
|
@ -407,28 +220,17 @@ function WalletSendTip(props: Props) {
|
|||
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
||||
// also sometimes it's returned as a string
|
||||
// eslint-disable-next-line
|
||||
if (tipAmount !== tipAmount || tipAmount === 'NaN') {
|
||||
return true;
|
||||
return tipAmount !== tipAmount || tipAmount === 'NaN';
|
||||
}
|
||||
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
|
||||
const displayAmount = !isNan(tipAmount) ? amountToShow : '';
|
||||
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||
|
||||
// build button text based on tab
|
||||
if (activeTab === TAB_BOOST) {
|
||||
return claimIsMine
|
||||
? __('Boost Your %claimTypeText%', { claimTypeText })
|
||||
: __('Boost This %claimTypeText%', { claimTypeText });
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return __('Send a $%displayAmount% Tip', { displayAmount });
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
return __('Send a %displayAmount% Credit Tip', { displayAmount });
|
||||
}
|
||||
|
@ -436,17 +238,13 @@ function WalletSendTip(props: Props) {
|
|||
|
||||
// dont allow user to click send button
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
return (
|
||||
(amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
);
|
||||
return amount > balance;
|
||||
}
|
||||
|
||||
// showed on confirm page above amount
|
||||
function setConfirmLabel() {
|
||||
if (activeTab === TAB_LBC) {
|
||||
return __('Tipping Credit');
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return __('Tipping Fiat (USD)');
|
||||
} else if (activeTab === TAB_BOOST) {
|
||||
return __('Boosting');
|
||||
}
|
||||
|
@ -488,27 +286,6 @@ function WalletSendTip(props: Props) {
|
|||
}}
|
||||
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 */}
|
||||
<Button
|
||||
key="boost"
|
||||
|
@ -551,11 +328,7 @@ function WalletSendTip(props: Props) {
|
|||
</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__value">
|
||||
{activeTab === TAB_FIAT ? (
|
||||
<p>$ {(Math.round(tipAmount * 100) / 100).toFixed(2)}</p>
|
||||
) : (
|
||||
<LbcSymbol postfix={tipAmount} size={22} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -570,14 +343,6 @@ function WalletSendTip(props: Props) {
|
|||
<ChannelSelector />
|
||||
</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 */}
|
||||
<div className="section">
|
||||
{DEFAULT_TIP_AMOUNTS.map((amount) => (
|
||||
|
@ -607,10 +372,9 @@ function WalletSendTip(props: Props) {
|
|||
label={__('Custom')}
|
||||
onClick={() => setUseCustomTip(true)}
|
||||
// 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="secondary"
|
||||
className="button-toggle-group-action"
|
||||
|
@ -629,15 +393,11 @@ function WalletSendTip(props: Props) {
|
|||
label={
|
||||
<React.Fragment>
|
||||
{__('Custom support amount')}{' '}
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<I18nMessage
|
||||
tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} showLBC={false} /> }}
|
||||
>
|
||||
(%lbc_balance% Credits available)
|
||||
</I18nMessage>
|
||||
) : (
|
||||
'in USD'
|
||||
)}
|
||||
</React.Fragment>
|
||||
}
|
||||
error={tipError}
|
||||
|
@ -645,7 +405,7 @@ function WalletSendTip(props: Props) {
|
|||
step="any"
|
||||
type="number"
|
||||
style={{
|
||||
width: activeTab === TAB_FIAT ? '99px' : '160px',
|
||||
width: '160px',
|
||||
}}
|
||||
placeholder="1.23"
|
||||
value={customTipAmount}
|
||||
|
@ -661,24 +421,12 @@ function WalletSendTip(props: Props) {
|
|||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={
|
||||
fetchingChannels ||
|
||||
isPending ||
|
||||
tipError ||
|
||||
!tipAmount ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
}
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<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
|
||||
|
|
|
@ -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