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", "feed": "^4.2.2",
"if-env": "^1.0.4", "if-env": "^1.0.4",
"react-datetime-picker": "^3.2.1", "react-datetime-picker": "^3.2.1",
"react-plastic": "^1.1.1",
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",
"remove-markdown": "^0.3.0", "remove-markdown": "^0.3.0",
"source-map-explorer": "^2.5.2", "source-map-explorer": "^2.5.2",

View file

@ -2026,7 +2026,7 @@
"Supporting content requires %lbc%": "Supporting content requires %lbc%", "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.", "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.", "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.", "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", "Add card to tip creators in USD": "Add card to tip creators in USD",
"Connect a bank account": "Connect a bank account", "Connect a bank account": "Connect a bank account",

View file

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

View file

@ -12,6 +12,7 @@ import { selectUserVerifiedEmail } from 'redux/selectors/user';
import { selectActiveChannelClaim } from 'redux/selectors/app'; import { selectActiveChannelClaim } from 'redux/selectors/app';
import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments'; import { makeSelectCommentsDisabledForUri } from 'redux/selectors/comments';
import { CommentCreate } from './view'; import { CommentCreate } from './view';
import { doToast } from 'redux/actions/notifications';
const select = (state, props) => ({ const select = (state, props) => ({
commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true, commentingEnabled: IS_WEB ? Boolean(selectUserVerifiedEmail(state)) : true,
@ -24,11 +25,12 @@ const select = (state, props) => ({
}); });
const perform = (dispatch, ownProps) => ({ const perform = (dispatch, ownProps) => ({
createComment: (comment, claimId, parentId, txid) => createComment: (comment, claimId, parentId, txid, payment_intent_id, environment) =>
dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid)), dispatch(doCommentCreate(comment, claimId, parentId, ownProps.uri, ownProps.livestream, txid, payment_intent_id, environment)),
openModal: (modal, props) => dispatch(doOpenModal(modal, props)), openModal: (modal, props) => dispatch(doOpenModal(modal, props)),
setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)), setActiveChannel: (claimId) => dispatch(doSetActiveChannel(claimId)),
sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)), sendTip: (params, callback, errorCallback) => dispatch(doSendTip(params, false, callback, errorCallback, false)),
doToast: (options) => dispatch(doToast(options)),
}); });
export default connect(select, perform)(CommentCreate); 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 ChannelThumbnail from 'component/channelThumbnail';
import UriIndicator from 'component/uriIndicator'; import UriIndicator from 'component/uriIndicator';
import Empty from 'component/common/empty'; 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 = { type Props = {
uri: string, uri: string,
claim: StreamClaim, claim: StreamClaim,
createComment: (string, string, string, ?string) => Promise<any>, createComment: (string, string, string, ?string, ?string, ?string) => Promise<any>,
commentsDisabledBySettings: boolean, commentsDisabledBySettings: boolean,
channels: ?Array<ChannelClaim>, channels: ?Array<ChannelClaim>,
onDoneReplying?: () => void, onDoneReplying?: () => void,
@ -35,6 +47,8 @@ type Props = {
toast: (string) => void, toast: (string) => void,
claimIsMine: boolean, claimIsMine: boolean,
sendTip: ({}, (any) => void, (any) => void) => void, sendTip: ({}, (any) => void, (any) => void) => void,
doToast: ({ message: string }) => void,
disabled: boolean,
}; };
export function CommentCreate(props: Props) { export function CommentCreate(props: Props) {
@ -53,8 +67,10 @@ export function CommentCreate(props: Props) {
livestream, livestream,
claimIsMine, claimIsMine,
sendTip, sendTip,
doToast,
} = props; } = props;
const buttonref: ElementRef<any> = React.useRef(); const buttonref: ElementRef<any> = React.useRef();
const { const {
push, push,
location: { pathname }, location: { pathname },
@ -72,6 +88,14 @@ export function CommentCreate(props: Props) {
const disabled = isSubmitting || !activeChannelClaim || !commentValue.length; const disabled = isSubmitting || !activeChannelClaim || !commentValue.length;
const charCount = commentValue.length; const charCount = commentValue.length;
const [activeTab, setActiveTab] = React.useState('');
const [tipError, setTipError] = React.useState();
// React.useEffect(() => {
// setTipError('yes');
// }, []);
function handleCommentChange(event) { function handleCommentChange(event) {
let commentValue; let commentValue;
if (isReply) { if (isReply) {
@ -123,26 +147,109 @@ export function CommentCreate(props: Props) {
channel_id: activeChannelClaim.claim_id, channel_id: activeChannelClaim.claim_id,
}; };
const activeChannelName = activeChannelClaim && activeChannelClaim.name;
const activeChannelId = activeChannelClaim && activeChannelClaim.claim_id;
console.log(activeChannelClaim);
setIsSubmitting(true); setIsSubmitting(true);
sendTip( if (activeTab === TAB_LBC) {
params, // call sendTip and then run the callback from the response
(response) => { // second parameter is callback
const { txid } = response; sendTip(
setTimeout(() => { params,
handleCreateComment(txid); (response) => {
}, 1500); const { txid } = response;
setSuccessTip({ txid, tipAmount }); // todo: why the setTimeout?
}, setTimeout(() => {
() => { handleCreateComment(txid);
setIsSubmitting(false); }, 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;
} }
);
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 });
});
}
} }
function handleCreateComment(txid) { /**
*
* @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); setIsSubmitting(true);
createComment(commentValue, claimId, parentId, txid)
createComment(commentValue, claimId, parentId, txid, payment_intent_id, environment)
.then((res) => { .then((res) => {
setIsSubmitting(false); setIsSubmitting(false);
@ -157,7 +264,7 @@ export function CommentCreate(props: Props) {
} }
} }
}) })
.catch(() => { .catch((e) => {
setIsSubmitting(false); setIsSubmitting(false);
setCommentFailure(true); setCommentFailure(true);
}); });
@ -198,10 +305,11 @@ export function CommentCreate(props: Props) {
} }
if (isReviewingSupportComment && activeChannelClaim) { if (isReviewingSupportComment && activeChannelClaim) {
return ( return (
<div className="comment__create"> <div className="comment__create">
<div className="comment__sc-preview"> <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} /> <ChannelThumbnail xsmall uri={activeChannelClaim.canonical_url} />
<div> <div>
@ -262,15 +370,17 @@ export function CommentCreate(props: Props) {
autoFocus={isReply} autoFocus={isReply}
textAreaMaxLength={livestream ? FF_MAX_CHARS_IN_LIVESTREAM_COMMENT : FF_MAX_CHARS_IN_COMMENT} 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"> <div className="section__actions section__actions--no-margin">
{isSupportComment ? ( {isSupportComment ? (
<> <>
<Button <Button
disabled={disabled} // TODO: add better check here
disabled={disabled || tipError}
type="button" type="button"
button="primary" button="primary"
icon={ICONS.LBC} icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
label={__('Review')} label={__('Review')}
onClick={() => setIsReviewingSupportComment(true)} onClick={() => setIsReviewingSupportComment(true)}
/> />
@ -296,7 +406,16 @@ export function CommentCreate(props: Props) {
requiresAuth={IS_WEB} requiresAuth={IS_WEB}
/> />
{!claimIsMine && ( {!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 && ( {isReply && (
<Button <Button

View file

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

View file

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

View file

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

View file

@ -46,6 +46,7 @@ export default function LivestreamComments(props: Props) {
superChatsTotalAmount, superChatsTotalAmount,
myChannels, myChannels,
} = props; } = props;
const commentsRef = React.createRef(); const commentsRef = React.createRef();
const [scrollBottom, setScrollBottom] = React.useState(true); const [scrollBottom, setScrollBottom] = React.useState(true);
const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT); const [viewMode, setViewMode] = React.useState(VIEW_MODE_CHAT);
@ -174,6 +175,7 @@ export default function LivestreamComments(props: Props) {
size={10} size={10}
className="livestream-superchat__amount-large" className="livestream-superchat__amount-large"
amount={superChat.support_amount} amount={superChat.support_amount}
isFiat={superChat.is_fiat}
/> />
</div> </div>
</div> </div>
@ -193,6 +195,7 @@ export default function LivestreamComments(props: Props) {
commentId={comment.comment_id} commentId={comment.comment_id}
message={comment.comment} message={comment.comment}
supportAmount={comment.support_amount} supportAmount={comment.support_amount}
isFiat={comment.is_fiat}
commentIsMine={comment.channel_id && isMyComment(comment.channel_id)} 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 SearchPage = lazyImport(() => import('page/search' /* webpackChunkName: "secondary" */));
const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */)); const SettingsAdvancedPage = lazyImport(() => import('page/settingsAdvanced' /* webpackChunkName: "secondary" */));
const SettingsStripeCard = lazyImport(() => import('page/settingsStripeCard' /* 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 SettingsCreatorPage = lazyImport(() => import('page/settingsCreator' /* webpackChunkName: "secondary" */));
const SettingsNotificationsPage = lazyImport(() => const SettingsNotificationsPage = lazyImport(() =>
import('page/settingsNotifications' /* webpackChunkName: "secondary" */) 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_NOTIFICATIONS}`} component={SettingsNotificationsPage} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} /> <PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} component={SettingsStripeCard} />
<PrivateRoute {...props} path={`/$/${PAGES.SETTINGS_STRIPE_ACCOUNT}`} component={SettingsStripeAccount} />
<PrivateRoute <PrivateRoute
{...props} {...props}
exact exact

View file

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

View file

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

View file

@ -10,40 +10,145 @@ import I18nMessage from 'component/i18nMessage';
import classnames from 'classnames'; import classnames from 'classnames';
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp'; import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
import { 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 DEFAULT_TIP_AMOUNTS = [1, 5, 25, 100];
const TAB_FIAT = 'TabFiat';
const TAB_LBC = 'TabLBC';
type Props = { type Props = {
balance: number, balance: number,
amount: number, amount: number,
onChange: (number) => void, onChange: (number) => void,
isAuthenticated: boolean,
claim: StreamClaim,
uri: string,
onTipErrorChange: (string) => void,
}; };
function WalletTipAmountSelector(props: Props) { 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 [useCustomTip, setUseCustomTip] = usePersistedState('comment-support:useCustomTip', false);
const [tipError, setTipError] = React.useState(); 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(() => { 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 regexp = RegExp(/^(\d*([.]\d{0,8})?)$/);
const validTipInput = regexp.test(String(amount)); const validTipInput = regexp.test(String(amount));
let tipError; let tipError;
if (!amount) { if (amount === 0) {
tipError = __('Amount must be a number');
} else if (amount <= 0) {
tipError = __('Amount must be a positive number'); tipError = __('Amount must be a positive number');
} else if (amount < MINIMUM_PUBLISH_BID) { } else if (!amount || typeof amount !== 'number') {
tipError = __('Amount must be higher'); tipError = __('Amount must be a number');
} else 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');
} }
// 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); setTipError(tipError);
}, [amount, balance, setTipError]); onTipErrorChange(tipError);
}, [amount, balance, setTipError, activeTab]);
function handleCustomPriceChange(amount: number) { function handleCustomPriceChange(amount: number) {
const tipAmount = parseFloat(amount); const tipAmount = parseFloat(amount);
@ -56,14 +161,14 @@ function WalletTipAmountSelector(props: Props) {
{DEFAULT_TIP_AMOUNTS.map((defaultAmount) => ( {DEFAULT_TIP_AMOUNTS.map((defaultAmount) => (
<Button <Button
key={defaultAmount} key={defaultAmount}
disabled={amount > balance} disabled={shouldDisableAmountSelector(defaultAmount)}
button="alt" button="alt"
className={classnames('button-toggle button-toggle--expandformobile', { className={classnames('button-toggle button-toggle--expandformobile', {
'button-toggle--active': defaultAmount === amount, 'button-toggle--active': defaultAmount === amount && !useCustomTip,
'button-toggle--disabled': amount > balance, 'button-toggle--disabled': amount > balance,
})} })}
label={defaultAmount} label={defaultAmount}
icon={ICONS.LBC} icon={activeTab === TAB_LBC ? ICONS.LBC : ICONS.FINANCE}
onClick={() => { onClick={() => {
handleCustomPriceChange(defaultAmount); handleCustomPriceChange(defaultAmount);
setUseCustomTip(false); setUseCustomTip(false);
@ -72,14 +177,15 @@ function WalletTipAmountSelector(props: Props) {
))} ))}
<Button <Button
button="alt" button="alt"
disabled={activeTab === TAB_FIAT && (!hasCardSaved || !canReceiveFiatTip)}
className={classnames('button-toggle button-toggle--expandformobile', { 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')} label={__('Custom')}
onClick={() => setUseCustomTip(true)} onClick={() => setUseCustomTip(true)}
/> />
{DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && ( {activeTab === TAB_LBC && DEFAULT_TIP_AMOUNTS.some((val) => val > balance) && (
<Button <Button
button="secondary" button="secondary"
className="button-toggle-group-action" className="button-toggle-group-action"
@ -90,18 +196,55 @@ function WalletTipAmountSelector(props: Props) {
)} )}
</div> </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 && ( {useCustomTip && (
<div className="comment__tip-input"> <div className="comment__tip-input">
<FormField <FormField
autoFocus autoFocus
name="tip-input" name="tip-input"
label={ label={ activeTab === TAB_LBC ?
<React.Fragment> <React.Fragment>
{__('Custom support amount')}{' '} {__('Custom support amount')}{' '}
<I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}> <I18nMessage tokens={{ lbc_balance: <CreditAmount precision={4} amount={balance} /> }}>
(%lbc_balance% available) (%lbc_balance% available)
</I18nMessage> </I18nMessage>
</React.Fragment> </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" className="form-field--price-amount"
error={tipError} error={tipError}
@ -115,7 +258,38 @@ function WalletTipAmountSelector(props: Props) {
</div> </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 CONFIRM_REMOVE_BTC_SWAP_ADDRESS = 'confirm_remove_btc_swap_address';
export const COLLECTION_ADD = 'collection_add'; export const COLLECTION_ADD = 'collection_add';
export const COLLECTION_DELETE = 'collection_delete'; 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.SEND = 'send';
exports.SETTINGS = 'settings'; exports.SETTINGS = 'settings';
exports.SETTINGS_STRIPE_CARD = 'settings/card'; exports.SETTINGS_STRIPE_CARD = 'settings/card';
exports.SETTINGS_STRIPE_ACCOUNT = 'settings/tip_account';
exports.SETTINGS_NOTIFICATIONS = 'settings/notifications'; exports.SETTINGS_NOTIFICATIONS = 'settings/notifications';
exports.SETTINGS_ADVANCED = 'settings/advanced'; exports.SETTINGS_ADVANCED = 'settings/advanced';
exports.SETTINGS_BLOCKED_MUTED = 'settings/block_and_mute'; 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 ModalPublish = lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */));
const ModalPublishPreview = lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)); const ModalPublishPreview = lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */));
const ModalRemoveBtcSwapAddress = lazyImport(() => import('modal/modalRemoveBtcSwapAddress' /* webpackChunkName: "modalRemoveBtcSwapAddress" */)); 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 ModalRemoveFile = lazyImport(() => import('modal/modalRemoveFile' /* webpackChunkName: "modalRemoveFile" */));
const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* webpackChunkName: "modalRevokeClaim" */)); const ModalRevokeClaim = lazyImport(() => import('modal/modalRevokeClaim' /* webpackChunkName: "modalRevokeClaim" */));
const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */)); const ModalRewardCode = lazyImport(() => import('modal/modalRewardCode' /* webpackChunkName: "modalRewardCode" */));
@ -151,6 +152,8 @@ function ModalRouter(props: Props) {
return ModalClaimCollectionAdd; return ModalClaimCollectionAdd;
case MODALS.COLLECTION_DELETE: case MODALS.COLLECTION_DELETE:
return ModalDeleteCollection; return ModalDeleteCollection;
case MODALS.CONFIRM_REMOVE_CARD:
return ModalRemoveCard;
default: default:
return null; return null;
} }

View file

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

View file

@ -72,6 +72,7 @@ type Props = {
enterSettings: () => void, enterSettings: () => void,
exitSettings: () => void, exitSettings: () => void,
myChannelUrls: ?Array<string>, myChannelUrls: ?Array<string>,
user: User,
}; };
type State = { type State = {
@ -189,6 +190,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
clearCache, clearCache,
openModal, openModal,
myChannelUrls, myChannelUrls,
user,
} = this.props; } = this.props;
const { storedPassword } = this.state; const { storedPassword } = this.state;
const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0;
@ -206,14 +208,32 @@ class SettingsPage extends React.PureComponent<Props, State> {
className="card-stack" className="card-stack"
> >
{/* @if TARGET='web' */} {/* @if TARGET='web' */}
<Card {user && user.fiat_enabled && <Card
title={__('Add card to tip creators in USD')} title={__('Bank Accounts')}
subtitle={__('Connect a bank account to receive tips and compensation in your local currency')}
actions={ actions={
<div className="section__actions"> <div className="section__actions">
<Button <Button
button="secondary" button="secondary"
label={__('Manage Card')} label={__('Manage')}
icon={ICONS.WALLET} 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}`} navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
/> />
</div> </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 { doSetClientSetting } from 'redux/actions/settings';
import { selectosNotificationsEnabled } from 'redux/selectors/settings'; import { selectosNotificationsEnabled } from 'redux/selectors/settings';
import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user'; import { selectUserVerifiedEmail, selectUserEmail } from 'redux/selectors/user';
import { doOpenModal } from 'redux/actions/app';
import { doToast } from 'redux/actions/notifications';
import SettingsStripeCard from './view'; import SettingsStripeCard from './view';
@ -13,6 +15,9 @@ const select = (state) => ({
const perform = (dispatch) => ({ const perform = (dispatch) => ({
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), 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); export default connect(select, perform)(SettingsStripeCard);

View file

@ -7,10 +7,10 @@ import Card from 'component/common/card';
import { Lbryio } from 'lbryinc'; import { Lbryio } from 'lbryinc';
import { STRIPE_PUBLIC_KEY } from 'config'; import { STRIPE_PUBLIC_KEY } from 'config';
import moment from 'moment'; import moment from 'moment';
import Plastic from 'react-plastic';
let scriptLoading = false; import Button from 'component/button';
// let scriptLoaded = false; import * as ICONS from 'constants/icons';
// let scriptDidError = false; // these could probably be in state if managing locally import * as MODALS from 'constants/modal_types';
let stripeEnvironment = 'test'; let stripeEnvironment = 'test';
// if the key contains pk_live it's a live key // 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'; stripeEnvironment = 'live';
} }
// type Props = { // eslint-disable-next-line flowtype/no-types-missing-file-annotation
// disabled: boolean, type Props = {
// label: ?string, disabled: boolean,
// email: ?string, label: ?string,
// scriptFailedToLoad: boolean, email: ?string,
// }; scriptFailedToLoad: boolean,
doOpenModal: (string, {}) => void,
openModal: (string, {}) => void,
setAsConfirmingCard: () => void,
};
// //
// type State = { // type State = {
// open: boolean, // open: boolean,
@ -46,12 +50,17 @@ class SettingsStripeCard extends React.Component<Props, State> {
customerTransactions: [], customerTransactions: [],
pageTitle: 'Add Card', pageTitle: 'Add Card',
userCardDetails: {}, userCardDetails: {},
paymentMethodId: '',
}; };
} }
componentDidMount() { componentDidMount() {
var that = this; var that = this;
console.log(this.props);
var doToast = this.props.doToast;
const script = document.createElement('script'); const script = document.createElement('script');
script.src = 'https://js.stripe.com/v3/'; script.src = 'https://js.stripe.com/v3/';
script.async = true; script.async = true;
@ -67,7 +76,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
// setting a timeout to let the client secret populate // setting a timeout to let the client secret populate
// TODO: fix this, should be a cleaner way // TODO: fix this, should be a cleaner way
setTimeout(function () { setTimeout(function() {
// check if customer has card setup already // check if customer has card setup already
Lbryio.call( Lbryio.call(
'customer', 'customer',
@ -86,33 +95,27 @@ class SettingsStripeCard extends React.Component<Props, State> {
if (userHasAlreadySetupPayment) { if (userHasAlreadySetupPayment) {
var card = customerStatusResponse.PaymentMethods[0].card; 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 = { var cardDetails = {
brand: card.brand, brand: card.brand,
expiryYear: card.exp_year, expiryYear: card.exp_year,
expiryMonth: card.exp_month, expiryMonth: card.exp_month,
lastFour: card.last4, lastFour: card.last4,
topOfDisplay: topOfDisplay,
bottomOfDisplay: bottomOfDisplay,
}; };
that.setState({ that.setState({
currentFlowStage: 'cardConfirmed', currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History', pageTitle: 'Tip History',
userCardDetails: cardDetails, userCardDetails: cardDetails,
}); paymentMethodId: customerStatusResponse.PaymentMethods[0].id,
// get customer transactions
Lbryio.call(
'customer',
'list',
{
environment: stripeEnvironment,
},
'post'
).then((customerTransactionsResponse) => {
that.setState({
customerTransactions: customerTransactionsResponse,
});
console.log(customerTransactionsResponse);
}); });
// otherwise, prompt them to save a card // otherwise, prompt them to save a card
@ -138,16 +141,32 @@ class SettingsStripeCard extends React.Component<Props, State> {
setupStripe(); 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 // if the status call fails, either an actual error or need to run setup first
}) })
.catch(function (error) { .catch(function(error) {
console.log(error); console.log(error);
// errorString passed from the API (with a 403 error) // errorString passed from the API (with a 403 error)
const errorString = 'user as customer is not setup yet'; const errorString = 'user as customer is not setup yet';
// if it's beamer's error indicating the account is not linked 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 // send them to save a card
that.setState({ that.setState({
currentFlowStage: 'confirmingCard', currentFlowStage: 'confirmingCard',
@ -169,16 +188,20 @@ class SettingsStripeCard extends React.Component<Props, State> {
// instantiate stripe elements // instantiate stripe elements
setupStripe(); 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 { } else {
console.log('Unseen before error'); console.log('Unseen before error');
} }
}); });
}, 250); }, 250);
function setupStripe() { function setupStripe() {
// TODO: have to fix this, using so that the script is available // TODO: have to fix this, using so that the script is available
setTimeout(function () { setTimeout(function() {
var stripeElements = function (publicKey, setupIntent) { var stripeElements = function(publicKey, setupIntent) {
var stripe = Stripe(publicKey); var stripe = Stripe(publicKey);
var elements = stripe.elements(); var elements = stripe.elements();
@ -200,17 +223,17 @@ class SettingsStripeCard extends React.Component<Props, State> {
card.mount('#card-element'); card.mount('#card-element');
// Element focus ring // Element focus ring
card.on('focus', function () { card.on('focus', function() {
var el = document.getElementById('card-element'); var el = document.getElementById('card-element');
el.classList.add('focused'); el.classList.add('focused');
}); });
card.on('blur', function () { card.on('blur', function() {
var el = document.getElementById('card-element'); var el = document.getElementById('card-element');
el.classList.remove('focused'); el.classList.remove('focused');
}); });
card.on('ready', function () { card.on('ready', function() {
card.focus(); card.focus();
}); });
@ -237,7 +260,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
billing_details: { email: email }, billing_details: { email: email },
}, },
}) })
.then(function (result) { .then(function(result) {
if (result.error) { if (result.error) {
console.log(result); console.log(result);
@ -254,7 +277,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
// Handle payment submission when user clicks the pay button. // Handle payment submission when user clicks the pay button.
var button = document.getElementById('submit'); var button = document.getElementById('submit');
button.addEventListener('click', function (event) { button.addEventListener('click', function(event) {
submitForm(event); submitForm(event);
}); });
@ -271,7 +294,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
stripeElements(publicKey, clientSecret); stripeElements(publicKey, clientSecret);
// Show a spinner on payment submission // Show a spinner on payment submission
var changeLoadingState = function (isLoading) { var changeLoadingState = function(isLoading) {
if (isLoading) { if (isLoading) {
// $FlowFixMe // $FlowFixMe
document.querySelector('button').disabled = true; document.querySelector('button').disabled = true;
@ -290,8 +313,8 @@ class SettingsStripeCard extends React.Component<Props, State> {
}; };
// shows a success / error message when the payment is complete // shows a success / error message when the payment is complete
var orderComplete = function (stripe, clientSecret) { var orderComplete = function(stripe, clientSecret) {
stripe.retrieveSetupIntent(clientSecret).then(function (result) { stripe.retrieveSetupIntent(clientSecret).then(function(result) {
Lbryio.call( Lbryio.call(
'customer', 'customer',
'status', 'status',
@ -302,17 +325,25 @@ class SettingsStripeCard extends React.Component<Props, State> {
).then((customerStatusResponse) => { ).then((customerStatusResponse) => {
var card = customerStatusResponse.PaymentMethods[0].card; var card = customerStatusResponse.PaymentMethods[0].card;
var customer = customerStatusResponse.Customer;
var topOfDisplay = customer.email.split('@')[0];
var bottomOfDisplay = '@' + customer.email.split('@')[1];
var cardDetails = { var cardDetails = {
brand: card.brand, brand: card.brand,
expiryYear: card.exp_year, expiryYear: card.exp_year,
expiryMonth: card.exp_month, expiryMonth: card.exp_month,
lastFour: card.last4, lastFour: card.last4,
topOfDisplay,
bottomOfDisplay,
}; };
that.setState({ that.setState({
currentFlowStage: 'cardConfirmed', currentFlowStage: 'cardConfirmed',
pageTitle: 'Tip History', pageTitle: 'Tip History',
userCardDetails: cardDetails, 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() { 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 ( return (
<Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation> <Page backout={{ title: pageTitle, backLabel: __('Done') }} noFooter noSideNavigation>
@ -393,6 +384,7 @@ class SettingsStripeCard extends React.Component<Props, State> {
</div> </div>
)} )}
{/* customer has not added a card yet */}
{currentFlowStage === 'confirmingCard' && ( {currentFlowStage === 'confirmingCard' && (
<div className="sr-root"> <div className="sr-root">
<div className="sr-main"> <div className="sr-main">
@ -411,21 +403,39 @@ class SettingsStripeCard extends React.Component<Props, State> {
</div> </div>
)} )}
{/* if the user has already confirmed their card */}
{currentFlowStage === 'cardConfirmed' && ( {currentFlowStage === 'cardConfirmed' && (
<div className="successCard"> <div className="successCard">
<Card <Card
title={__('Card Details')} title={__('Card Details')}
body={ body={
<> <>
<h4 className="grey-text"> <Plastic
Brand: {userCardDetails.brand.toUpperCase()} &nbsp; Last 4: {userCardDetails.lastFour} &nbsp; type={userCardDetails.brand}
Expires: {userCardDetails.expiryMonth}/{userCardDetails.expiryYear} &nbsp; name={userCardDetails.topOfDisplay + ' ' + userCardDetails.bottomOfDisplay}
</h4> 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 /> <br />
{/* if a user has no transactions yet */}
{(!customerTransactions || customerTransactions.length === 0) && ( {(!customerTransactions || customerTransactions.length === 0) && (
<Card <Card
title={__('Tip History')} title={__('Tip History')}
@ -433,40 +443,60 @@ class SettingsStripeCard extends React.Component<Props, State> {
/> />
)} )}
{customerTransactions && customerTransactions.length > 0 && (
<Card
title={__('Tip History')}
body={
<>
<div className="table__wrapper">
<table className="table table--transactions">
<thead>
<tr>
<th className="date-header">{__('Date')}</th>
<th>{<>{__('Receiving Channel Name')}</>}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Anonymous')}</th>
</tr>
</thead>
<tbody>
{customerTransactions &&
customerTransactions.map((transaction) => (
<tr key={transaction.name + transaction.created_at}>
<td>{moment(transaction.created_at).format('LLL')}</td>
<td>{transaction.channel_name}</td>
<td>${transaction.tipped_amount / 100}</td>
<td>{transaction.private_tip ? 'Yes' : 'No'}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
}
/>
)}
</div> </div>
)} )}
{/* customer already has transactions */}
{customerTransactions && customerTransactions.length > 0 && (
<Card
title={__('Tip History')}
body={
<>
<div className="table__wrapper">
<table className="table table--transactions">
<thead>
<tr>
<th className="date-header">{__('Date')}</th>
<th>{<>{__('Receiving Channel Name')}</>}</th>
<th>{__('Tip Location')}</th>
<th>{__('Amount (USD)')} </th>
<th>{__('Anonymous')}</th>
</tr>
</thead>
<tbody>
{customerTransactions &&
customerTransactions.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.private_tip ? 'Yes' : 'No'}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
}
/>
)}
</Page> </Page>
); );
} }

View file

@ -34,9 +34,6 @@ const WalletPage = (props: Props) => {
) : ( ) : (
<div className="card-stack"> <div className="card-stack">
<WalletBalance /> <WalletBalance />
{/* @if TARGET='web' */}
<StripeAccountConnection />
{/* @endif */}
<TxoList search={search} /> <TxoList search={search} />
</div> </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( export function doCommentCreate(
comment: string = '', comment: string = '',
claim_id: string = '', claim_id: string = '',
parent_id?: string, parent_id?: string,
uri: string, uri: string,
livestream?: boolean = false, livestream?: boolean = false,
txid?: string txid?: string,
payment_intent_id?: string,
environment?: string,
) { ) {
return async (dispatch: Dispatch, getState: GetState) => { return async (dispatch: Dispatch, getState: GetState) => {
const state = getState(); const state = getState();
// get active channel that will receive comment and optional tip
const activeChannelClaim = selectActiveChannelClaim(state); const activeChannelClaim = selectActiveChannelClaim(state);
if (!activeChannelClaim) { if (!activeChannelClaim) {
@ -401,6 +417,7 @@ export function doCommentCreate(
} catch (e) {} } catch (e) {}
} }
// send a notification
if (parent_id) { if (parent_id) {
const notification = makeSelectNotificationForCommentId(parent_id)(state); const notification = makeSelectNotificationForCommentId(parent_id)(state);
if (notification && !notification.is_seen) { 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.') })); 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({ return Comments.comment_create({
comment: comment, comment: comment,
claim_id: claim_id, claim_id: claim_id,
@ -420,9 +439,12 @@ export function doCommentCreate(
parent_id: parent_id, parent_id: parent_id,
signature: signatureData.signature, signature: signatureData.signature,
signing_ts: signatureData.signing_ts, 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) => { .then((result: CommentCreateResponse) => {
console.log(result);
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_COMPLETED, type: ACTIONS.COMMENT_CREATE_COMPLETED,
data: { data: {
@ -435,6 +457,7 @@ export function doCommentCreate(
return result; return result;
}) })
.catch((error) => { .catch((error) => {
console.log(error);
dispatch({ dispatch({
type: ACTIONS.COMMENT_CREATE_FAILED, type: ACTIONS.COMMENT_CREATE_FAILED,
data: error, 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 { .date-header {
width: 30%; width: 30%;
} }
@ -15,7 +29,7 @@
padding: 48px; padding: 48px;
align-content: center; align-content: center;
justify-content: center; justify-content: center;
height: 400px; height: 319px;
margin: 0 auto; margin: 0 auto;
} }