tip modal no fiat

This commit is contained in:
zeppi 2021-10-19 13:57:24 -04:00 committed by jessopb
parent 73214a94ec
commit c66cfb28b5
17 changed files with 135 additions and 1602 deletions

View file

@ -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--"
} }

View file

@ -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}

View file

@ -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

View file

@ -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>
); );
}; };

View file

@ -1,3 +0,0 @@
import FiatAccountHistory from './view';
export default FiatAccountHistory;

View file

@ -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;

View file

@ -1,3 +0,0 @@
import WalletFiatBalance from './view';
export default WalletFiatBalance;

View file

@ -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;

View file

@ -1,3 +0,0 @@
import WalletFiatPaymentBalance from './view';
export default WalletFiatPaymentBalance;

View file

@ -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;

View file

@ -1,3 +0,0 @@
import WalletFiatPaymentHistory from './view';
export default WalletFiatPaymentHistory;

View file

@ -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;

View file

@ -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

View file

@ -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));

View file

@ -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&currency=fiat`}
/>
)}
</>
}
/>
<br />
</Page>
);
}
}
export default StripeAccountConnection;

View file

@ -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);

View file

@ -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&currency=fiat`}
style={{marginLeft: '10px'}}
/>
</>
}
/>
<br />
</div>
)}
</Page>
);
}
}
export default SettingsStripeCard;
/* eslint-enable no-undef */
/* eslint-enable react/prop-types */