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; +}