updated code
about to test something generate programatically beginning of the frontend stripe integration page seems to be working add user put functionality behind conditional tag connect frontend working well adding environment variables to save success and failure url bugfix bugfix final clean up adding credit card page seems to be coming along calls successfully coming from the frontend fixing up frontend cleaning up frontend coming along client secret working basic frontend in place adding tip page adding more to the tip frontend frontend almost done tabs coming along one last thing to do for frontend adding explainer text as custom function putting finishing touches on tabs support tabs working well disable fiat toggle when card not connected fix frontend gui bug bugfix and pull out label function fix symbol for tip gui modal when card is not yet saved fix fiat disabled bug knowing whether card is added programatically sending tip with frontend tip functionality working show unpaid balance add frontend for card add section update frontend update frontend bugfix change to use react instead of css update how stripe is instantiated fix bug use customer setup coming along working but needs optimization persist if card is saved adding anonymous tip functionality fix nan bug build stripe endpoints programatically show for all users for time being allow the stripe key to automatically switch to live environment bugfix bugfix fix jslint fix channel page support button better docs show customer transactions on frontend basic table in place various page updates per jeremys notes showing card details nicer tip history table add better prompt to add card on file viewer page some linting time put connect account behind fiat enabled no persist fiat mode wallet calls tip stuff
This commit is contained in:
parent
10c04a7991
commit
bec50829c1
19 changed files with 1309 additions and 51 deletions
|
@ -63,3 +63,5 @@ AUTO_FOLLOW_CHANNELS=lbry://@lbry#3fda836a92faaceedfe398225fb9b2ee2ed1f01a
|
|||
# PINNED_URI_2=$/discover?t=lbrytvpaidbeta&fee_amount=>0&claim_type=stream&channel_ids=5af39f818f668d8c00943c9326c5201c4fe3c423,cda9c4e92f19d6fe0764524a2012056e06ca2055,760da3ba3dd85830a843beaaed543a89b7a367e7,40c36948f0da072dcba3e4833e90f71e16de78be,e8f68563d242f6ac9784dcbc41dd86c28a9391d6,7236fc5d2783ea7314d9076ae6c8a250e3992d1a,cf7792c2a37d0d76aaaff84aff0b99a8c791429d,8316ac90764fedf3147799b7b81a6575a9cc398e,8627af93c1a1219150f06b698f4b33e6ed2f1c1e,8972a1bd06de5186e5e89292b05aac8aaa817791,c5b0b17838df2f6c31162f64d55f60f34ae8bfc6,f576d5dba905fc179de880c3fe3eb3281ea74f59,97dd77c93c9603cbb2583f3589f7f5a6c92baa43,f399d873e0c37cf24de9569b5f22bbb30a5c6709,dba870d0620d41b2b9a152c961e0c06cf875ccfc,ca1fd651c9d14bf2e5088bb2aa0146ee7aeb2ae0,50ad846a4b1543b847bf3fdafb7b45f6b2f5844c,e09ff5abe9fb44dd0dd0576894a6db60a6211603,7b6f7517f6b816827d076fa0eaad550aa315a4e7,2068452c41d8da3bd68961335da0072a99258a1a,5da63df97c8255ae94a88940695b8471657dd5a1,3645cf2f5d0bdac0523f945be1c3ff60758f7845,4da85b12244839d6368b9290f1619ff9514ab2a8,4ad942982e43326c7700b1b6443049b3cfd82161,55304f219244abf82f684f759cc0c7769242f3b4,8f42e5b592bb7f7a03f4a94a86a41b1236bb099f,e2a014d885a48f5be2dc6409610996337312facb,c18996ca488753f714d36d4654715927c1d7f9c2,ebc4214424cfa683a7046e1f794fea1e44788d84,06b6d6d6a893fb589ec2ded948f5122856921ed5,07e4546674268fc0222b2ca22d31d0549dc217ee,060940e41973d4f7f16d72a2733138e931c35f41,f8d6eccd887c9cebd36b1d42aa349279b7f5c3ed,68098b8426f967b8d04cc566348b5c128823219e,2bfe6cdb24a21bdc1b76fb7c416edd50e9e85945,1f9bb08bfa2259629f4aaa9ed40f97e9a41b6fa1,2f20148495612946675fe1c8ea99171e4d950b81,bc6938fa1e09e840056c2e831abf9664f397c472,2a6194792beac5130641e932b5ac6e5a99b5ca4f,185ba2bd547a5e4a77d29fe6c1484f47db5e058f,29cc7f6081268eaa5b3f2946e0cd0b952a94812c,49389450b1241f5d8f4c8c4271a3eb56bba33965,ffdc62ac2f7549398d3aca9d2119e83d80d588d5,d7a4d2808074b0c55d6b239f69d90e7a4930f943,d58aa4a0b2f6c2504c3abce8de3f1afb71800acc,77ae23dc7eb8a75609881d4548a79e4935a89d37,f79bce8a60fbece671f6265adc39f6469f3b9b8c,051995fdf0af634e4911704057a551e9392e62b1
|
||||
# PINNED_LABEL_2=Paid Beta
|
||||
|
||||
# Stripe
|
||||
STRIPE_PUBLIC_KEY='pk_test_NoL1JWL7i1ipfhVId5KfDZgo'
|
||||
|
|
|
@ -49,6 +49,7 @@ const config = {
|
|||
PINNED_URI_2: process.env.PINNED_URI_2,
|
||||
PINNED_LABEL_2: process.env.PINNED_LABEL_2,
|
||||
KNOWN_APP_DOMAINS: process.env.KNOWN_APP_DOMAINS ? process.env.KNOWN_APP_DOMAINS.split(',') : [],
|
||||
STRIPE_PUBLIC_KEY: process.env.STRIPE_PUBLIC_KEY,
|
||||
};
|
||||
|
||||
config.URL_LOCAL = `http://localhost:${config.WEB_SERVER_PORT}`;
|
||||
|
|
|
@ -111,6 +111,7 @@ class CardVerify extends React.Component {
|
|||
CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
key: this.props.stripeKey,
|
||||
});
|
||||
|
||||
if (this.hasPendingClick) {
|
||||
this.showStripeDialog();
|
||||
}
|
||||
|
|
|
@ -12,6 +12,15 @@ import { doToggleTagFollowDesktop } from 'redux/actions/tags';
|
|||
import { makeSelectClientSetting, selectShowMatureContent, selectLanguage } from 'redux/selectors/settings';
|
||||
import { selectModerationBlockList } from 'redux/selectors/comments';
|
||||
import ClaimListDiscover from './view';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
const selectState = state => state.claims || {};
|
||||
|
||||
export const selectClaimsById = createSelector(
|
||||
selectState,
|
||||
state => state.byId || {}
|
||||
);
|
||||
|
||||
|
||||
const select = (state) => ({
|
||||
followedTags: selectFollowedTags(state),
|
||||
|
|
|
@ -66,6 +66,7 @@ const RewardsPage = React.lazy(() => import('page/rewards' /* webpackChunkName:
|
|||
const RewardsVerifyPage = React.lazy(() => import('page/rewardsVerify' /* webpackChunkName: "secondary" */));
|
||||
const SearchPage = React.lazy(() => import('page/search' /* webpackChunkName: "secondary" */));
|
||||
const SettingsAdvancedPage = React.lazy(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
|
||||
const SettingsStripeCard = React.lazy(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
|
||||
const SettingsCreatorPage = React.lazy(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
|
||||
const SettingsNotificationsPage = React.lazy(() =>
|
||||
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
|
||||
|
@ -288,6 +289,7 @@ 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}
|
||||
exact
|
||||
|
|
13
ui/component/stripeAccountConnection/index.js
Normal file
13
ui/component/stripeAccountConnection/index.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { withRouter } from 'react-router';
|
||||
import StripeAccountConnection from './view';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
|
||||
// function that receives state parameter and returns object of functions that accept state
|
||||
const select = (state) => ({
|
||||
user: selectUser(state),
|
||||
});
|
||||
|
||||
const perform = (dispatch) => ({});
|
||||
|
||||
export default withRouter(connect(select, perform)(StripeAccountConnection));
|
194
ui/component/stripeAccountConnection/view.jsx
Normal file
194
ui/component/stripeAccountConnection/view.jsx
Normal file
|
@ -0,0 +1,194 @@
|
|||
// @flow
|
||||
import * as ICONS from 'constants/icons';
|
||||
import React from 'react';
|
||||
import Button from 'component/button';
|
||||
import Card from 'component/common/card';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
|
||||
|
||||
const isDev = process.env.NODE_ENV !== 'production';
|
||||
|
||||
let stripeEnvironment = 'test';
|
||||
// if the key contains pk_live it's a live key
|
||||
// update the environment for the calls to the backend to indicate which environment to hit
|
||||
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||
stripeEnvironment = 'live';
|
||||
}
|
||||
|
||||
let successStripeRedirectUrl, failureStripeRedirectUrl;
|
||||
let successEndpoint = '/$/wallet';
|
||||
let failureEndpoint = '/$/wallet';
|
||||
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,
|
||||
user: User,
|
||||
};
|
||||
|
||||
type State = {
|
||||
error: boolean,
|
||||
loading: boolean,
|
||||
content: ?string,
|
||||
stripeConnectionUrl: string,
|
||||
alreadyUpdated: boolean,
|
||||
accountConfirmed: boolean,
|
||||
unpaidBalance: string
|
||||
};
|
||||
|
||||
class StripeAccountConnection extends React.Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: false,
|
||||
content: null,
|
||||
loading: true,
|
||||
accountConfirmed: false,
|
||||
accountPendingConfirmation: false,
|
||||
unpaidBalance: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { user } = this.props;
|
||||
|
||||
this.experimentalUiEnabled = user && user.experimental_ui;
|
||||
|
||||
var that = this;
|
||||
|
||||
function getAndSetAccountLink() {
|
||||
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;
|
||||
|
||||
that.setState({
|
||||
stripeConnectionUrl,
|
||||
accountPendingConfirmation: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// call the account status endpoint
|
||||
Lbryio.call('account', 'status', {
|
||||
environment: stripeEnvironment,
|
||||
}, 'post').then(accountStatusResponse => {
|
||||
// if charges already enabled, no need to generate an account link
|
||||
if (accountStatusResponse.charges_enabled) {
|
||||
// account has already been confirmed
|
||||
|
||||
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
|
||||
if (yetToBeCashedOutBalance) {
|
||||
that.setState({
|
||||
unpaidBalance: yetToBeCashedOutBalance,
|
||||
});
|
||||
}
|
||||
|
||||
console.log(accountStatusResponse.total_received_unpaid);
|
||||
|
||||
that.setState({
|
||||
accountConfirmed: true,
|
||||
});
|
||||
} else {
|
||||
// get stripe link and set it on the frontend
|
||||
getAndSetAccountLink();
|
||||
}
|
||||
}).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();
|
||||
} else {
|
||||
// not an error from Beamer, throw it
|
||||
throw new Error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { stripeConnectionUrl, accountConfirmed, accountPendingConfirmation, unpaidBalance } = this.state;
|
||||
|
||||
const { user } = this.props;
|
||||
|
||||
if (user.fiat_enabled) {
|
||||
return (
|
||||
<Card
|
||||
title={<div className="table__header-text">{__(`Connect A Bank Account`)}</div>}
|
||||
isBodyList
|
||||
body={
|
||||
<div>
|
||||
{/* show while waiting for account status */}
|
||||
{!accountConfirmed && !accountPendingConfirmation &&
|
||||
<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>
|
||||
{unpaidBalance > 0 ? <div><br></br>
|
||||
<h3>Your account balance is ${unpaidBalance/100} USD. When the functionality exists you will be able to withdraw your balance.</h3>
|
||||
</div> : <div><br></br>
|
||||
<h3>Your account balance is $0 USD. When you receive a tip you will see it here.</h3>
|
||||
</div>}
|
||||
</div>
|
||||
<div className="section__actions">
|
||||
<a href="/$/wallet">
|
||||
<Button
|
||||
button="secondary"
|
||||
label={__('View Your Transaction History')}
|
||||
icon={ICONS.SETTINGS}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (<></>);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StripeAccountConnection;
|
|
@ -71,7 +71,7 @@ const WalletBalance = (props: Props) => {
|
|||
Your total balance. All of this is yours, but some %lbc% is in use on channels and content right now.
|
||||
</I18nMessage>
|
||||
) : (
|
||||
<span>{__('Your total balance.')}</span>
|
||||
<span>{__('Your total balance')}</span>
|
||||
)
|
||||
}
|
||||
actions={
|
||||
|
|
|
@ -14,6 +14,8 @@ import { doOpenModal, doHideModal } from 'redux/actions/app';
|
|||
import { withRouter } from 'react-router';
|
||||
import { makeSelectClientSetting } from 'redux/selectors/settings';
|
||||
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
|
||||
import { doToast } from 'redux/actions/notifications';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
|
||||
const select = (state, props) => ({
|
||||
isPending: selectIsSendingSupport(state),
|
||||
|
@ -26,12 +28,14 @@ const select = (state, props) => ({
|
|||
fetchingChannels: selectFetchingMyChannels(state),
|
||||
activeChannelClaim: selectActiveChannelClaim(state),
|
||||
incognito: selectIncognito(state),
|
||||
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
|
||||
});
|
||||
|
||||
const perform = dispatch => ({
|
||||
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
sendSupport: (params, isSupport) => dispatch(doSendTip(params, isSupport)),
|
||||
doToast: (options) => dispatch(doToast(options)),
|
||||
});
|
||||
|
||||
export default withRouter(connect(select, perform)(WalletSendTip));
|
||||
|
|
|
@ -15,9 +15,20 @@ import LbcSymbol from 'component/common/lbc-symbol';
|
|||
import { parseURI } from 'lbry-redux';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||
|
||||
let stripeEnvironment = 'test';
|
||||
// if the key contains pk_live it's a live key
|
||||
// update the environment for the calls to the backend to indicate which environment to hit
|
||||
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||
stripeEnvironment = 'live';
|
||||
}
|
||||
|
||||
const DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
|
||||
|
||||
const TAB_BOOST = 'TabBoost';
|
||||
const TAB_FIAT = 'TabFiat';
|
||||
const TAB_LBC = 'TabLBC';
|
||||
type SupportParams = { amount: number, claim_id: string, channel_id?: string };
|
||||
|
||||
type Props = {
|
||||
|
@ -26,15 +37,17 @@ type Props = {
|
|||
title: string,
|
||||
claim: StreamClaim,
|
||||
isPending: boolean,
|
||||
sendSupport: (SupportParams, boolean) => void,
|
||||
isSupport: boolean,
|
||||
sendSupport: (SupportParams, boolean) => void, // function that comes from lbry-redux
|
||||
closeModal: () => void,
|
||||
balance: number,
|
||||
isSupport: boolean,
|
||||
fetchingChannels: boolean,
|
||||
instantTipEnabled: boolean,
|
||||
instantTipMax: { amount: number, currency: string },
|
||||
activeChannelClaim: ?ChannelClaim,
|
||||
incognito: boolean,
|
||||
doToast: ({ message: string }) => void,
|
||||
isAuthenticated: boolean,
|
||||
};
|
||||
|
||||
function WalletSendTip(props: Props) {
|
||||
|
@ -52,20 +65,107 @@ function WalletSendTip(props: Props) {
|
|||
fetchingChannels,
|
||||
incognito,
|
||||
activeChannelClaim,
|
||||
doToast,
|
||||
isAuthenticated,
|
||||
} = props;
|
||||
const [presetTipAmount, setPresetTipAmount] = usePersistedState('comment-support:presetTip', DEFAULT_TIP_AMOUNTS[0]);
|
||||
const [customTipAmount, setCustomTipAmount] = usePersistedState('comment-support:customTip', 1.0);
|
||||
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
|
||||
const [tipError, setTipError] = React.useState();
|
||||
const [sendAsTip, setSendAsTip] = usePersistedState('comment-support:sendAsTip', true);
|
||||
const [isConfirming, setIsConfirming] = React.useState(false);
|
||||
const { claim_id: claimId } = claim;
|
||||
const { channelName } = parseURI(uri);
|
||||
const noBalance = balance === 0;
|
||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||
const isSupport = claimIsMine || !sendAsTip;
|
||||
|
||||
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
|
||||
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
|
||||
|
||||
// setup variables for 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;
|
||||
|
||||
// TODO: come up with a better way to do this,
|
||||
// TODO: waiting 100ms to wait for token to populate
|
||||
|
||||
// check if creator has an account saved
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId && isAuthenticated) {
|
||||
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]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (channelClaimId) {
|
||||
Lbryio.call(
|
||||
'account',
|
||||
'check',
|
||||
{
|
||||
channel_claim_id: channelClaimId,
|
||||
channel_name: tipChannelName,
|
||||
environment: stripeEnvironment,
|
||||
},
|
||||
'post'
|
||||
)
|
||||
.then((accountCheckResponse) => {
|
||||
if (accountCheckResponse === true && canReceiveFiatTip !== true) {
|
||||
setCanReceiveFiatTip(true);
|
||||
}
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}, [channelClaimId]);
|
||||
|
||||
const noBalance = balance === 0;
|
||||
const tipAmount = useCustomTip ? customTipAmount : presetTipAmount;
|
||||
|
||||
const [activeTab, setActiveTab] = React.useState(TAB_LBC);
|
||||
|
||||
let iconToUse, explainerText;
|
||||
if (activeTab === TAB_BOOST) {
|
||||
iconToUse = ICONS.LBC;
|
||||
explainerText = 'This refundable boost will improve the discoverability of this content while active. ';
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
iconToUse = ICONS.FINANCE;
|
||||
explainerText = 'Show this channel your appreciation by sending a donation of cash 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. ';
|
||||
}
|
||||
|
||||
const isSupport = claimIsMine || activeTab === TAB_BOOST;
|
||||
|
||||
React.useEffect(() => {
|
||||
// Regex for number up to 8 decimal places
|
||||
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
|
||||
const validTipInput = regexp.test(String(tipAmount));
|
||||
let tipError;
|
||||
|
@ -83,34 +183,35 @@ function WalletSendTip(props: Props) {
|
|||
} else if (tipAmount > balance) {
|
||||
tipError = __('Not enough Credits');
|
||||
}
|
||||
|
||||
setTipError(tipError);
|
||||
}, [tipAmount, balance, setTipError]);
|
||||
|
||||
//
|
||||
function sendSupportOrConfirm(instantTipMaxAmount = null) {
|
||||
let selectedChannelId;
|
||||
if (!incognito && activeChannelClaim) {
|
||||
selectedChannelId = activeChannelClaim.claim_id;
|
||||
}
|
||||
|
||||
if (
|
||||
!isSupport &&
|
||||
!isConfirming &&
|
||||
(!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)
|
||||
) {
|
||||
// send a tip
|
||||
if (!isConfirming && (!instantTipMaxAmount || !instantTipEnabled || tipAmount > instantTipMaxAmount)) {
|
||||
setIsConfirming(true);
|
||||
} else {
|
||||
// send a boost
|
||||
const supportParams: SupportParams = { amount: tipAmount, claim_id: claimId };
|
||||
if (selectedChannelId) {
|
||||
supportParams.channel_id = selectedChannelId;
|
||||
|
||||
// include channel name if donation not anonymous
|
||||
if (activeChannelClaim && !incognito) {
|
||||
supportParams.channel_id = activeChannelClaim.claim_id;
|
||||
}
|
||||
|
||||
// send tip/boost
|
||||
sendSupport(supportParams, isSupport);
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
// when the form button is clicked
|
||||
function handleSubmit() {
|
||||
if (tipAmount && claimId) {
|
||||
if (instantTipEnabled) {
|
||||
// send an instant tip (no need to go to an exchange first)
|
||||
if (instantTipEnabled && activeTab !== TAB_FIAT) {
|
||||
if (instantTipMax.currency === 'LBC') {
|
||||
sendSupportOrConfirm(instantTipMax.amount);
|
||||
} else {
|
||||
|
@ -119,6 +220,44 @@ function WalletSendTip(props: Props) {
|
|||
sendSupportOrConfirm(instantTipMax.amount / LBC_USD);
|
||||
});
|
||||
}
|
||||
// sending fiat tip
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
if (!isConfirming) {
|
||||
setIsConfirming(true);
|
||||
} else if (isConfirming) {
|
||||
let sendAnonymously = !activeChannelClaim || incognito;
|
||||
|
||||
Lbryio.call(
|
||||
'customer',
|
||||
'tip',
|
||||
{
|
||||
amount: 100 * tipAmount, // convert from dollars to cents
|
||||
channel_name: tipChannelName,
|
||||
channel_claim_id: channelClaimId,
|
||||
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,
|
||||
}),
|
||||
});
|
||||
console.log(customerTipResponse);
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.log(error);
|
||||
doToast({ message: error.message, isError: true });
|
||||
});
|
||||
|
||||
closeModal();
|
||||
}
|
||||
// if it's a boost (?)
|
||||
} else {
|
||||
sendSupportOrConfirm();
|
||||
}
|
||||
|
@ -130,8 +269,49 @@ function WalletSendTip(props: Props) {
|
|||
setCustomTipAmount(tipAmount);
|
||||
}
|
||||
|
||||
function buildButtonText() {
|
||||
// test if frontend will show up as isNan
|
||||
function isNan(tipAmount) {
|
||||
// testing for NaN ES5 style https://stackoverflow.com/a/35912757/3973137
|
||||
// also sometimes it's returned as a string
|
||||
if (tipAmount !== tipAmount || tipAmount === 'NaN') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// if it's a valid number display it, otherwise do an empty string
|
||||
const displayAmount = !isNan(tipAmount) ? tipAmount : '';
|
||||
|
||||
if (activeTab === TAB_BOOST) {
|
||||
return 'Boost This Content';
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return 'Send a $' + displayAmount + ' Tip';
|
||||
} else if (activeTab === TAB_LBC) {
|
||||
return 'Send a ' + displayAmount + ' LBC Tip';
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDisableAmountSelector(amount) {
|
||||
return (
|
||||
(amount > balance && activeTab !== TAB_FIAT) ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
);
|
||||
}
|
||||
|
||||
function setConfirmLabel() {
|
||||
if (activeTab === TAB_LBC) {
|
||||
return 'Tipping LBC';
|
||||
} else if (activeTab === TAB_FIAT) {
|
||||
return 'Tipping Fiat (USD)';
|
||||
} else if (activeTab === TAB_BOOST) {
|
||||
return 'Boosting';
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
{/* if there is no LBC balance, show user frontend to get credits */}
|
||||
{noBalance ? (
|
||||
<Card
|
||||
title={<I18nMessage tokens={{ lbc: <LbcSymbol size={22} /> }}>Supporting content requires %lbc%</I18nMessage>}
|
||||
|
@ -155,41 +335,65 @@ function WalletSendTip(props: Props) {
|
|||
}
|
||||
/>
|
||||
) : (
|
||||
// if there is lbc, the main tip/boost gui with the 3 tabs at the top
|
||||
<Card
|
||||
title={<LbcSymbol postfix={claimIsMine ? __('Boost your content') : __('Support this content')} size={22} />}
|
||||
subtitle={
|
||||
<React.Fragment>
|
||||
{!claimIsMine && (
|
||||
<div className="section">
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="tip"
|
||||
icon={ICONS.SUPPORT}
|
||||
icon={ICONS.LBC}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => setSendAsTip(true)}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': sendAsTip })}
|
||||
onClick={() => {
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_LBC);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_LBC })}
|
||||
/>
|
||||
{/* tip fiat tab button */}
|
||||
<Button
|
||||
key="tip-fiat"
|
||||
icon={ICONS.FINANCE}
|
||||
label={__('Tip')}
|
||||
button="alt"
|
||||
onClick={() => {
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_FIAT);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_FIAT })}
|
||||
/>
|
||||
{/* tip LBC tab button */}
|
||||
<Button
|
||||
key="boost"
|
||||
icon={ICONS.TRENDING}
|
||||
label={__('Boost')}
|
||||
button="alt"
|
||||
onClick={() => setSendAsTip(false)}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': !sendAsTip })}
|
||||
onClick={() => {
|
||||
if (!isConfirming) {
|
||||
setActiveTab(TAB_BOOST);
|
||||
}
|
||||
}}
|
||||
className={classnames('button-toggle', { 'button-toggle--active': activeTab === TAB_BOOST })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* short explainer under the button */}
|
||||
<div className="section__subtitle">
|
||||
{isSupport
|
||||
? __(
|
||||
'This will increase the overall bid amount for this content, which will boost its ability to be discovered while active.'
|
||||
)
|
||||
: __('Show this channel your appreciation by sending a donation.')}{' '}
|
||||
<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />
|
||||
{explainerText}
|
||||
{/* {activeTab === TAB_FIAT && !hasCardSaved && <Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add A Card')} button="link" />} */}
|
||||
{<Button label={__('Learn more')} button="link" href="https://lbry.com/faq/tipping" />}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
actions={
|
||||
// confirmation modal, allow user to confirm or cancel transaction
|
||||
isConfirming ? (
|
||||
<>
|
||||
<div className="section section--padded card--inline confirm__wrapper">
|
||||
|
@ -200,9 +404,9 @@ function WalletSendTip(props: Props) {
|
|||
<div className="confirm__value">
|
||||
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
|
||||
</div>
|
||||
<div className="confirm__label">{__(isSupport ? 'Boosting' : 'Tipping')}</div>
|
||||
<div className="confirm__label">{setConfirmLabel()}</div>
|
||||
<div className="confirm__value">
|
||||
<LbcSymbol postfix={tipAmount} size={22} />
|
||||
{activeTab === TAB_FIAT ? <p>$ {tipAmount}</p> : <LbcSymbol postfix={tipAmount} size={22} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -223,34 +427,45 @@ function WalletSendTip(props: Props) {
|
|||
<ChannelSelector />
|
||||
</div>
|
||||
|
||||
{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) => (
|
||||
<Button
|
||||
key={amount}
|
||||
disabled={amount > balance}
|
||||
disabled={shouldDisableAmountSelector(amount)}
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': tipAmount === amount,
|
||||
'button-toggle--active': tipAmount === amount && !useCustomTip,
|
||||
'button-toggle--disabled': amount > balance,
|
||||
})}
|
||||
label={amount}
|
||||
icon={ICONS.LBC}
|
||||
icon={iconToUse}
|
||||
onClick={() => {
|
||||
setPresetTipAmount(amount);
|
||||
setUseCustomTip(false);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle button-toggle--expandformobile', {
|
||||
'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(tipAmount),
|
||||
'button-toggle--active': useCustomTip, // set as active
|
||||
})}
|
||||
icon={ICONS.LBC}
|
||||
icon={iconToUse}
|
||||
label={__('Custom')}
|
||||
onClick={() => setUseCustomTip(true)}
|
||||
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
|
||||
/>
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
|
||||
|
||||
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && activeTab !== TAB_FIAT && (
|
||||
<Button
|
||||
button="secondary"
|
||||
className="button-toggle-group-action"
|
||||
|
@ -269,15 +484,19 @@ 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>
|
||||
}
|
||||
className="form-field--price-amount"
|
||||
error={tipError}
|
||||
error={tipError && activeTab !== TAB_FIAT}
|
||||
min="0"
|
||||
step="any"
|
||||
type="number"
|
||||
|
@ -288,22 +507,31 @@ function WalletSendTip(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* send tip/boost button */}
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
autoFocus
|
||||
icon={isSupport ? ICONS.TRENDING : ICONS.SUPPORT}
|
||||
button="primary"
|
||||
type="submit"
|
||||
disabled={fetchingChannels || isPending || tipError || !tipAmount}
|
||||
label={
|
||||
isSupport
|
||||
? __('Boost This Content')
|
||||
: __('Send a %amount% Tip', { amount: tipAmount ? `${tipAmount} Credit` : '' })
|
||||
disabled={
|
||||
fetchingChannels ||
|
||||
isPending ||
|
||||
(tipError && activeTab !== TAB_FIAT) ||
|
||||
!tipAmount ||
|
||||
(activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
|
||||
}
|
||||
label={buildButtonText()}
|
||||
/>
|
||||
{fetchingChannels && <span className="help">{__('Loading your channels...')}</span>}
|
||||
</div>
|
||||
{activeTab !== TAB_FIAT ? (
|
||||
<WalletSpendableBalanceHelp />
|
||||
) : !canReceiveFiatTip ? (
|
||||
<div className="help">Only select creators can receive tips at this time</div>
|
||||
) : (
|
||||
<div className="help">The payment will be made from your saved card</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ exports.REWARDS_VERIFY = 'rewards/verify';
|
|||
exports.REPOST_NEW = 'repost';
|
||||
exports.SEND = 'send';
|
||||
exports.SETTINGS = 'settings';
|
||||
exports.SETTINGS_STRIPE_CARD = 'settings/card';
|
||||
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
|
||||
exports.SETTINGS_ADVANCED = 'settings/advanced';
|
||||
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';
|
||||
|
|
|
@ -205,6 +205,21 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
|||
}}
|
||||
className="card-stack"
|
||||
>
|
||||
|
||||
<Card
|
||||
title={__('Add card to tip creators in USD')}
|
||||
actions={
|
||||
<div className="section__actions">
|
||||
<Button
|
||||
button="secondary"
|
||||
label={__('Manage Card')}
|
||||
icon={ICONS.WALLET}
|
||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
|
||||
<Card title={__('Language')} actions={<SettingLanguage />} />
|
||||
{homepages && Object.keys(homepages).length > 1 && (
|
||||
<Card title={__('Homepage')} actions={<HomepageSelector />} />
|
||||
|
|
19
ui/page/settingsStripeCard/index.js
Normal file
19
ui/page/settingsStripeCard/index.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { doSetClientSetting } from 'redux/actions/settings';
|
||||
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
|
||||
import { selectUserVerifiedEmail } from 'redux/selectors/user';
|
||||
import { selectUserEmail } from 'redux/selectors/user';
|
||||
|
||||
import SettingsPage 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)),
|
||||
});
|
||||
|
||||
export default connect(select, perform)(SettingsPage);
|
428
ui/page/settingsStripeCard/view.jsx
Normal file
428
ui/page/settingsStripeCard/view.jsx
Normal file
|
@ -0,0 +1,428 @@
|
|||
// @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 { SETTINGS } from 'lbry-redux';
|
||||
import { Lbryio } from 'lbryinc';
|
||||
import { STRIPE_PUBLIC_KEY } from 'config';
|
||||
import classnames from 'classnames';
|
||||
import moment from 'moment';
|
||||
|
||||
let scriptLoading = false;
|
||||
let scriptLoaded = false;
|
||||
let scriptDidError = false;
|
||||
|
||||
const dateFormat = {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
};
|
||||
|
||||
let stripeEnvironment = 'test';
|
||||
// if the key contains pk_live it's a live key
|
||||
// update the environment for the calls to the backend to indicate which environment to hit
|
||||
if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
|
||||
stripeEnvironment = 'live';
|
||||
}
|
||||
|
||||
type Props = {
|
||||
disabled: boolean,
|
||||
label: ?string,
|
||||
email: ?string,
|
||||
}
|
||||
|
||||
class CardVerify 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: {},
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var that = this;
|
||||
|
||||
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
|
||||
var publicKey = STRIPE_PUBLIC_KEY;
|
||||
|
||||
// client secret of the SetupIntent (don't share with anyone but customer)
|
||||
var clientSecret = '';
|
||||
|
||||
// setting a timeout to let the client secret populate
|
||||
// TODO: fix this, should be a cleaner way
|
||||
setTimeout(function() {
|
||||
// check if customer has card setup already
|
||||
Lbryio.call('customer', 'status', {
|
||||
environment: stripeEnvironment,
|
||||
}, 'post').then(customerStatusResponse => {
|
||||
|
||||
// user has a card saved if their defaultPaymentMethod has an id
|
||||
const defaultPaymentMethod = customerStatusResponse.Customer.invoice_settings.default_payment_method;
|
||||
var userHasAlreadySetupPayment = Boolean(defaultPaymentMethod && defaultPaymentMethod.id);
|
||||
|
||||
// show different frontend if user already has card
|
||||
if (userHasAlreadySetupPayment) {
|
||||
|
||||
var card = customerStatusResponse.PaymentMethods[0].card;
|
||||
|
||||
var cardDetails = {
|
||||
brand: card.brand,
|
||||
expiryYear: card.exp_year,
|
||||
expiryMonth: card.exp_month,
|
||||
lastFour: card.last4
|
||||
};
|
||||
|
||||
that.setState({
|
||||
currentFlowStage: 'cardConfirmed',
|
||||
pageTitle: 'Tip History',
|
||||
userCardDetails: cardDetails,
|
||||
});
|
||||
|
||||
// get customer transactions
|
||||
Lbryio.call('customer', 'list', {
|
||||
environment: stripeEnvironment,
|
||||
}, 'post').then(customerTransactionsResponse => {
|
||||
that.setState({
|
||||
customerTransactions: customerTransactionsResponse,
|
||||
})
|
||||
|
||||
console.log(customerTransactionsResponse);
|
||||
});
|
||||
|
||||
// otherwise, prompt them to save a card
|
||||
} else {
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
|
||||
// get a payment method secret for frontend
|
||||
Lbryio.call('customer', 'setup', {
|
||||
environment: stripeEnvironment,
|
||||
}, 'post').then(customerSetupResponse => {
|
||||
console.log(customerSetupResponse);
|
||||
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// instantiate stripe elements
|
||||
setupStripe();
|
||||
});
|
||||
}
|
||||
// if the status call fails, either an actual error or need to run setup first
|
||||
}).catch(function(error) {
|
||||
console.log(error);
|
||||
|
||||
// errorString passed from the API (with a 403 error)
|
||||
const errorString = 'user as customer is not setup yet';
|
||||
|
||||
// if it's beamer's error indicating the account is not linked yet
|
||||
if (error.message.indexOf(errorString) > -1) {
|
||||
// send them to save a card
|
||||
that.setState({
|
||||
currentFlowStage: 'confirmingCard',
|
||||
});
|
||||
|
||||
// get a payment method secret for frontend
|
||||
Lbryio.call('customer', 'setup', {
|
||||
environment: stripeEnvironment,
|
||||
}, 'post').then(customerSetupResponse => {
|
||||
console.log(customerSetupResponse);
|
||||
|
||||
clientSecret = customerSetupResponse.client_secret;
|
||||
|
||||
// instantiate stripe elements
|
||||
setupStripe();
|
||||
});
|
||||
} else {
|
||||
console.log('Unseen before error');
|
||||
}
|
||||
});
|
||||
}, 250);
|
||||
|
||||
function setupStripe() {
|
||||
// TODO: have to fix this, using so that the script is available
|
||||
setTimeout(function() {
|
||||
var stripeElements = function(publicKey, setupIntent) {
|
||||
var stripe = Stripe(publicKey);
|
||||
var elements = stripe.elements();
|
||||
|
||||
// Element styles
|
||||
var style = {
|
||||
base: {
|
||||
fontSize: '16px',
|
||||
color: '#32325d',
|
||||
fontFamily:
|
||||
'-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif',
|
||||
fontSmoothing: 'antialiased',
|
||||
'::placeholder': {
|
||||
color: 'rgba(0,0,0,0.4)',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var card = elements.create('card', { style: style });
|
||||
|
||||
card.mount('#card-element');
|
||||
|
||||
// Element focus ring
|
||||
card.on('focus', function() {
|
||||
var el = document.getElementById('card-element');
|
||||
el.classList.add('focused');
|
||||
});
|
||||
|
||||
card.on('blur', function() {
|
||||
var el = document.getElementById('card-element');
|
||||
el.classList.remove('focused');
|
||||
});
|
||||
|
||||
card.on('ready', function() {
|
||||
card.focus();
|
||||
});
|
||||
|
||||
var email = that.props.email;
|
||||
|
||||
function submitForm(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// if client secret wasn't loaded properly
|
||||
if (!clientSecret) {
|
||||
var displayErrorText = 'There was an error in generating your payment method. Please contact a developer';
|
||||
var displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = displayErrorText;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
changeLoadingState(true);
|
||||
|
||||
stripe.confirmCardSetup(clientSecret, {
|
||||
payment_method: {
|
||||
card: card,
|
||||
billing_details: { email: email },
|
||||
},
|
||||
}).then(function(result) {
|
||||
if (result.error) {
|
||||
console.log(result);
|
||||
|
||||
changeLoadingState(false);
|
||||
var displayError = document.getElementById('card-errors');
|
||||
displayError.textContent = result.error.message;
|
||||
} else {
|
||||
// The PaymentMethod was successfully set up
|
||||
// hide and show the proper divs
|
||||
orderComplete(stripe, clientSecret);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle payment submission when user clicks the pay button.
|
||||
var button = document.getElementById('submit');
|
||||
button.addEventListener('click', function(event) {
|
||||
submitForm(event);
|
||||
});
|
||||
|
||||
// currently doesn't work because the iframe javascript context is different
|
||||
// would be nice though if it's even technically possible
|
||||
// window.addEventListener('keyup', function(event) {
|
||||
// if (event.keyCode === 13) {
|
||||
// submitForm(event);
|
||||
// }
|
||||
// }, false);
|
||||
};
|
||||
|
||||
// TODO: possible bug here where clientSecret isn't done
|
||||
stripeElements(publicKey, clientSecret);
|
||||
|
||||
// Show a spinner on payment submission
|
||||
var changeLoadingState = function(isLoading) {
|
||||
if (isLoading) {
|
||||
document.querySelector('button').disabled = true;
|
||||
document.querySelector('#spinner').classList.remove('hidden');
|
||||
document.querySelector('#button-text').classList.add('hidden');
|
||||
} else {
|
||||
document.querySelector('button').disabled = false;
|
||||
document.querySelector('#spinner').classList.add('hidden');
|
||||
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 => {
|
||||
var card = customerStatusResponse.PaymentMethods[0].card;
|
||||
|
||||
var cardDetails = {
|
||||
brand: card.brand,
|
||||
expiryYear: card.exp_year,
|
||||
expiryMonth: card.exp_month,
|
||||
lastFour: card.last4,
|
||||
};
|
||||
|
||||
that.setState({
|
||||
currentFlowStage: 'cardConfirmed',
|
||||
pageTitle: 'Tip History',
|
||||
userCardDetails: cardDetails,
|
||||
});
|
||||
});
|
||||
|
||||
console.log(result);
|
||||
|
||||
changeLoadingState(false);
|
||||
});
|
||||
};
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (!scriptLoading) {
|
||||
this.updateStripeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.loadPromise) {
|
||||
this.loadPromise.reject();
|
||||
}
|
||||
if (CardVerify.stripeHandler && this.state.open) {
|
||||
CardVerify.stripeHandler.close();
|
||||
}
|
||||
}
|
||||
|
||||
onScriptLoaded = () => {
|
||||
// if (!CardVerify.stripeHandler) {
|
||||
// CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
// key: 'pk_test_NoL1JWL7i1ipfhVId5KfDZgo',
|
||||
// });
|
||||
//
|
||||
// if (this.hasPendingClick) {
|
||||
// this.showStripeDialog();
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
||||
onScriptError = (...args) => {
|
||||
this.setState({ scriptFailedToLoad: true });
|
||||
};
|
||||
|
||||
onClosed = () => {
|
||||
this.setState({ open: false });
|
||||
};
|
||||
|
||||
updateStripeHandler() {
|
||||
// if (!CardVerify.stripeHandler) {
|
||||
// CardVerify.stripeHandler = StripeCheckout.configure({
|
||||
// key: this.props.stripeKey,
|
||||
// });
|
||||
// }
|
||||
}
|
||||
|
||||
render() {
|
||||
const { scriptFailedToLoad } = this.props;
|
||||
|
||||
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails } = this.state;
|
||||
|
||||
return (
|
||||
|
||||
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
|
||||
<div>
|
||||
{scriptFailedToLoad && (
|
||||
<div className="error__text">There was an error connecting to Stripe. Please try again later.</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{currentFlowStage === 'loading' && <div className="headerCard toConfirmCard">
|
||||
<Card
|
||||
title={__('Connect your card with Odysee')}
|
||||
subtitle={__('Getting your card connection status...')}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
{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="spinner hidden" id="spinner" />
|
||||
<span id="button-text">Add Card</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
{currentFlowStage === 'cardConfirmed' && <div className="successCard">
|
||||
<Card
|
||||
title={__('Card Details')}
|
||||
body={<>
|
||||
<h4 className="grey-text">Brand: {userCardDetails.brand.toUpperCase()}
|
||||
Last 4: {userCardDetails.lastFour}
|
||||
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear}
|
||||
</h4>
|
||||
</>}
|
||||
/>
|
||||
<br />
|
||||
|
||||
{(!customerTransactions || customerTransactions.length === 0) && <Card
|
||||
title={__('Tip History')}
|
||||
subtitle={__('You have not sent any tips yet. When you do they will appear here. ')}
|
||||
/>}
|
||||
|
||||
{customerTransactions && customerTransactions.length > 0 && <Card
|
||||
title={__('Tip History')}
|
||||
body={<><div className="table__wrapper">
|
||||
<table className="table table--transactions">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="date-header">{__('Date')}</th>
|
||||
<th>{<>{__('Receiving Channel Name')}</>}</th>
|
||||
<th>{__('Amount (USD)')} </th>
|
||||
<th>{__('Anonymous')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{customerTransactions &&
|
||||
customerTransactions.map((transaction) => (
|
||||
<tr key={transaction.name + transaction.created_at}>
|
||||
<td>{moment(transaction.created_at).format('LLL')}</td>
|
||||
<td>{transaction.channel_name}</td>
|
||||
<td>${transaction.tipped_amount / 100}</td>
|
||||
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div></>}
|
||||
/>}
|
||||
</div>}
|
||||
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default CardVerify;
|
||||
/* eslint-enable no-undef */
|
||||
/* eslint-enable react/prop-types */
|
|
@ -1,6 +1,7 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectTotalBalance } from 'lbry-redux';
|
||||
import { doOpenModal } from 'redux/actions/app';
|
||||
import { selectUser } from 'redux/selectors/user';
|
||||
import Wallet from './view';
|
||||
|
||||
const select = state => ({
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { withRouter } from 'react-router';
|
||||
import WalletBalance from 'component/walletBalance';
|
||||
import TxoList from 'component/txoList';
|
||||
import StripeAccountConnection from 'component/stripeAccountConnection';
|
||||
import Page from 'component/page';
|
||||
import Spinner from 'component/spinner';
|
||||
import YrblWalletEmpty from 'component/yrblWalletEmpty';
|
||||
|
@ -33,6 +34,7 @@ const WalletPage = (props: Props) => {
|
|||
) : (
|
||||
<div className="card-stack">
|
||||
<WalletBalance />
|
||||
<StripeAccountConnection />
|
||||
<TxoList search={search} />
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -65,3 +65,6 @@
|
|||
@import 'component/wunderbar';
|
||||
@import 'component/yrbl';
|
||||
@import 'component/empty';
|
||||
@import 'component/stripe-card';
|
||||
@import 'component/wallet-tip-send';
|
||||
|
||||
|
|
331
ui/scss/component/_stripe-card.scss
Normal file
331
ui/scss/component/_stripe-card.scss
Normal file
|
@ -0,0 +1,331 @@
|
|||
.date-header {
|
||||
width: 30%;
|
||||
}
|
||||
|
||||
.grey-text {
|
||||
color: rgb(156, 163, 175)
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.sr-root {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
max-width: 980px;
|
||||
padding: 48px;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
height: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.sr-picker {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.sr-picker button {
|
||||
background: none !important;
|
||||
border: none;
|
||||
padding: 0 !important;
|
||||
/*optional*/
|
||||
font-family: arial, sans-serif;
|
||||
/*input has OS specific font-family*/
|
||||
color: var(--accent-color);
|
||||
cursor: pointer;
|
||||
width: 75px;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sr-picker button:hover,
|
||||
.sr-picker button:focus,
|
||||
.sr-picker button.selected {
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
.sr-picker button:hover,
|
||||
.sr-picker button:focus {
|
||||
border-bottom: 2px solid var(--secondary-color);
|
||||
}
|
||||
.sr-picker button.selected {
|
||||
border-bottom: 2px solid var(--accent-color);
|
||||
}
|
||||
.sr-field-error {
|
||||
color: var(--font-color);
|
||||
text-align: left;
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
margin-top: 12px;
|
||||
max-width: 269px;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
.sr-input {
|
||||
border: 1px solid var(--gray-border);
|
||||
border-radius: var(--radius);
|
||||
padding: 5px 12px;
|
||||
height: 44px;
|
||||
width: 100%;
|
||||
transition: box-shadow 0.2s ease;
|
||||
background: white;
|
||||
-moz-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width:284px;
|
||||
}
|
||||
.sr-input:focus {
|
||||
box-shadow: 0 0 0 1px rgba(50, 151, 211, 0.3), 0 1px 1px 0 rgba(0, 0, 0, 0.07),
|
||||
0 0 0 4px rgba(50, 151, 211, 0.3);
|
||||
outline: none;
|
||||
z-index: 9;
|
||||
}
|
||||
.sr-input::placeholder {
|
||||
color: var(--gray-light);
|
||||
}
|
||||
.sr-result {
|
||||
height: 44px;
|
||||
-webkit-transition: height 1s ease;
|
||||
-moz-transition: height 1s ease;
|
||||
-o-transition: height 1s ease;
|
||||
transition: height 1s ease;
|
||||
color: var(--font-color);
|
||||
overflow: auto;
|
||||
}
|
||||
.sr-result code {
|
||||
overflow: scroll;
|
||||
}
|
||||
.sr-result.expand {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.sr-combo-inputs-row {
|
||||
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
|
||||
0px 2px 5px 0px rgba(50, 50, 93, 0.1), 0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.sr-form-row {
|
||||
margin: 16px 0;
|
||||
}
|
||||
//label {
|
||||
// font-size: 13px;
|
||||
// font-weight: 500;
|
||||
// margin-bottom: 8px;
|
||||
// display: inline-block;
|
||||
//}
|
||||
|
||||
/* Buttons and links */
|
||||
button.linkButton {
|
||||
background-color: rgb(30, 166, 114);
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
border: 0;
|
||||
padding: 12px 16px;
|
||||
margin-top: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: block;
|
||||
box-shadow: 0px 4px 5.5px 0px rgba(0, 0, 0, 0.07);
|
||||
width: 100%;
|
||||
max-width:280px;
|
||||
}
|
||||
button.linkButton:hover {
|
||||
filter: contrast(115%);
|
||||
}
|
||||
button.linkButton:active {
|
||||
transform: translateY(0px) scale(0.98);
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
button.linkButton:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: none;
|
||||
}
|
||||
|
||||
//a {
|
||||
// color: var(--link-color);
|
||||
// text-decoration: none;
|
||||
// transition: all 0.2s ease;
|
||||
//}
|
||||
//
|
||||
//a:hover {
|
||||
// filter: brightness(0.8);
|
||||
//}
|
||||
//
|
||||
//a:active {
|
||||
// filter: brightness(0.5);
|
||||
//}
|
||||
|
||||
/* Code block */
|
||||
code,
|
||||
pre {
|
||||
font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Stripe Element placeholder */
|
||||
.sr-card-element {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* Responsiveness */
|
||||
@media (max-width: 720px) {
|
||||
.sr-root {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
padding: 48px 20px;
|
||||
min-width: 320px;
|
||||
}
|
||||
|
||||
.sr-header__logo {
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.sr-payment-summary {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sr-payment-form {
|
||||
padding: 10px 34px;
|
||||
}
|
||||
|
||||
.sr-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sr-main {
|
||||
width: 100%;
|
||||
height: 305px;
|
||||
box-shadow: 0px 0px 0px 0.5px rgba(50, 50, 93, 0.1),
|
||||
0px 2px 5px 0px rgba(50, 50, 93, 0.1),
|
||||
0px 1px 1.5px 0px rgba(0, 0, 0, 0.07);
|
||||
border-radius: 6px;
|
||||
padding: 10px 20px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* todo: spinner/processing state, errors, animations */
|
||||
|
||||
.spinner,
|
||||
.spinner:before,
|
||||
.spinner:after {
|
||||
border-radius: 50%;
|
||||
}
|
||||
.spinner {
|
||||
color: #ffffff;
|
||||
font-size: 22px;
|
||||
text-indent: -99999px;
|
||||
margin: 0px auto;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
box-shadow: inset 0 0 0 2px;
|
||||
-webkit-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
.spinner:before,
|
||||
.spinner:after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
}
|
||||
.spinner:before {
|
||||
width: 10.4px;
|
||||
height: 20.4px;
|
||||
background: var(--accent-color);
|
||||
border-radius: 20.4px 0 0 20.4px;
|
||||
top: -0.2px;
|
||||
left: -0.2px;
|
||||
-webkit-transform-origin: 10.4px 10.2px;
|
||||
transform-origin: 10.4px 10.2px;
|
||||
-webkit-animation: loading 2s infinite ease 1.5s;
|
||||
animation: loading 2s infinite ease 1.5s;
|
||||
}
|
||||
.spinner:after {
|
||||
width: 10.4px;
|
||||
height: 10.2px;
|
||||
background: var(--accent-color);
|
||||
border-radius: 0 10.2px 10.2px 0;
|
||||
top: -0.1px;
|
||||
left: 10.2px;
|
||||
-webkit-transform-origin: 0px 10.2px;
|
||||
transform-origin: 0px 10.2px;
|
||||
-webkit-animation: loading 2s infinite ease;
|
||||
animation: loading 2s infinite ease;
|
||||
}
|
||||
|
||||
@-webkit-keyframes loading {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(360deg);
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Animated form */
|
||||
|
||||
.sr-root {
|
||||
animation: 0.4s form-in;
|
||||
animation-fill-mode: both;
|
||||
animation-timing-function: ease;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes field-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0px) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes form-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.sr-payment-form {
|
||||
padding: 10px 35px;
|
||||
padding-bottom:28px;
|
||||
}
|
||||
|
||||
.payment-details {
|
||||
font-size:16px;
|
||||
margin-bottom:4px;
|
||||
}
|
||||
|
||||
.sr-field-error {
|
||||
font-size:15px;
|
||||
}
|
||||
|
||||
//.successCard, .toConfirmCard {
|
||||
// max-width: 94%
|
||||
//}
|
||||
|
4
ui/scss/component/_wallet-tip-send.scss
Normal file
4
ui/scss/component/_wallet-tip-send.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
.add-card-prompt {
|
||||
margin-top: -21px;
|
||||
margin-bottom: -21px;
|
||||
}
|
Loading…
Reference in a new issue