Preorder content functionality (#1743)
* adding preorder button * adding preorder modal * frontend mostly done * check if its already purchased * refresh page after purchase * smooth out purchase process * check if user has card saved * handle case where its the users own upload * fix transaction listing order bug * cleaning up code for merge * fix lint errors * fix flow errors * allow eur purchases * support eur on customer transaction page * fix css
This commit is contained in:
parent
017df02816
commit
4f47779303
18 changed files with 557 additions and 15 deletions
|
@ -5,6 +5,7 @@ import {
|
||||||
selectHasChannels,
|
selectHasChannels,
|
||||||
makeSelectTagInClaimOrChannelForUri,
|
makeSelectTagInClaimOrChannelForUri,
|
||||||
selectClaimIsNsfwForUri,
|
selectClaimIsNsfwForUri,
|
||||||
|
selectPreorderTag,
|
||||||
} from 'redux/selectors/claims';
|
} from 'redux/selectors/claims';
|
||||||
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
|
||||||
import { doPrepareEdit } from 'redux/actions/publish';
|
import { doPrepareEdit } from 'redux/actions/publish';
|
||||||
|
@ -32,6 +33,7 @@ const select = (state, props) => {
|
||||||
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
|
||||||
disableDownloadButton: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_DOWNLOAD_BUTTON_TAG)(state),
|
disableDownloadButton: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_DOWNLOAD_BUTTON_TAG)(state),
|
||||||
isMature: selectClaimIsNsfwForUri(state, uri),
|
isMature: selectClaimIsNsfwForUri(state, uri),
|
||||||
|
isAPreorder: selectPreorderTag(state, props.uri),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ type Props = {
|
||||||
doToast: (data: { message: string }) => void,
|
doToast: (data: { message: string }) => void,
|
||||||
doDownloadUri: (uri: string) => void,
|
doDownloadUri: (uri: string) => void,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
|
isAPreorder: boolean,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function FileActions(props: Props) {
|
export default function FileActions(props: Props) {
|
||||||
|
@ -54,6 +55,7 @@ export default function FileActions(props: Props) {
|
||||||
doToast,
|
doToast,
|
||||||
doDownloadUri,
|
doDownloadUri,
|
||||||
isMature,
|
isMature,
|
||||||
|
isAPreorder,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -115,7 +117,7 @@ export default function FileActions(props: Props) {
|
||||||
<div className="media__actions">
|
<div className="media__actions">
|
||||||
{ENABLE_FILE_REACTIONS && <FileReactions uri={uri} />}
|
{ENABLE_FILE_REACTIONS && <FileReactions uri={uri} />}
|
||||||
|
|
||||||
<ClaimSupportButton uri={uri} fileAction />
|
{ !isAPreorder && <ClaimSupportButton uri={uri} fileAction />}
|
||||||
|
|
||||||
<ClaimCollectionAddButton uri={uri} fileAction />
|
<ClaimCollectionAddButton uri={uri} fileAction />
|
||||||
|
|
||||||
|
|
23
ui/component/preorderButton/index.js
Normal file
23
ui/component/preorderButton/index.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { selectPreorderTag, selectClaimForUri, selectClaimIsMine } from 'redux/selectors/claims';
|
||||||
|
import ClaimTags from './view';
|
||||||
|
import { doOpenModal } from 'redux/actions/app';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
const claim = selectClaimForUri(state, props.uri);
|
||||||
|
|
||||||
|
return {
|
||||||
|
preorderTag: selectPreorderTag(state, props.uri),
|
||||||
|
claimIsMine: selectClaimIsMine(state, claim),
|
||||||
|
claim,
|
||||||
|
preferredCurrency: selectClientSetting(state, SETTINGS.PREFERRED_CURRENCY),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doOpenModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(select, perform)(ClaimTags);
|
111
ui/component/preorderButton/view.jsx
Normal file
111
ui/component/preorderButton/view.jsx
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
// @flow
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as MODALS from 'constants/modal_types';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import * as ICONS from 'constants/icons';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
|
let stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
preorderTag: string,
|
||||||
|
doOpenModal: (string, {}) => void,
|
||||||
|
claim: StreamClaim,
|
||||||
|
uri: string,
|
||||||
|
claimIsMine: boolean,
|
||||||
|
preferredCurrency: string,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PreorderButton(props: Props) {
|
||||||
|
const { preorderTag, doOpenModal, uri, claim, claimIsMine, preferredCurrency } = props;
|
||||||
|
|
||||||
|
const claimId = claim.claim_id;
|
||||||
|
const myUpload = claimIsMine;
|
||||||
|
|
||||||
|
const [hasAlreadyPreordered, setHasAlreadyPreordered] = React.useState(false);
|
||||||
|
|
||||||
|
function getPaymentHistory() {
|
||||||
|
return Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'list',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkIfAlreadyPurchased() {
|
||||||
|
try {
|
||||||
|
// get card payments customer has made
|
||||||
|
let customerTransactionResponse = await getPaymentHistory();
|
||||||
|
|
||||||
|
let matchingTransaction = false;
|
||||||
|
for (const transaction of customerTransactionResponse) {
|
||||||
|
if (claimId === transaction.source_claim_id) {
|
||||||
|
matchingTransaction = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matchingTransaction) {
|
||||||
|
setHasAlreadyPreordered(true);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fiatIconToUse = ICONS.FINANCE;
|
||||||
|
let fiatSymbolToUse = '$';
|
||||||
|
if (preferredCurrency === 'EUR') {
|
||||||
|
fiatIconToUse = ICONS.EURO;
|
||||||
|
fiatSymbolToUse = '€';
|
||||||
|
}
|
||||||
|
|
||||||
|
// populate customer payment data
|
||||||
|
React.useEffect(() => {
|
||||||
|
checkIfAlreadyPurchased();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{preorderTag && !hasAlreadyPreordered && !myUpload && (<div>
|
||||||
|
<Button
|
||||||
|
// ref={buttonRef}
|
||||||
|
iconColor="red"
|
||||||
|
className={'preorder-button'}
|
||||||
|
// largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
|
||||||
|
icon={fiatIconToUse}
|
||||||
|
button="primary"
|
||||||
|
label={`Preorder now for ${fiatSymbolToUse}${preorderTag}`}
|
||||||
|
// title={titlePrefix}
|
||||||
|
requiresAuth
|
||||||
|
onClick={() => doOpenModal(MODALS.PREORDER_CONTENT, { uri, checkIfAlreadyPurchased })}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
{preorderTag && hasAlreadyPreordered && !myUpload && (<div>
|
||||||
|
<Button
|
||||||
|
// ref={buttonRef}
|
||||||
|
iconColor="red"
|
||||||
|
className={'preorder-button'}
|
||||||
|
// largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
|
||||||
|
button="primary"
|
||||||
|
label={'You have preordered this content'}
|
||||||
|
// title={titlePrefix}
|
||||||
|
requiresAuth
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
{preorderTag && myUpload && (<div>
|
||||||
|
<Button
|
||||||
|
// ref={buttonRef}
|
||||||
|
iconColor="red"
|
||||||
|
className={'preorder-button'}
|
||||||
|
// largestLabel={isMobile && shrinkOnMobile ? '' : subscriptionLabel}
|
||||||
|
button="primary"
|
||||||
|
label={'You cannot preorder your own content'}
|
||||||
|
// title={titlePrefix}
|
||||||
|
/>
|
||||||
|
</div>)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
45
ui/component/preorderContent/index.js
Normal file
45
ui/component/preorderContent/index.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import {
|
||||||
|
selectClaimForUri,
|
||||||
|
selectPreorderTag,
|
||||||
|
} from 'redux/selectors/claims';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
import { preOrderPurchase } from 'redux/actions/wallet';
|
||||||
|
import { selectClientSetting } from 'redux/selectors/settings';
|
||||||
|
import { selectActiveChannelClaim } from 'redux/selectors/app';
|
||||||
|
import { withRouter } from 'react-router';
|
||||||
|
import * as SETTINGS from 'constants/settings';
|
||||||
|
import { getChannelIdFromClaim, getChannelNameFromClaim } from 'util/claim';
|
||||||
|
import WalletSendTip from './view';
|
||||||
|
|
||||||
|
const select = (state, props) => {
|
||||||
|
const { uri } = props;
|
||||||
|
|
||||||
|
const claim = selectClaimForUri(state, uri, false);
|
||||||
|
const { claim_id: claimId, value_type: claimType } = claim || {};
|
||||||
|
|
||||||
|
// setup variables for backend tip API
|
||||||
|
const channelClaimId = getChannelIdFromClaim(claim);
|
||||||
|
const tipChannelName = getChannelNameFromClaim(claim);
|
||||||
|
|
||||||
|
const activeChannelClaim = selectActiveChannelClaim(state);
|
||||||
|
const { name: activeChannelName, claim_id: activeChannelId } = activeChannelClaim || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeChannelName,
|
||||||
|
activeChannelId,
|
||||||
|
claimId,
|
||||||
|
claimType,
|
||||||
|
channelClaimId,
|
||||||
|
tipChannelName,
|
||||||
|
preferredCurrency: selectClientSetting(state, SETTINGS.PREFERRED_CURRENCY),
|
||||||
|
preorderTag: selectPreorderTag(state, props.uri),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doHideModal,
|
||||||
|
preOrderPurchase,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withRouter(connect(select, perform)(WalletSendTip));
|
190
ui/component/preorderContent/view.jsx
Normal file
190
ui/component/preorderContent/view.jsx
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
// @flow
|
||||||
|
import { Form } from 'component/common/form';
|
||||||
|
import { Lbryio } from 'lbryinc';
|
||||||
|
import * as PAGES from 'constants/pages';
|
||||||
|
import Button from 'component/button';
|
||||||
|
import Card from 'component/common/card';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
|
const stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
type TipParams = { tipAmount: number, tipChannelName: string, channelClaimId: string };
|
||||||
|
type UserParams = { activeChannelName: ?string, activeChannelId: ?string };
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
activeChannelId?: string,
|
||||||
|
activeChannelName?: string,
|
||||||
|
claimId: string,
|
||||||
|
claimType?: string,
|
||||||
|
channelClaimId?: string,
|
||||||
|
tipChannelName?: string,
|
||||||
|
claimIsMine: boolean,
|
||||||
|
isSupport: boolean,
|
||||||
|
isTipOnly?: boolean,
|
||||||
|
customText?: string,
|
||||||
|
doHideModal: () => void,
|
||||||
|
setAmount?: (number) => void,
|
||||||
|
preferredCurrency: string,
|
||||||
|
preOrderPurchase: (
|
||||||
|
TipParams,
|
||||||
|
anonymous: boolean,
|
||||||
|
UserParams,
|
||||||
|
claimId: string,
|
||||||
|
stripe: ?string,
|
||||||
|
preferredCurrency: string,
|
||||||
|
?(any) => Promise<void>,
|
||||||
|
?(any) => void
|
||||||
|
) => void,
|
||||||
|
preorderTag: string,
|
||||||
|
checkIfAlreadyPurchased: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PreorderContent(props: Props) {
|
||||||
|
const {
|
||||||
|
activeChannelId,
|
||||||
|
activeChannelName,
|
||||||
|
claimId,
|
||||||
|
channelClaimId,
|
||||||
|
tipChannelName,
|
||||||
|
doHideModal,
|
||||||
|
preOrderPurchase,
|
||||||
|
preferredCurrency,
|
||||||
|
preorderTag,
|
||||||
|
checkIfAlreadyPurchased,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// set the purchase amount once the preorder tag is selected
|
||||||
|
React.useEffect(() => {
|
||||||
|
setTipAmount(Number(preorderTag));
|
||||||
|
}, [preorderTag]);
|
||||||
|
|
||||||
|
/** STATE **/
|
||||||
|
const [tipAmount, setTipAmount] = React.useState(0);
|
||||||
|
|
||||||
|
const [waitingForBackend, setWaitingForBackend] = React.useState(false);
|
||||||
|
|
||||||
|
const [hasCardSaved, setHasSavedCard] = React.useState(true);
|
||||||
|
|
||||||
|
// check if user has a payment method saved
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!stripeEnvironment) return;
|
||||||
|
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'status',
|
||||||
|
{
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
).then((customerStatusResponse) => {
|
||||||
|
const defaultPaymentMethodId =
|
||||||
|
customerStatusResponse.Customer &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method &&
|
||||||
|
customerStatusResponse.Customer.invoice_settings.default_payment_method.id;
|
||||||
|
|
||||||
|
setHasSavedCard(Boolean(defaultPaymentMethodId));
|
||||||
|
});
|
||||||
|
}, [setHasSavedCard]);
|
||||||
|
|
||||||
|
// text for modal header
|
||||||
|
const titleText = 'Preorder Your Content';
|
||||||
|
|
||||||
|
// icon to use or explainer text to show per tab
|
||||||
|
let explainerText =
|
||||||
|
'This content is not available yet but you' + ' can pre-order it now so you can access it as soon as it goes live';
|
||||||
|
|
||||||
|
// when the form button is clicked
|
||||||
|
function handleSubmit() {
|
||||||
|
const tipParams: TipParams = {
|
||||||
|
tipAmount,
|
||||||
|
tipChannelName: tipChannelName || '',
|
||||||
|
channelClaimId: channelClaimId || '',
|
||||||
|
};
|
||||||
|
const userParams: UserParams = { activeChannelName, activeChannelId };
|
||||||
|
|
||||||
|
async function checkIfFinished() {
|
||||||
|
await checkIfAlreadyPurchased();
|
||||||
|
doHideModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
setWaitingForBackend(true);
|
||||||
|
|
||||||
|
// hit backend to send tip
|
||||||
|
preOrderPurchase(
|
||||||
|
tipParams,
|
||||||
|
!activeChannelId,
|
||||||
|
userParams,
|
||||||
|
claimId,
|
||||||
|
stripeEnvironment,
|
||||||
|
preferredCurrency,
|
||||||
|
checkIfFinished,
|
||||||
|
doHideModal
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fiatSymbolToUse = '$';
|
||||||
|
if (preferredCurrency === 'EUR') {
|
||||||
|
fiatSymbolToUse = '€';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildButtonText() {
|
||||||
|
return `Preorder your content for ${fiatSymbolToUse}${tipAmount.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
{!waitingForBackend && (
|
||||||
|
<Card
|
||||||
|
title={titleText}
|
||||||
|
className={'preorder-content-modal'}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
{/* short explainer under the button */}
|
||||||
|
<div className="section__subtitle">{explainerText}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
actions={
|
||||||
|
// confirm purchase card
|
||||||
|
<>
|
||||||
|
{/* */}
|
||||||
|
<div className="handle-submit-area">
|
||||||
|
<Button
|
||||||
|
autoFocus
|
||||||
|
onClick={handleSubmit}
|
||||||
|
button="primary"
|
||||||
|
// label={__('Confirm')}
|
||||||
|
label={buildButtonText()}
|
||||||
|
disabled={!hasCardSaved}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!hasCardSaved && (
|
||||||
|
<>
|
||||||
|
<div className="add-card-prompt">
|
||||||
|
<Button navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`} label={__('Add a Card')} button="link" />
|
||||||
|
{' ' + __('To Preorder Content')}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* processing payment card */}
|
||||||
|
{waitingForBackend && (
|
||||||
|
<Card
|
||||||
|
title={titleText}
|
||||||
|
className={'preorder-content-modal-loading'}
|
||||||
|
subtitle={
|
||||||
|
<>
|
||||||
|
{/* short explainer under the button */}
|
||||||
|
<div className="section__subtitle">{'Processing your purchase...'}</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
import FiatAccountHistory from './view';
|
import WalletFiatAccountHistory from './view';
|
||||||
|
|
||||||
export default FiatAccountHistory;
|
export default WalletFiatAccountHistory;
|
||||||
|
|
|
@ -8,17 +8,13 @@ type Props = {
|
||||||
transactions: any,
|
transactions: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const WalletBalance = (props: Props) => {
|
const WalletFiatAccountHistory = (props: Props) => {
|
||||||
// receive transactions from parent component
|
// receive transactions from parent component
|
||||||
const { transactions } = props;
|
const { transactions } = props;
|
||||||
|
|
||||||
let accountTransactions;
|
let accountTransactions = transactions;
|
||||||
|
|
||||||
// reverse so most recent payments come first
|
|
||||||
if (transactions && transactions.length) {
|
|
||||||
accountTransactions = transactions.reverse();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// TODO: should add pagination here
|
||||||
// if there are more than 10 transactions, limit it to 10 for the frontend
|
// if there are more than 10 transactions, limit it to 10 for the frontend
|
||||||
// if (accountTransactions && accountTransactions.length > 10) {
|
// if (accountTransactions && accountTransactions.length > 10) {
|
||||||
// accountTransactions.length = 10;
|
// accountTransactions.length = 10;
|
||||||
|
@ -104,4 +100,4 @@ const WalletBalance = (props: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WalletBalance;
|
export default WalletFiatAccountHistory;
|
||||||
|
|
|
@ -11,12 +11,28 @@ type Props = {
|
||||||
transactions: any,
|
transactions: any,
|
||||||
};
|
};
|
||||||
|
|
||||||
const WalletBalance = (props: Props) => {
|
const WalletFiatPaymentHistory = (props: Props) => {
|
||||||
// receive transactions from parent component
|
// receive transactions from parent component
|
||||||
const { transactions: accountTransactions } = props;
|
const { transactions: accountTransactions } = props;
|
||||||
|
|
||||||
const [lastFour, setLastFour] = React.useState();
|
const [lastFour, setLastFour] = React.useState();
|
||||||
|
|
||||||
|
function getSymbol(transaction) {
|
||||||
|
if (transaction.currency === 'eur') {
|
||||||
|
return '€';
|
||||||
|
} else {
|
||||||
|
return '$';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrencyIso(transaction) {
|
||||||
|
if (transaction.currency === 'eur') {
|
||||||
|
return 'EUR';
|
||||||
|
} else {
|
||||||
|
return 'USD';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getCustomerStatus() {
|
function getCustomerStatus() {
|
||||||
return Lbryio.call(
|
return Lbryio.call(
|
||||||
'customer',
|
'customer',
|
||||||
|
@ -53,7 +69,7 @@ const WalletBalance = (props: Props) => {
|
||||||
<th className="date-header">{__('Date')}</th>
|
<th className="date-header">{__('Date')}</th>
|
||||||
<th className="channelName-header">{<>{__('Receiving Channel Name')}</>}</th>
|
<th className="channelName-header">{<>{__('Receiving Channel Name')}</>}</th>
|
||||||
<th className="location-header">{__('Tip Location')}</th>
|
<th className="location-header">{__('Tip Location')}</th>
|
||||||
<th className="amount-header">{__('Amount (USD)')} </th>
|
<th className="amount-header">{__('Amount')} </th>
|
||||||
<th className="card-header">{__('Card Last 4')}</th>
|
<th className="card-header">{__('Card Last 4')}</th>
|
||||||
<th className="anonymous-header">{__('Anonymous')}</th>
|
<th className="anonymous-header">{__('Anonymous')}</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -88,7 +104,10 @@ const WalletBalance = (props: Props) => {
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{/* how much tipped */}
|
{/* how much tipped */}
|
||||||
<td>${transaction.tipped_amount / 100}</td>
|
<td>
|
||||||
|
{getSymbol(transaction)}
|
||||||
|
{transaction.tipped_amount / 100} {getCurrencyIso(transaction)}
|
||||||
|
</td>
|
||||||
{/* TODO: this is incorrect need it per transactions not per user */}
|
{/* TODO: this is incorrect need it per transactions not per user */}
|
||||||
{/* last four of credit card */}
|
{/* last four of credit card */}
|
||||||
<td>{lastFour}</td>
|
<td>{lastFour}</td>
|
||||||
|
@ -108,4 +127,4 @@ const WalletBalance = (props: Props) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default WalletBalance;
|
export default WalletFiatPaymentHistory;
|
||||||
|
|
|
@ -19,6 +19,7 @@ export const REWARD_GENERATED_CODE = 'reward_generated_code';
|
||||||
export const AFFIRM_PURCHASE = 'affirm_purchase';
|
export const AFFIRM_PURCHASE = 'affirm_purchase';
|
||||||
export const CONFIRM_CLAIM_REVOKE = 'confirm_claim_revoke';
|
export const CONFIRM_CLAIM_REVOKE = 'confirm_claim_revoke';
|
||||||
export const SEND_TIP = 'send_tip';
|
export const SEND_TIP = 'send_tip';
|
||||||
|
export const PREORDER_CONTENT = 'preorder_content';
|
||||||
export const CONFIRM_SEND_TIP = 'confirm_send_tip';
|
export const CONFIRM_SEND_TIP = 'confirm_send_tip';
|
||||||
export const REPOST = 'repost';
|
export const REPOST = 'repost';
|
||||||
export const SOCIAL_SHARE = 'social_share';
|
export const SOCIAL_SHARE = 'social_share';
|
||||||
|
|
9
ui/modal/modalPreorderContent/index.js
Normal file
9
ui/modal/modalPreorderContent/index.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { doHideModal } from 'redux/actions/app';
|
||||||
|
import ModalSendTip from './view';
|
||||||
|
|
||||||
|
const perform = {
|
||||||
|
doHideModal,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default connect(null, perform)(ModalSendTip);
|
28
ui/modal/modalPreorderContent/view.jsx
Normal file
28
ui/modal/modalPreorderContent/view.jsx
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
// @flow
|
||||||
|
import React from 'react';
|
||||||
|
import { Modal } from 'modal/modal';
|
||||||
|
import PreorderContent from 'component/preorderContent';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
uri: string,
|
||||||
|
doHideModal: () => void,
|
||||||
|
checkIfAlreadyPurchased: () => void,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ModalSendTip extends React.PureComponent<Props> {
|
||||||
|
render() {
|
||||||
|
const { uri, doHideModal, checkIfAlreadyPurchased } = this.props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal onAborted={doHideModal} isOpen type="card">
|
||||||
|
<PreorderContent
|
||||||
|
uri={uri}
|
||||||
|
onCancel={doHideModal}
|
||||||
|
checkIfAlreadyPurchased={checkIfAlreadyPurchased}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ModalSendTip;
|
|
@ -38,6 +38,7 @@ const MAP = Object.freeze({
|
||||||
[MODALS.MIN_CHANNEL_AGE]: lazyImport(() => import('modal/modalMinChannelAge' /* webpackChunkName: "modalMinChannelAge" */)),
|
[MODALS.MIN_CHANNEL_AGE]: lazyImport(() => import('modal/modalMinChannelAge' /* webpackChunkName: "modalMinChannelAge" */)),
|
||||||
[MODALS.MOBILE_SEARCH]: lazyImport(() => import('modal/modalMobileSearch' /* webpackChunkName: "modalMobileSearch" */)),
|
[MODALS.MOBILE_SEARCH]: lazyImport(() => import('modal/modalMobileSearch' /* webpackChunkName: "modalMobileSearch" */)),
|
||||||
[MODALS.PHONE_COLLECTION]: lazyImport(() => import('modal/modalPhoneCollection' /* webpackChunkName: "modalPhoneCollection" */)),
|
[MODALS.PHONE_COLLECTION]: lazyImport(() => import('modal/modalPhoneCollection' /* webpackChunkName: "modalPhoneCollection" */)),
|
||||||
|
[MODALS.PREORDER_CONTENT]: lazyImport(() => import('modal/modalPreorderContent' /* webpackChunkName: "modalPreorderContent" */)),
|
||||||
[MODALS.PUBLISH]: lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */)),
|
[MODALS.PUBLISH]: lazyImport(() => import('modal/modalPublish' /* webpackChunkName: "modalPublish" */)),
|
||||||
[MODALS.PUBLISH_PREVIEW]: lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)),
|
[MODALS.PUBLISH_PREVIEW]: lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)),
|
||||||
[MODALS.REPOST]: lazyImport(() => import('modal/modalRepost' /* webpackChunkName: "modalRepost" */)),
|
[MODALS.REPOST]: lazyImport(() => import('modal/modalRepost' /* webpackChunkName: "modalRepost" */)),
|
||||||
|
|
|
@ -16,6 +16,7 @@ import Button from 'component/button';
|
||||||
import Empty from 'component/common/empty';
|
import Empty from 'component/common/empty';
|
||||||
import SwipeableDrawer from 'component/swipeableDrawer';
|
import SwipeableDrawer from 'component/swipeableDrawer';
|
||||||
import DrawerExpandButton from 'component/swipeableDrawerExpand';
|
import DrawerExpandButton from 'component/swipeableDrawerExpand';
|
||||||
|
import PreorderButton from 'component/preorderButton';
|
||||||
import { useIsMobile, useIsMobileLandscape } from 'effects/use-screensize';
|
import { useIsMobile, useIsMobileLandscape } from 'effects/use-screensize';
|
||||||
|
|
||||||
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
|
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
|
||||||
|
@ -224,6 +225,7 @@ export default function FilePage(props: Props) {
|
||||||
{!isMarkdown && (
|
{!isMarkdown && (
|
||||||
<div className="file-page__secondary-content">
|
<div className="file-page__secondary-content">
|
||||||
<section className="file-page__media-actions">
|
<section className="file-page__media-actions">
|
||||||
|
<PreorderButton uri={uri} />
|
||||||
{claimIsMine && isLivestream && (
|
{claimIsMine && isLivestream && (
|
||||||
<div className="livestream__creator-message">
|
<div className="livestream__creator-message">
|
||||||
<h4>{__('Only visible to you')}</h4>
|
<h4>{__('Only visible to you')}</h4>
|
||||||
|
|
|
@ -797,3 +797,55 @@ export const doSendCashTip = (
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const preOrderPurchase = (
|
||||||
|
tipParams,
|
||||||
|
anonymous,
|
||||||
|
userParams,
|
||||||
|
claimId,
|
||||||
|
stripeEnvironment,
|
||||||
|
preferredCurrency,
|
||||||
|
successCallback,
|
||||||
|
failureCallback
|
||||||
|
) => (dispatch) => {
|
||||||
|
Lbryio.call(
|
||||||
|
'customer',
|
||||||
|
'tip',
|
||||||
|
{
|
||||||
|
// round to fix issues with floating point numbers
|
||||||
|
amount: Math.round(100 * tipParams.tipAmount), // convert from dollars to cents
|
||||||
|
creator_channel_name: tipParams.tipChannelName, // creator_channel_name
|
||||||
|
creator_channel_claim_id: tipParams.channelClaimId,
|
||||||
|
tipper_channel_name: userParams.activeChannelName,
|
||||||
|
tipper_channel_claim_id: userParams.activeChannelId,
|
||||||
|
currency: preferredCurrency || 'USD',
|
||||||
|
anonymous: anonymous,
|
||||||
|
source_claim_id: claimId,
|
||||||
|
environment: stripeEnvironment,
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
)
|
||||||
|
.then((customerTipResponse) => {
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
message: __('Preorder completed successfully'),
|
||||||
|
subMessage: __("You will be able to see the content as soon as it's available!"),
|
||||||
|
// linkText: `${fiatSymbol}${tipParams.tipAmount} ⇒ ${tipParams.tipChannelName}`,
|
||||||
|
// linkTarget: '/wallet',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (successCallback) successCallback(customerTipResponse);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
// show error message from Stripe if one exists (being passed from backend by Beamer's API currently)
|
||||||
|
dispatch(
|
||||||
|
doToast({
|
||||||
|
message: error.message || __('Sorry, there was an error in processing your payment!'),
|
||||||
|
isError: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (failureCallback) failureCallback(error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
|
@ -680,6 +680,11 @@ export const selectTagsForUri = createCachedSelector(selectMetadataForUri, (meta
|
||||||
return metadata && metadata.tags ? metadata.tags.filter((tag) => !INTERNAL_TAGS.includes(tag)) : [];
|
return metadata && metadata.tags ? metadata.tags.filter((tag) => !INTERNAL_TAGS.includes(tag)) : [];
|
||||||
})((state, uri) => String(uri));
|
})((state, uri) => String(uri));
|
||||||
|
|
||||||
|
export const selectPreorderTag = createCachedSelector(selectMetadataForUri, (metadata: ?GenericMetadata) => {
|
||||||
|
const matchingTag = metadata && metadata.tags && metadata.tags.find((tag) => tag.includes('preorder:'));
|
||||||
|
if (matchingTag) return matchingTag.slice(9);
|
||||||
|
})((state, uri) => String(uri));
|
||||||
|
|
||||||
export const selectFetchingClaimSearchByQuery = (state: State) => selectState(state).fetchingClaimSearchByQuery || {};
|
export const selectFetchingClaimSearchByQuery = (state: State) => selectState(state).fetchingClaimSearchByQuery || {};
|
||||||
|
|
||||||
export const selectFetchingClaimSearch = createSelector(
|
export const selectFetchingClaimSearch = createSelector(
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
@import 'component/notification';
|
@import 'component/notification';
|
||||||
@import 'component/nudge';
|
@import 'component/nudge';
|
||||||
@import 'component/pagination';
|
@import 'component/pagination';
|
||||||
|
@import 'component/preorder-button';
|
||||||
@import 'component/post';
|
@import 'component/post';
|
||||||
@import 'component/purchase';
|
@import 'component/purchase';
|
||||||
@import 'component/placeholder';
|
@import 'component/placeholder';
|
||||||
|
|
55
ui/scss/component/_preorder-button.scss
Normal file
55
ui/scss/component/_preorder-button.scss
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
.preorder-button {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
margin-top: 2px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
stroke: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preorder-content-modal {
|
||||||
|
max-width: 580px;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.preorder-content-modal {
|
||||||
|
h2.card__title, .section__subtitle, .handle-submit-area {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title-section {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__subtitle {
|
||||||
|
max-width: 421px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__main-actions {
|
||||||
|
padding-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-card-prompt {
|
||||||
|
margin: 0 auto;
|
||||||
|
margin-top: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
section.preorder-content-modal-loading {
|
||||||
|
h2.card__title, .section__subtitle {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card__title-section {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section__subtitle {
|
||||||
|
margin-top: 27px;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue