From bec50829c14cf955853292512e0926b179ff1bfc Mon Sep 17 00:00:00 2001 From: zeppi Date: Sat, 3 Jul 2021 13:19:23 -0400 Subject: [PATCH] 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 --- .env.defaults | 2 + config.js | 1 + ui/component/cardVerify/view.jsx | 1 + ui/component/claimListDiscover/index.js | 9 + ui/component/router/view.jsx | 2 + ui/component/stripeAccountConnection/index.js | 13 + ui/component/stripeAccountConnection/view.jsx | 194 ++++++++ ui/component/walletBalance/view.jsx | 2 +- ui/component/walletSendTip/index.js | 4 + ui/component/walletSendTip/view.jsx | 328 ++++++++++++-- ui/constants/pages.js | 1 + ui/page/settings/view.jsx | 15 + ui/page/settingsStripeCard/index.js | 19 + ui/page/settingsStripeCard/view.jsx | 428 ++++++++++++++++++ ui/page/wallet/index.js | 1 + ui/page/wallet/view.jsx | 2 + ui/scss/all.scss | 3 + ui/scss/component/_stripe-card.scss | 331 ++++++++++++++ ui/scss/component/_wallet-tip-send.scss | 4 + 19 files changed, 1309 insertions(+), 51 deletions(-) create mode 100644 ui/component/stripeAccountConnection/index.js create mode 100644 ui/component/stripeAccountConnection/view.jsx create mode 100644 ui/page/settingsStripeCard/index.js create mode 100644 ui/page/settingsStripeCard/view.jsx create mode 100644 ui/scss/component/_stripe-card.scss create mode 100644 ui/scss/component/_wallet-tip-send.scss diff --git a/.env.defaults b/.env.defaults index e8584724a..4f2890b6e 100644 --- a/.env.defaults +++ b/.env.defaults @@ -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' diff --git a/config.js b/config.js index 0646db770..f96f290b6 100644 --- a/config.js +++ b/config.js @@ -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}`; diff --git a/ui/component/cardVerify/view.jsx b/ui/component/cardVerify/view.jsx index 904973481..3db81c13f 100644 --- a/ui/component/cardVerify/view.jsx +++ b/ui/component/cardVerify/view.jsx @@ -111,6 +111,7 @@ class CardVerify extends React.Component { CardVerify.stripeHandler = StripeCheckout.configure({ key: this.props.stripeKey, }); + if (this.hasPendingClick) { this.showStripeDialog(); } diff --git a/ui/component/claimListDiscover/index.js b/ui/component/claimListDiscover/index.js index db5d76259..ea9606252 100644 --- a/ui/component/claimListDiscover/index.js +++ b/ui/component/claimListDiscover/index.js @@ -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), diff --git a/ui/component/router/view.jsx b/ui/component/router/view.jsx index a1cbf2032..2f8689d73 100644 --- a/ui/component/router/view.jsx +++ b/ui/component/router/view.jsx @@ -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} /> + ({ + user: selectUser(state), +}); + +const perform = (dispatch) => ({}); + +export default withRouter(connect(select, perform)(StripeAccountConnection)); diff --git a/ui/component/stripeAccountConnection/view.jsx b/ui/component/stripeAccountConnection/view.jsx new file mode 100644 index 000000000..55f8bc3db --- /dev/null +++ b/ui/component/stripeAccountConnection/view.jsx @@ -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 { + 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 ( + {__(`Connect A Bank Account`)}} + isBodyList + body={ +
+ {/* show while waiting for account status */} + {!accountConfirmed && !accountPendingConfirmation && +
+
+
+

Getting your Bank Account Connection status...

+
+
+
+ } + {/* user has yet to complete their integration */} + {!accountConfirmed && accountPendingConfirmation && +
+
+
+

Connect your Bank Account to Odysee to receive donations directly from users

+
+ +
+
+ } + {/* user has completed their integration */} + {accountConfirmed && +
+
+
+

Congratulations! Your account has been connected with Odysee.

+ {unpaidBalance > 0 ?


+

Your account balance is ${unpaidBalance/100} USD. When the functionality exists you will be able to withdraw your balance.

+
:


+

Your account balance is $0 USD. When you receive a tip you will see it here.

+
} +
+ +
+
+ } +
+ } + /> + ); + } else { + return (<>); + } + } +} + +export default StripeAccountConnection; diff --git a/ui/component/walletBalance/view.jsx b/ui/component/walletBalance/view.jsx index 6746d5c36..de9676504 100644 --- a/ui/component/walletBalance/view.jsx +++ b/ui/component/walletBalance/view.jsx @@ -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. ) : ( - {__('Your total balance.')} + {__('Your total balance')} ) } actions={ diff --git a/ui/component/walletSendTip/index.js b/ui/component/walletSendTip/index.js index 50987b588..b09425fe4 100644 --- a/ui/component/walletSendTip/index.js +++ b/ui/component/walletSendTip/index.js @@ -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)); diff --git a/ui/component/walletSendTip/view.jsx b/ui/component/walletSendTip/view.jsx index 1d6db1f38..c2ade7a51 100644 --- a/ui/component/walletSendTip/view.jsx +++ b/ui/component/walletSendTip/view.jsx @@ -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 (
+ {/* if there is no LBC balance, show user frontend to get credits */} {noBalance ? ( }}>Supporting content requires %lbc%} @@ -155,41 +335,65 @@ function WalletSendTip(props: Props) { } /> ) : ( + // if there is lbc, the main tip/boost gui with the 3 tabs at the top } subtitle={ {!claimIsMine && (
+ {/* tip LBC tab button */}
)} + + {/* short explainer under the button */}
- {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.')}{' '} -
} actions={ + // confirmation modal, allow user to confirm or cancel transaction isConfirming ? ( <>
@@ -200,9 +404,9 @@ function WalletSendTip(props: Props) {
{activeChannelClaim && !incognito ? activeChannelClaim.name : __('Anonymous')}
-
{__(isSupport ? 'Boosting' : 'Tipping')}
+
{setConfirmLabel()}
- + {activeTab === TAB_FIAT ?

$ {tipAmount}

: }
@@ -223,34 +427,45 @@ function WalletSendTip(props: Props) { + {activeTab === TAB_FIAT && !hasCardSaved && ( +

+

+ )} + + {/* section to pick tip/boost amount */}
{DEFAULT_TIP_AMOUNTS.map((amount) => (
)} + {/* send tip/boost button */}
- + {activeTab !== TAB_FIAT ? ( + + ) : !canReceiveFiatTip ? ( +
Only select creators can receive tips at this time
+ ) : ( +
The payment will be made from your saved card
+ )} ) } diff --git a/ui/constants/pages.js b/ui/constants/pages.js index 9ffa85048..8b99b58db 100644 --- a/ui/constants/pages.js +++ b/ui/constants/pages.js @@ -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'; diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index cb7e86af7..11e207b51 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -205,6 +205,21 @@ class SettingsPage extends React.PureComponent { }} className="card-stack" > + + + + + + } + + {currentFlowStage === 'cardConfirmed' &&
+ +

Brand: {userCardDetails.brand.toUpperCase()}   + Last 4: {userCardDetails.lastFour}   + Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear}   +

+ } + /> +
+ + {(!customerTransactions || customerTransactions.length === 0) && } + + {customerTransactions && customerTransactions.length > 0 &&
+ + + + + + + + + + + {customerTransactions && + customerTransactions.map((transaction) => ( + + + + + + + ))} + +
{__('Date')}{<>{__('Receiving Channel Name')}}{__('Amount (USD)')} {__('Anonymous')}
{moment(transaction.created_at).format('LLL')}{transaction.channel_name}${transaction.tipped_amount / 100}{transaction.private_tip ? 'Yes' : 'No'}
+
} + />} +
} + + + ); + } +} + +export default CardVerify; +/* eslint-enable no-undef */ +/* eslint-enable react/prop-types */ diff --git a/ui/page/wallet/index.js b/ui/page/wallet/index.js index 2e51e48c3..2183ae1d2 100644 --- a/ui/page/wallet/index.js +++ b/ui/page/wallet/index.js @@ -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 => ({ diff --git a/ui/page/wallet/view.jsx b/ui/page/wallet/view.jsx index dc323c1fa..7664b02e8 100644 --- a/ui/page/wallet/view.jsx +++ b/ui/page/wallet/view.jsx @@ -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) => { ) : (
+
)} diff --git a/ui/scss/all.scss b/ui/scss/all.scss index 432822793..70e7aa195 100644 --- a/ui/scss/all.scss +++ b/ui/scss/all.scss @@ -65,3 +65,6 @@ @import 'component/wunderbar'; @import 'component/yrbl'; @import 'component/empty'; +@import 'component/stripe-card'; +@import 'component/wallet-tip-send'; + diff --git a/ui/scss/component/_stripe-card.scss b/ui/scss/component/_stripe-card.scss new file mode 100644 index 000000000..4eb5d4d2e --- /dev/null +++ b/ui/scss/component/_stripe-card.scss @@ -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% +//} + diff --git a/ui/scss/component/_wallet-tip-send.scss b/ui/scss/component/_wallet-tip-send.scss new file mode 100644 index 000000000..825aba026 --- /dev/null +++ b/ui/scss/component/_wallet-tip-send.scss @@ -0,0 +1,4 @@ +.add-card-prompt { + margin-top: -21px; + margin-bottom: -21px; +}