show visible card and add remove card button

show your transactions even if you dont have a card

fix presentational issues

show your transactions even if you dont have a card

fix presentational issues

add link to channel section

update yarn

show donation location

add remove card modal still needs completion and also changed how stripe is used on settings stripe card page

add confirm remove card modal to router

move bank account stuff to settings page

move account functionality to settings page

continuing to move account transactions to settings

list transactions for creator

updating copy

touchup tip error

do a better job autofocusing

bugfix

show an error on the card page if api returns 500

building out frontend for comment tip

display dollar sign if its a fiat tip

more frontend work

more frontend work

more frontend bug fixes

working with hardcoded payment intent id

working but with one bug

bugfixed

add toast if payment fails

add add card button

cant get claim id but otherwise done

more frontend work

call is working

show fiat for livestream comments

add is fiat on comments

round and show values properly

dont allow review if tiperror

copy displaying properly

disable buttons conditionally properly

remove card button working

remove card working with a workaround by refreshing page

bugfix

send toast when tip on comment

jeremy frontend changes

only show cart on lbc
This commit is contained in:
Anthony 2021-07-06 22:28:29 +02:00 committed by jessopb
parent cd32fb71c7
commit 7bb5df97fd
27 changed files with 1112 additions and 209 deletions

View file

@ -56,6 +56,7 @@
"feed": "^4.2.2",
"if-env": "^1.0.4",
"react-datetime-picker": "^3.2.1",
"react-plastic": "^1.1.1",
"react-top-loading-bar": "^2.0.1",
"remove-markdown": "^0.3.0",
"source-map-explorer": "^2.5.2",

View file

@ -2026,7 +2026,7 @@
"Supporting content requires %lbc%": "Supporting content requires %lbc%",
"With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.": "With %lbc%, you can send tips to your favorite creators, or help boost their content for more people to see.",
"This refundable boost will improve the discoverability of this content while active.": "This refundable boost will improve the discoverability of this content while active.",
"Show this channel your appreciation by sending a donation of cash in USD.": "Show this channel your appreciation by sending a donation of cash in USD.",
"Show this channel your appreciation by sending a donation in USD.": "Show this channel your appreciation by sending a donation in USD.",
"Show this channel your appreciation by sending a donation of Credits.": "Show this channel your appreciation by sending a donation of Credits.",
"Add card to tip creators in USD": "Add card to tip creators in USD",
"Connect a bank account": "Connect a bank account",

View file

@ -59,6 +59,7 @@ type Props = {
stakedLevel: number,
supportAmount: number,
numDirectReplies: number,
isFiat: boolean
};
const LENGTH_TO_COLLAPSE = 300;
@ -91,6 +92,7 @@ function Comment(props: Props) {
stakedLevel,
supportAmount,
numDirectReplies,
isFiat,
} = props;
const {
@ -240,7 +242,7 @@ function Comment(props: Props) {
label={<DateTime date={timePosted} timeAgo />}
/>
{supportAmount > 0 && <CreditAmount amount={supportAmount} superChatLight size={12} />}
{supportAmount > 0 && <CreditAmount isFiat={isFiat} amount={supportAmount} superChatLight size={12} />}
{isPinned && (
<span className="comment__pin">

View file

@ -12,6 +12,7 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app';
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
import { CommentCreate } from './view';
import { doToast } from 'redux/actions/notifications';
const select = (state, props) => ({
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
@ -24,11 +25,12 @@ const select = (state, props) => ({
});
const perform = (dispatch, ownProps) => ({
createComment: (comment, claimId, parentId, txid) =>
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid)),
createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid, payment_intent_id, environment)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
doToast: (options) => dispatch(doToast(options)),
});
export default connect(select, perform)(CommentCreate);

View file

@ -16,11 +16,23 @@ import CreditAmount from 'component/common/credit-amount';
import ChannelThumbnail from 'component/channelThumbnail';
import UriIndicator from 'component/uriIndicator';
import Empty from 'component/common/empty';
import { STRIPE_PUBLIC_KEY } from 'config';
import { Lbryio } from 'lbryinc';
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 TAB_FIAT = 'TabFiat';
const TAB_LBC = 'TabLBC';
type Props = {
uri: string,
claim: StreamClaim,
createComment: (string, string, string, ?string) => Promise<any>,
createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
commentsDisabledBySettings: boolean,
channels: ?Array<ChannelClaim>,
onDoneReplying?: () => void,
@ -35,6 +47,8 @@ type Props = {
toast: (string) => void,
claimIsMine: boolean,
sendTip: ({}, (any) => void, (any) => void) => void,
doToast: ({ message: string }) => void,
disabled: boolean,
};
export function CommentCreate(props: Props) {
@ -53,8 +67,10 @@ export function CommentCreate(props: Props) {
livestream,
claimIsMine,
sendTip,
doToast,
} = props;
const buttonref: ElementRef<any> = React.useRef();
const {
push,
location: { pathname },
@ -72,6 +88,14 @@ export function CommentCreate(props: Props) {
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
const charCount = commentValue.length;
const [activeTab, setActiveTab] = React.useState('');
const [tipError, setTipError] = React.useState();
// React.useEffect(() => {
// setTipError('yes');
// }, []);
function handleCommentChange(event) {
let commentValue;
if (isReply) {
@ -123,26 +147,109 @@ export function CommentCreate(props: Props) {
channel_id: activeChannelClaim.claim_id,
};
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
console.log(activeChannelClaim);
setIsSubmitting(true);
if (activeTab === TAB_LBC) {
// call sendTip and then run the callback from the response
// second parameter is callback
sendTip(
params,
(response) => {
const { txid } = response;
// todo: why the setTimeout?
setTimeout(() => {
handleCreateComment(txid);
}, 1500);
setSuccessTip({ txid, tipAmount });
},
() => {
// reset the frontend so people can send a new comment
setIsSubmitting(false);
}
);
} else {
// 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;
}
function handleCreateComment(txid) {
const sourceClaimId = claim.claim_id;
var roundedAmount = Math.round(tipAmount * 100) / 100;
Lbryio.call(
'customer',
'tip',
{
amount: 100 * roundedAmount, // convert from dollars to cents
creator_channel_name: tipChannelName, // creator_channel_name
creator_channel_claim_id: channelClaimId,
tipper_channel_name: activeChannelName,
tipper_channel_claim_id: activeChannelId,
currency: 'USD',
anonymous: false,
source_claim_id: sourceClaimId,
environment: stripeEnvironment,
},
'post'
)
.then((customerTipResponse) => {
console.log(customerTipResponse);
const paymentIntendId = customerTipResponse.payment_intent_id;
handleCreateComment(null, paymentIntendId, stripeEnvironment);
setCommentValue('');
setIsReviewingSupportComment(false);
setIsSupportComment(false);
setCommentFailure(false);
setIsSubmitting(false);
doToast({
message: __("You sent $%formattedAmount% as a tip to %tipChannelName%, I'm sure they appreciate it!", {
formattedAmount: roundedAmount.toFixed(2), // force show decimal places
tipChannelName,
}),
});
// handleCreateComment(null);
})
.catch(function(error) {
var displayError = 'Sorry, there was an error in processing your payment!';
if (error.message !== 'payment intent failed to confirm') {
displayError = error.message;
}
doToast({ message: displayError, isError: true });
});
}
}
/**
*
* @param {string} [txid] Optional transaction id generated by
* @param {string} [payment_intent_id] Optional payment_intent_id from Stripe payment
* @param {string} [environment] Optional environment for Stripe (test|live)
*/
function handleCreateComment(txid, payment_intent_id, environment) {
setIsSubmitting(true);
createComment(commentValue, claimId, parentId, txid)
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
.then((res) => {
setIsSubmitting(false);
@ -157,7 +264,7 @@ export function CommentCreate(props: Props) {
}
}
})
.catch(() => {
.catch((e) => {
setIsSubmitting(false);
setCommentFailure(true);
});
@ -198,10 +305,11 @@ export function CommentCreate(props: Props) {
}
if (isReviewingSupportComment && activeChannelClaim) {
return (
<div className="comment__create">
<div className="comment__sc-preview">
<CreditAmount className="comment__scpreview-amount" amount={tipAmount} size={18} />
<CreditAmount className="comment__scpreview-amount" isFiat={activeTab === TAB_FIAT} amount={tipAmount} size={activeTab === TAB_LBC ? 18 : 2} />
<ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<div>
@ -262,15 +370,17 @@ export function CommentCreate(props: Props) {
autoFocus={isReply}
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT}
/>
{isSupportComment && <WalletTipAmountSelector amount={tipAmount} onChange={(amount) => setTipAmount(amount)} />}
{/* TODO: the tip validation is done in selector */}
{isSupportComment && <WalletTipAmountSelector onTipErrorChange={setTipError} claim={claim} activeTab={activeTab} amount={tipAmount} onChange={(amount) => setTipAmount(amount)} />}
<div className="section__actions section__actions--no-margin">
{isSupportComment ? (
<>
<Button
disabled={disabled}
// TODO: add better check here
disabled={disabled || tipError}
type="button"
button="primary"
icon={ICONS.LBC}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
label={__('Review')}
onClick={() => setIsReviewingSupportComment(true)}
/>
@ -296,7 +406,16 @@ export function CommentCreate(props: Props) {
requiresAuth={IS_WEB}
/>
{!claimIsMine && (
<Button disabled={disabled} button="alt" icon={ICONS.LBC} onClick={() => setIsSupportComment(true)} />
<Button disabled={disabled} button="alt" className="thatButton" icon={ICONS.LBC} onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_LBC);
}} />
)}
{!claimIsMine && (
<Button disabled={disabled} button="alt" className="thisButton" icon={ICONS.FINANCE} onClick={() => {
setIsSupportComment(true);
setActiveTab(TAB_FIAT);
}} />
)}
{isReply && (
<Button

View file

@ -307,6 +307,7 @@ function CommentList(props: Props) {
isPinned={comment.is_pinned}
supportAmount={comment.support_amount}
numDirectReplies={comment.replies}
isFiat={comment.is_fiat}
/>
);
})}

View file

@ -18,6 +18,7 @@ type Props = {
size?: number,
superChat?: boolean,
superChatLight?: boolean,
isFiat?: boolean
};
class CreditAmount extends React.PureComponent<Props> {
@ -45,6 +46,7 @@ class CreditAmount extends React.PureComponent<Props> {
size,
superChat,
superChatLight,
isFiat,
} = this.props;
const minimumRenderableAmount = 10 ** (-1 * precision);
const fullPrice = formatFullPrice(amount, 2);
@ -70,8 +72,10 @@ class CreditAmount extends React.PureComponent<Props> {
amountText = `+${amountText}`;
}
if (showLBC) {
if (showLBC && !isFiat) {
amountText = <LbcSymbol postfix={amountText} size={size} />;
} else if (showLBC && isFiat) {
amountText = <p> ${(Math.round(amountText * 100) / 100).toFixed(2)}</p>;
}
if (fee) {

View file

@ -20,10 +20,11 @@ type Props = {
commentIsMine: boolean,
stakedLevel: number,
supportAmount: number,
isFiat: boolean,
};
function LivestreamComment(props: Props) {
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount } = props;
const { claim, uri, authorUri, message, commentIsMine, commentId, stakedLevel, supportAmount, isFiat } = props;
const [mouseIsHovering, setMouseHover] = React.useState(false);
const commentByOwnerOfContent = claim && claim.signing_channel && claim.signing_channel.permanent_url === authorUri;
const { claimName } = parseURI(authorUri);
@ -39,7 +40,7 @@ function LivestreamComment(props: Props) {
{supportAmount > 0 && (
<div className="super-chat livestream-superchat__banner">
<div className="livestream-superchat__banner-corner" />
<CreditAmount amount={supportAmount} superChat className="livestream-superchat__amount" />
<CreditAmount isFiat={isFiat} amount={supportAmount} superChat className="livestream-superchat__amount" />
</div>
)}

View file

@ -46,6 +46,7 @@ export default function LivestreamComments(props: Props) {
superChatsTotalAmount,
myChannels,
} = props;
const commentsRef = React.createRef();
const [scrollBottom, setScrollBottom] = React.useState(true);
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
@ -174,6 +175,7 @@ export default function LivestreamComments(props: Props) {
size={10}
className="livestream-superchat__amount-large"
amount={superChat.support_amount}
isFiat={superChat.is_fiat}
/>
</div>
</div>
@ -193,6 +195,7 @@ export default function LivestreamComments(props: Props) {
commentId={comment.comment_id}
message={comment.comment}
supportAmount={comment.support_amount}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)}
/>
))}

View file

@ -69,6 +69,7 @@ const RewardsVerifyPage = lazyImport(() => import('page/rewardsVerify' /* webpac
const SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* webpackChunkName: "secondary" */));
const SettingsStripeAccount = lazyImport(() => import('page/settingsStripeAccount' /* webpackChunkName: "secondary" */));
const SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
const SettingsNotificationsPage = lazyImport(() =>
import('page/settingsNotifications' /* webpackChunkName: "secondary" */)
@ -292,6 +293,7 @@ function AppRouter(props: Props) {
/>
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} />
<PrivateRoute
{...props}
exact

View file

@ -96,10 +96,7 @@ function WalletSendTip(props: Props) {
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
// check if creator has a payment method saved
React.useEffect(() => {
if (channelClaimId && isAuthenticated) {
Lbryio.call(
@ -121,6 +118,12 @@ function WalletSendTip(props: Props) {
}
}, [channelClaimId, isAuthenticated]);
// check if creator has an account saved
React.useEffect(() => {
var tipInputElement = document.getElementById('tip-input');
if (tipInputElement) { tipInputElement.focus() }
}, []);
React.useEffect(() => {
if (channelClaimId) {
Lbryio.call(
@ -139,7 +142,7 @@ function WalletSendTip(props: Props) {
}
})
.catch(function (error) {
console.log(error);
// console.log(error);
});
}
}, [channelClaimId]);
@ -170,7 +173,7 @@ function WalletSendTip(props: Props) {
explainerText = __('This refundable boost will improve the discoverability of this %claimTypeText% while active.', {claimTypeText});
} else if (activeTab === TAB_FIAT) {
iconToUse = ICONS.FINANCE;
explainerText = __('Show this channel your appreciation by sending a donation of cash in USD.');
explainerText = __('Show this channel your appreciation by sending a donation in USD. ');
// if (!hasCardSaved) {
// explainerText += __('You must add a card to use this functionality.');
// }
@ -187,22 +190,34 @@ function WalletSendTip(props: Props) {
const validTipInput = regexp.test(String(tipAmount));
let tipError;
if (!tipAmount) {
tipError = __('Amount must be a number');
} else if (tipAmount <= 0) {
if (tipAmount === 0) {
tipError = __('Amount must be a positive number');
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
} else if (!validTipInput) {
} else if (!tipAmount || typeof tipAmount !== 'number') {
tipError = __('Amount must be a number');
}
// if it's not fiat, aka it's boost or lbc tip
else if (activeTab !== TAB_FIAT) {
if (!validTipInput) {
tipError = __('Amount must have no more than 8 decimal places');
} else if (tipAmount === balance) {
tipError = __('Please decrease the amount to account for transaction fees');
} else if (tipAmount > balance) {
tipError = __('Not enough Credits');
} else if (tipAmount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
}
// if tip fiat tab
} else {
if (tipAmount < 1) {
tipError = __('Amount must be at least one dollar');
} else if (tipAmount > 1000) {
tipError = __('Amount cannot be over 1000 dollars');
}
}
setTipError(tipError);
}, [tipAmount, balance, setTipError]);
}, [tipAmount, balance, setTipError, activeTab]);
//
function sendSupportOrConfirm(instantTipMaxAmount = null) {
@ -267,11 +282,15 @@ function WalletSendTip(props: Props) {
tipChannelName,
}),
});
console.log(customerTipResponse);
})
.catch(function(error) {
console.log(error);
doToast({ message: error.message, isError: true });
var displayError = 'Sorry, there was an error in processing your payment!';
if (error.message !== 'payment intent failed to confirm') {
displayError = error.message;
}
doToast({ message: displayError, isError: true });
});
closeModal();
@ -285,6 +304,7 @@ function WalletSendTip(props: Props) {
function handleCustomPriceChange(event: SyntheticInputEvent<*>) {
const tipAmount = parseFloat(event.target.value);
setCustomTipAmount(tipAmount);
}
@ -368,6 +388,8 @@ function WalletSendTip(props: Props) {
label={__('Tip')}
button="alt"
onClick={() => {
var tipInputElement = document.getElementById('tip-input');
if (tipInputElement) { tipInputElement.focus() }
if (!isConfirming) {
setActiveTab(TAB_LBC);
}
@ -382,6 +404,8 @@ function WalletSendTip(props: Props) {
label={__('Tip')}
button="alt"
onClick={() => {
var tipInputElement = document.getElementById('tip-input');
if (tipInputElement) { tipInputElement.focus() }
if (!isConfirming) {
setActiveTab(TAB_FIAT);
}
@ -396,6 +420,8 @@ function WalletSendTip(props: Props) {
label={__('Boost')}
button="alt"
onClick={() => {
var tipInputElement = document.getElementById('tip-input');
if (tipInputElement) { tipInputElement.focus() }
if (!isConfirming) {
setActiveTab(TAB_BOOST);
}
@ -483,6 +509,7 @@ function WalletSendTip(props: Props) {
icon={iconToUse}
label={__('Custom')}
onClick={() => setUseCustomTip(true)}
// disabled if it's receive fiat and there is no card or creator can't receive tips
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
/>

View file

@ -1,9 +1,13 @@
import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux';
import { makeSelectClaimForUri, selectBalance } from 'lbry-redux';
import WalletTipAmountSelector from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
const select = (state, props) => ({
balance: selectBalance(state),
isAuthenticated: Boolean(selectUserVerifiedEmail(state)),
// claim: makeSelectClaimForUri(props.uri)(state),
// claim: makeSelectClaimForUri(props.uri, false)(state),
});
export default connect(select)(WalletTipAmountSelector);

View file

@ -10,40 +10,145 @@ import I18nMessage from 'component/i18nMessage';
import classnames from 'classnames';
import usePersistedState from 'effects/use-persisted-state';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
import { Lbryio } from 'lbryinc';
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_FIAT = 'TabFiat';
const TAB_LBC = 'TabLBC';
type Props = {
balance: number,
amount: number,
onChange: (number) => void,
isAuthenticated: boolean,
claim: StreamClaim,
uri: string,
onTipErrorChange: (string) => void,
};
function WalletTipAmountSelector(props: Props) {
const { balance, amount, onChange } = props;
const { balance, amount, onChange, activeTab, isAuthenticated, claim, uri, onTipErrorChange } = props;
const [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
const [tipError, setTipError] = React.useState();
const [canReceiveFiatTip, setCanReceiveFiatTip] = React.useState(); // dont persist because it needs to be calc'd per creator
const [hasCardSaved, setHasSavedCard] = usePersistedState('comment-support:hasCardSaved', false);
function shouldDisableAmountSelector(amount) {
return (
(amount > balance && activeTab !== TAB_FIAT) || (activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip))
);
}
console.log(activeTab);
console.log(claim);
// 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;
}
// check if creator has a payment method saved
React.useEffect(() => {
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;
console.log('here');
console.log(defaultPaymentMethodId);
setHasSavedCard(Boolean(defaultPaymentMethodId));
});
}, []);
//
React.useEffect(() => {
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);
});
}, []);
React.useEffect(() => {
// setHasSavedCard(false);
// setCanReceiveFiatTip(true);
const regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
const validTipInput = regexp.test(String(amount));
let tipError;
if (!amount) {
tipError = __('Amount must be a number');
} else if (amount <= 0) {
if (amount === 0) {
tipError = __('Amount must be a positive number');
} else if (amount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
} else if (!validTipInput) {
} else if (!amount || typeof amount !== 'number') {
tipError = __('Amount must be a number');
}
// if it's not fiat, aka it's boost or lbc tip
else if (activeTab !== TAB_FIAT) {
if (!validTipInput) {
tipError = __('Amount must have no more than 8 decimal places');
} else if (amount === balance) {
tipError = __('Please decrease the amount to account for transaction fees');
} else if (amount > balance) {
tipError = __('Not enough Credits');
} else if (amount < MINIMUM_PUBLISH_BID) {
tipError = __('Amount must be higher');
}
// if tip fiat tab
} else {
if (amount < 1) {
tipError = __('Amount must be at least one dollar');
} else if (amount > 1000) {
tipError = __('Amount cannot be over 1000 dollars');
}
}
setTipError(tipError);
}, [amount, balance, setTipError]);
onTipErrorChange(tipError);
}, [amount, balance, setTipError, activeTab]);
function handleCustomPriceChange(amount: number) {
const tipAmount = parseFloat(amount);
@ -56,14 +161,14 @@ function WalletTipAmountSelector(props: Props) {
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
<Button
key={defaultAmount}
disabled={amount > balance}
disabled={shouldDisableAmountSelector(defaultAmount)}
button="alt"
className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--active': defaultAmount === amount,
'button-toggle--active': defaultAmount === amount && !useCustomTip,
'button-toggle--disabled': amount > balance,
})}
label={defaultAmount}
icon={ICONS.LBC}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
onClick={() => {
handleCustomPriceChange(defaultAmount);
setUseCustomTip(false);
@ -72,14 +177,15 @@ function WalletTipAmountSelector(props: Props) {
))}
<Button
button="alt"
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--active': !DEFAULT_TIP_AMOUNTS.includes(amount),
'button-toggle--active': useCustomTip,
})}
icon={ICONS.LBC}
icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
label={__('Custom')}
onClick={() => setUseCustomTip(true)}
/>
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
{activeTab === TAB_LBC && DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
<Button
button="secondary"
className="button-toggle-group-action"
@ -90,18 +196,55 @@ function WalletTipAmountSelector(props: Props) {
)}
</div>
{useCustomTip && activeTab === TAB_FIAT && !hasCardSaved &&
<>
<div className="help">
<span className="help--spendable">
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To {__(' Tip Creators')}
</span>
</div>
</>
}
{/* has card saved but cant creator cant receive tips */}
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip &&
<>
<div className="help">
<span className="help--spendable">Only select creators can receive tips at this time</span>
</div>
</>
}
{/* has card saved but cant creator cant receive tips */}
{useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip &&
<>
<div className="help">
<span className="help--spendable">Send a tip directly from your attached card</span>
</div>
</>
}
{useCustomTip && (
<div className="comment__tip-input">
<FormField
autoFocus
name="tip-input"
label={
label={ activeTab === TAB_LBC ?
<React.Fragment>
{__('Custom support amount')}{' '}
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
(%lbc_balance% available)
</I18nMessage>
</React.Fragment>
// TODO: add conditional based on hasSavedCard
: <>
</>
// <>
// <div className="">
// <span className="help--spendable">Send a tip directly from your attached card</span>
// </div>
// </>
}
className="form-field--price-amount"
error={tipError}
@ -115,7 +258,38 @@ function WalletTipAmountSelector(props: Props) {
</div>
)}
{!useCustomTip && <WalletSpendableBalanceHelp />}
{/*// TODO: add conditional based on hasSavedCard*/}
{/* lbc tab */}
{activeTab === TAB_LBC && <WalletSpendableBalanceHelp />}
{/* fiat button but no card saved */}
{!useCustomTip && activeTab === TAB_FIAT && !hasCardSaved &&
<>
<div className="help">
<span className="help--spendable">
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card ')} button="link" /> To {__(' Tip Creators')}
</span>
</div>
</>
}
{/* has card saved but cant creator cant receive tips */}
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && !canReceiveFiatTip &&
<>
<div className="help">
<span className="help--spendable">Only select creators can receive tips at this time</span>
</div>
</>
}
{/* has card saved but cant creator cant receive tips */}
{!useCustomTip && activeTab === TAB_FIAT && hasCardSaved && canReceiveFiatTip &&
<>
<div className="help">
<span className="help--spendable">Send a tip directly from your attached card</span>
</div>
</>
}
</>
);
}

View file

@ -45,3 +45,4 @@ export const VIEW_IMAGE = 'view_image';
export const CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete';
export const CONFIRM_REMOVE_CARD = 'CONFIRM_REMOVE_CARD';

View file

@ -39,6 +39,7 @@ exports.REPOST_NEW = 'repost';
exports.SEND = 'send';
exports.SETTINGS = 'settings';
exports.SETTINGS_STRIPE_CARD = 'settings/card';
exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute';

View file

@ -0,0 +1,19 @@
import { connect } from 'react-redux';
import { doHideModal } from 'redux/actions/app';
import { doAbandonTxo, doAbandonClaim, selectTransactionItems, doResolveUri } from 'lbry-redux';
import { doToast } from 'redux/actions/notifications';
import ModalRevokeClaim from './view';
const select = state => ({
transactionItems: selectTransactionItems(state),
});
const perform = dispatch => ({
toast: (message, isError) => dispatch(doToast({ message, isError })),
closeModal: () => dispatch(doHideModal()),
abandonTxo: (txo, cb) => dispatch(doAbandonTxo(txo, cb)),
abandonClaim: (txid, nout, cb) => dispatch(doAbandonClaim(txid, nout, cb)),
doResolveUri: (uri) => dispatch(doResolveUri(uri)),
});
export default connect(select, perform)(ModalRevokeClaim);

View file

@ -0,0 +1,92 @@
// @flow
import React, { useState } from 'react';
import { Modal } from 'modal/modal';
import { FormField } from 'component/common/form';
import * as txnTypes from 'constants/transaction_types';
import Card from 'component/common/card';
import Button from 'component/button';
import I18nMessage from 'component/i18nMessage';
import LbcSymbol from 'component/common/lbc-symbol';
import * as ICONS from 'constants/icons';
import { Lbryio } from 'lbryinc';
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';
}
type Props = {
closeModal: () => void,
abandonTxo: (Txo, () => void) => void,
abandonClaim: (string, number, ?() => void) => void,
tx: Txo,
claim: GenericClaim,
cb: () => void,
doResolveUri: (string) => void,
uri: string,
paymentMethodId: string,
setAsConfirmingCard: () => void,
};
export default function ModalRevokeClaim(props: Props) {
var that = this;
console.log(that);
console.log(props);
const { closeModal, uri, paymentMethodId, setAsConfirmingCard } = props;
console.log(uri);
console.log(setAsConfirmingCard)
function removeCard(){
console.log(paymentMethodId);
Lbryio.call(
'customer',
'detach',
{
environment: stripeEnvironment,
payment_method_id: paymentMethodId
},
'post'
).then((removeCardResponse) => {
console.log(removeCardResponse)
//TODO: add toast here
// closeModal();
location.reload();
});
}
return (
<Modal ariaHideApp={false} isOpen contentLabel={'hello'} type="card" onAborted={closeModal}>
<Card
title={'Confirm Remove Card'}
// body={getMsgBody(type, isSupport, name)}
actions={
<div className="section__actions">
<Button
className="stripe__confirm-remove-card"
button="secondary"
icon={ICONS.DELETE}
label={'Remove Card'}
onClick={removeCard}
/>
<Button button="link" label={__('Cancel')} onClick={closeModal} />
</div>
}
/>
</Modal>
);
}

View file

@ -29,6 +29,7 @@ const ModalPhoneCollection = lazyImport(() => import('modal/modalPhoneCollection
const ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */));
const ModalPublishPreview = lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */));
const ModalRemoveBtcSwapAddress = lazyImport(() => import('modal/modalRemoveBtcSwapAddress' /* webpackChunkName: "modalRemoveBtcSwapAddress" */));
const ModalRemoveCard = lazyImport(() => import('modal/modalRemoveCard' /* webpackChunkName: "modalRemoveCard" */));
const ModalRemoveFile = lazyImport(() => import('modal/modalRemoveFile' /* webpackChunkName: "modalRemoveFile" */));
const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* webpackChunkName: "modalRevokeClaim" */));
const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */));
@ -151,6 +152,8 @@ function ModalRouter(props: Props) {
return ModalClaimCollectionAdd;
case MODALS.COLLECTION_DELETE:
return ModalDeleteCollection;
case MODALS.CONFIRM_REMOVE_CARD:
return ModalRemoveCard;
default:
return null;
}

View file

@ -18,7 +18,7 @@ import {
} from 'redux/selectors/settings';
import { doWalletStatus, selectMyChannelUrls, selectWalletIsEncrypted, SETTINGS } from 'lbry-redux';
import SettingsPage from './view';
import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectUserVerifiedEmail, selectUser } from 'redux/selectors/user';
const select = (state) => ({
daemonSettings: selectDaemonSettings(state),
@ -38,6 +38,7 @@ const select = (state) => ({
darkModeTimes: makeSelectClientSetting(SETTINGS.DARK_MODE_TIMES)(state),
language: selectLanguage(state),
myChannelUrls: selectMyChannelUrls(state),
user: selectUser(state),
});
const perform = (dispatch) => ({

View file

@ -72,6 +72,7 @@ type Props = {
enterSettings: () => void,
exitSettings: () => void,
myChannelUrls: ?Array<string>,
user: User,
};
type State = {
@ -189,6 +190,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
clearCache,
openModal,
myChannelUrls,
user,
} = this.props;
const { storedPassword } = this.state;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -206,14 +208,32 @@ class SettingsPage extends React.PureComponent<Props, State> {
className="card-stack"
>
{/* @if TARGET='web' */}
<Card
title={__('Add card to tip creators in USD')}
{user && user.fiat_enabled && <Card
title={__('Bank Accounts')}
subtitle={__('Connect a bank account to receive tips and compensation in your local currency')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage Card')}
icon={ICONS.WALLET}
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`}
/>
</div>
}
/>}
{/* @endif */}
{/* @if TARGET='web' */}
<Card
title={__('Payment Methods')}
subtitle={__('Add a credit card to tip creators in their local currency')}
actions={
<div className="section__actions">
<Button
button="secondary"
label={__('Manage')}
icon={ICONS.SETTINGS}
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
/>
</div>

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

View file

@ -0,0 +1,344 @@
// @flow
import * as ICONS from 'constants/icons';
import React from 'react';
import Button from 'component/button';
import Card from 'component/common/card';
import Page from 'component/page';
import { Lbryio } from 'lbryinc';
import { URL, WEBPACK_WEB_PORT, STRIPE_PUBLIC_KEY } from 'config';
import moment from 'moment';
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 = '/$/settings/tip_account';
let failureEndpoint = '/$/settings/tip_account';
if (isDev) {
successStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + successEndpoint;
failureStripeRedirectUrl = 'http://localhost:' + WEBPACK_WEB_PORT + failureEndpoint;
} else {
successStripeRedirectUrl = URL + successEndpoint;
failureStripeRedirectUrl = URL + failureEndpoint;
}
type Props = {
source: string,
user: User,
};
type State = {
error: boolean,
loading: boolean,
content: ?string,
stripeConnectionUrl: string,
// alreadyUpdated: boolean,
accountConfirmed: boolean,
accountPendingConfirmation: boolean,
accountNotConfirmedButReceivedTips: boolean,
unpaidBalance: number,
pageTitle: 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,
accountNotConfirmedButReceivedTips: false,
unpaidBalance: 0,
stripeConnectionUrl: '',
pageTitle: 'Add Payout Method',
accountTransactions: [],
// alreadyUpdated: false,
};
}
componentDidMount() {
const { user } = this.props;
// $FlowFixMe
this.experimentalUiEnabled = user && user.experimental_ui;
var that = this;
function getAndSetAccountLink(stillNeedToConfirmAccount) {
Lbryio.call(
'account',
'link',
{
return_url: successStripeRedirectUrl,
refresh_url: failureStripeRedirectUrl,
environment: stripeEnvironment,
},
'post'
).then((accountLinkResponse) => {
// stripe link for user to navigate to and confirm account
const stripeConnectionUrl = accountLinkResponse.url;
// set connection url on frontend
that.setState({
stripeConnectionUrl,
});
// show the account confirmation link if not created already
if (stillNeedToConfirmAccount) {
that.setState({
accountPendingConfirmation: true,
});
}
});
}
// call the account status endpoint
Lbryio.call(
'account',
'status',
{
environment: stripeEnvironment,
},
'post'
)
.then((accountStatusResponse) => {
const yetToBeCashedOutBalance = accountStatusResponse.total_received_unpaid;
if (yetToBeCashedOutBalance) {
that.setState({
unpaidBalance: yetToBeCashedOutBalance,
});
Lbryio.call(
'account',
'list',
{
environment: stripeEnvironment,
},
'post'
)
.then((accountListResponse) => {
that.setState({
accountTransactions: accountListResponse,
});
console.log(accountListResponse);
});
}
// if charges already enabled, no need to generate an account link
if (accountStatusResponse.charges_enabled) {
// account has already been confirmed
that.setState({
accountConfirmed: true,
});
// user has not confirmed an account but have received payments
} else if (accountStatusResponse.total_received_unpaid > 0) {
that.setState({
accountNotConfirmedButReceivedTips: true,
});
getAndSetAccountLink();
// user has not received any amount or confirmed an account
} else {
// get stripe link and set it on the frontend
// pass true so it updates the frontend
getAndSetAccountLink(true);
}
})
.catch(function (error) {
// errorString passed from the API (with a 403 error)
const errorString = 'account not linked to user, please link first';
// if it's beamer's error indicating the account is not linked yet
if (error.message.indexOf(errorString) > -1) {
// get stripe link and set it on the frontend
getAndSetAccountLink();
} else {
// not an error from Beamer, throw it
throw new Error(error);
}
});
}
render() {
const {
stripeConnectionUrl,
accountConfirmed,
accountPendingConfirmation,
unpaidBalance,
accountNotConfirmedButReceivedTips,
pageTitle,
accountTransactions,
} = this.state;
const { user } = this.props;
if (user.fiat_enabled) {
return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
<Card
title={<div className="table__header-text">{__('Connect a bank account')}</div>}
isBodyList
body={
<div>
{/* show while waiting for account status */}
{!accountConfirmed && !accountPendingConfirmation && !accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Getting your bank account connection status...')}</h3>
</div>
</div>
</div>
)}
{/* user has yet to complete their integration */}
{!accountConfirmed && accountPendingConfirmation && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Connect your bank account to Odysee to receive donations directly from users')}</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
)}
{/* user has completed their integration */}
{accountConfirmed && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations! Your account has been connected with Odysee.')}</h3>
{unpaidBalance > 0 ? (
<div>
<br />
<h3>
{__(
'Your pending account balance is $%balance% USD.',
{ balance: unpaidBalance / 100 }
)}
</h3>
</div>
) : (
<div>
<br />
<h3>{__('Your account balance is $0 USD. When you receive a tip you will see it here.')}</h3>
</div>
)}
</div>
</div>
</div>
)}
{accountNotConfirmedButReceivedTips && (
<div className="card__body-actions">
<div>
<div>
<h3>{__('Congratulations, you have already begun receiving tips on Odysee!')}</h3>
<div>
<br />
<h3>
{__(
'Your pending account balance is $%balance% USD.',
{ balance: unpaidBalance / 100 }
)}
</h3>
</div>
<br />
<div>
<h3>
{__('Connect your bank account to be able to cash your pending balance out to your account.')}
</h3>
</div>
<div className="section__actions">
<a href={stripeConnectionUrl}>
<Button button="secondary" label={__('Connect your bank account')} icon={ICONS.FINANCE} />
</a>
</div>
</div>
</div>
</div>
)}
</div>
}
/>
<br />
{/* customer already has transactions */}
{accountTransactions && accountTransactions.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>{__('Tip Location')}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Processing Fee')}</th>
<th>{__('Odysee Fee')}</th>
<th>{__('Received Amount')}</th>
</tr>
</thead>
<tbody>
{accountTransactions &&
accountTransactions.reverse().map((transaction) => (
<tr key={transaction.name + transaction.created_at}>
<td>{moment(transaction.created_at).format('LLL')}</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
label={transaction.channel_name}
button="link"
/>
</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
label={transaction.channel_claim_id === transaction.source_claim_id ? 'Channel Page' : 'File Page'}
button="link"
/>
</td>
<td>${transaction.tipped_amount / 100}</td>
<td>${transaction.transaction_fee / 100}</td>
<td>${transaction.application_fee / 100}</td>
<td>${transaction.received_amount / 100}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
}
/>
)}
</Page>
);
} else {
return <></>; // probably null;
}
}
}
export default StripeAccountConnection;

View file

@ -2,6 +2,8 @@ import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings';
import { selectosNotificationsEnabled } from 'redux/selectors/settings';
import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user';
import { doOpenModal } from 'redux/actions/app';
import { doToast } from 'redux/actions/notifications';
import SettingsStripeCard from './view';
@ -13,6 +15,9 @@ const select = (state) => ({
const perform = (dispatch) => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
doOpenModal,
openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
doToast: (options) => dispatch(doToast(options)),
});
export default connect(select, perform)(SettingsStripeCard);

View file

@ -7,10 +7,10 @@ import Card from 'component/common/card';
import { Lbryio } from 'lbryinc';
import { STRIPE_PUBLIC_KEY } from 'config';
import moment from 'moment';
let scriptLoading = false;
// let scriptLoaded = false;
// let scriptDidError = false; // these could probably be in state if managing locally
import Plastic from 'react-plastic';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import * as MODALS from 'constants/modal_types';
let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key
@ -19,12 +19,16 @@ if (STRIPE_PUBLIC_KEY.indexOf('pk_live') > -1) {
stripeEnvironment = 'live';
}
// type Props = {
// disabled: boolean,
// label: ?string,
// email: ?string,
// scriptFailedToLoad: boolean,
// };
// eslint-disable-next-line flowtype/no-types-missing-file-annotation
type Props = {
disabled: boolean,
label: ?string,
email: ?string,
scriptFailedToLoad: boolean,
doOpenModal: (string, {}) => void,
openModal: (string, {}) => void,
setAsConfirmingCard: () => void,
};
//
// type State = {
// open: boolean,
@ -46,12 +50,17 @@ class SettingsStripeCard extends React.Component<Props, State> {
customerTransactions: [],
pageTitle: 'Add Card',
userCardDetails: {},
paymentMethodId: '',
};
}
componentDidMount() {
var that = this;
console.log(this.props);
var doToast = this.props.doToast;
const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/';
script.async = true;
@ -86,33 +95,27 @@ class SettingsStripeCard extends React.Component<Props, State> {
if (userHasAlreadySetupPayment) {
var card = customerStatusResponse.PaymentMethods[0].card;
var customer = customerStatusResponse.Customer;
var topOfDisplay = customer.email.split('@')[0];
var bottomOfDisplay = '@' + customer.email.split('@')[1];
console.log(customerStatusResponse.Customer);
var cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4,
topOfDisplay: topOfDisplay,
bottomOfDisplay: bottomOfDisplay,
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
});
// get customer transactions
Lbryio.call(
'customer',
'list',
{
environment: stripeEnvironment,
},
'post'
).then((customerTransactionsResponse) => {
that.setState({
customerTransactions: customerTransactionsResponse,
});
console.log(customerTransactionsResponse);
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
});
// otherwise, prompt them to save a card
@ -138,6 +141,22 @@ class SettingsStripeCard extends React.Component<Props, State> {
setupStripe();
});
}
// get customer transactions
Lbryio.call(
'customer',
'list',
{
environment: stripeEnvironment,
},
'post'
).then((customerTransactionsResponse) => {
that.setState({
customerTransactions: customerTransactionsResponse,
});
console.log(customerTransactionsResponse);
});
// if the status call fails, either an actual error or need to run setup first
})
.catch(function(error) {
@ -147,7 +166,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
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) {
if (error.message && error.message.indexOf(errorString) > -1) {
// send them to save a card
that.setState({
currentFlowStage: 'confirmingCard',
@ -169,12 +188,16 @@ class SettingsStripeCard extends React.Component<Props, State> {
// instantiate stripe elements
setupStripe();
});
} else if (error === 'internal_apis_down') {
var displayString = 'There was an error from the server, please let support know'
doToast({ message: displayString, isError: true });
} else {
console.log('Unseen before error');
}
});
}, 250);
function setupStripe() {
// TODO: have to fix this, using so that the script is available
setTimeout(function() {
@ -302,17 +325,25 @@ class SettingsStripeCard extends React.Component<Props, State> {
).then((customerStatusResponse) => {
var card = customerStatusResponse.PaymentMethods[0].card;
var customer = customerStatusResponse.Customer;
var topOfDisplay = customer.email.split('@')[0];
var bottomOfDisplay = '@' + customer.email.split('@')[1];
var cardDetails = {
brand: card.brand,
expiryYear: card.exp_year,
expiryMonth: card.exp_month,
lastFour: card.last4,
topOfDisplay,
bottomOfDisplay,
};
that.setState({
currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History',
userCardDetails: cardDetails,
paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
});
});
@ -325,59 +356,19 @@ class SettingsStripeCard extends React.Component<Props, State> {
}
}
componentDidUpdate() {
if (!scriptLoading) {
this.updateStripeHandler();
}
}
componentWillUnmount() {
// pretty sure this doesn't exist
// $FlowFixMe
if (this.loadPromise) {
// $FlowFixMe
this.loadPromise.reject();
}
// pretty sure this doesn't exist
// $FlowFixMe
if (CardVerify.stripeHandler && this.state.open) {
// $FlowFixMe
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;
var that = this;
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails } = this.state;
function setAsConfirmingCard(){
that.setState({
currentFlowStage: 'confirmingCard',
})
}
const { scriptFailedToLoad, doOpenModal, openModal } = this.props;
const { currentFlowStage, customerTransactions, pageTitle, userCardDetails, paymentMethodId } = this.state;
return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
@ -393,6 +384,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
</div>
)}
{/* customer has not added a card yet */}
{currentFlowStage === 'confirmingCard' && (
<div className="sr-root">
<div className="sr-main">
@ -411,21 +403,39 @@ class SettingsStripeCard extends React.Component<Props, State> {
</div>
)}
{/* if the user has already confirmed their card */}
{currentFlowStage === 'cardConfirmed' && (
<div className="successCard">
<Card
title={__('Card Details')}
body={
<>
<h4 className="grey-text">
Brand: {userCardDetails.brand.toUpperCase()} &nbsp; Last 4: {userCardDetails.lastFour} &nbsp;
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear} &nbsp;
</h4>
<Plastic
type={userCardDetails.brand}
name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay}
expiry={userCardDetails.expiryMonth + '/' + userCardDetails.expiryYear}
number={'____________' + userCardDetails.lastFour}
/>
<br />
<Button
button="secondary"
label={__('Remove Card')}
icon={ICONS.DELETE}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
openModal(MODALS.CONFIRM_REMOVE_CARD, {
paymentMethodId: paymentMethodId,
setAsConfirmingCard: setAsConfirmingCard,
});
}}
/>
</>
}
/>
<br />
{/* if a user has no transactions yet */}
{(!customerTransactions || customerTransactions.length === 0) && (
<Card
title={__('Tip History')}
@ -433,6 +443,10 @@ class SettingsStripeCard extends React.Component<Props, State> {
/>
)}
</div>
)}
{/* customer already has transactions */}
{customerTransactions && customerTransactions.length > 0 && (
<Card
title={__('Tip History')}
@ -444,16 +458,33 @@ class SettingsStripeCard extends React.Component<Props, State> {
<tr>
<th className="date-header">{__('Date')}</th>
<th>{<>{__('Receiving Channel Name')}</>}</th>
<th>{__('Tip Location')}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Anonymous')}</th>
</tr>
</thead>
<tbody>
{customerTransactions &&
customerTransactions.map((transaction) => (
customerTransactions.reverse().map((transaction) => (
<tr key={transaction.name + transaction.created_at}>
<td>{moment(transaction.created_at).format('LLL')}</td>
<td>{transaction.channel_name}</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.channel_claim_id}
label={transaction.channel_name}
button="link"
/>
</td>
<td>
<Button
className="stripe__card-link-text"
navigate={'/' + transaction.channel_name + ':' + transaction.source_claim_id}
label={transaction.channel_claim_id === transaction.source_claim_id ? 'Channel Page' : 'File Page'}
button="link"
/>
</td>
<td>${transaction.tipped_amount / 100}</td>
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
</tr>
@ -465,8 +496,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
}
/>
)}
</div>
)}
</Page>
);
}

View file

@ -34,9 +34,6 @@ const WalletPage = (props: Props) => {
) : (
<div className="card-stack">
<WalletBalance />
{/* @if TARGET='web' */}
<StripeAccountConnection />
{/* @endif */}
<TxoList search={search} />
</div>
)}

View file

@ -370,16 +370,32 @@ export function doCommentReact(commentId: string, type: string) {
};
}
/**
*
* @param comment
* @param claim_id - File claim id
* @param parent_id - What is this?
* @param uri
* @param livestream
* @param {string} [txid] Optional transaction id
* @param {string} [payment_intent_id] Optional transaction id
* @param {string} [environment] Optional environment for Stripe (test|live)
* @returns {(function(Dispatch, GetState): Promise<undefined|void|*>)|*}
*/
export function doCommentCreate(
comment: string = '',
claim_id: string = '',
parent_id?: string,
uri: string,
livestream?: boolean = false,
txid?: string
txid?: string,
payment_intent_id?: string,
environment?: string,
) {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
// get active channel that will receive comment and optional tip
const activeChannelClaim = selectActiveChannelClaim(state);
if (!activeChannelClaim) {
@ -401,6 +417,7 @@ export function doCommentCreate(
} catch (e) {}
}
// send a notification
if (parent_id) {
const notification = makeSelectNotificationForCommentId(parent_id)(state);
if (notification && !notification.is_seen) {
@ -412,6 +429,8 @@ export function doCommentCreate(
return dispatch(doToast({ isError: true, message: __('Unable to verify your channel. Please try again.') }));
}
// Comments is a function which helps make calls to the backend
// these params passed in POST call.
return Comments.comment_create({
comment: comment,
claim_id: claim_id,
@ -420,9 +439,12 @@ export function doCommentCreate(
parent_id: parent_id,
signature: signatureData.signature,
signing_ts: signatureData.signing_ts,
...(txid ? { support_tx_id: txid } : {}),
...(txid ? { support_tx_id: txid } : {}), // add transaction id if it exists
...(payment_intent_id ? { payment_intent_id } : {}), // add payment_intent_id if it exists
...(environment ? { environment } : {}), // add environment for stripe if it exists
})
.then((result: CommentCreateResponse) => {
console.log(result);
dispatch({
type: ACTIONS.COMMENT_CREATE_COMPLETED,
data: {
@ -435,6 +457,7 @@ export function doCommentCreate(
return result;
})
.catch((error) => {
console.log(error);
dispatch({
type: ACTIONS.COMMENT_CREATE_FAILED,
data: error,

View file

@ -1,3 +1,17 @@
//plastic card image creator
.jp-card-name {
bottom: -7px !important;
}
.jp-card-container {
margin: 0 !important;
margin-top: 8px !important;
}
.stripe__card-link-text {
color: rgb(253, 253, 253)
}
.date-header {
width: 30%;
}
@ -15,7 +29,7 @@
padding: 48px;
align-content: center;
justify-content: center;
height: 400px;
height: 319px;
margin: 0 auto;
}