diff --git a/ui/component/fileActions/index.js b/ui/component/fileActions/index.js
index 38eb6a723..e93a4ad36 100644
--- a/ui/component/fileActions/index.js
+++ b/ui/component/fileActions/index.js
@@ -5,6 +5,7 @@ import {
selectHasChannels,
makeSelectTagInClaimOrChannelForUri,
selectClaimIsNsfwForUri,
+ selectPreorderTag,
} from 'redux/selectors/claims';
import { makeSelectStreamingUrlForUri } from 'redux/selectors/file_info';
import { doPrepareEdit } from 'redux/actions/publish';
@@ -32,6 +33,7 @@ const select = (state, props) => {
streamingUrl: makeSelectStreamingUrlForUri(uri)(state),
disableDownloadButton: makeSelectTagInClaimOrChannelForUri(uri, DISABLE_DOWNLOAD_BUTTON_TAG)(state),
isMature: selectClaimIsNsfwForUri(state, uri),
+ isAPreorder: selectPreorderTag(state, props.uri),
};
};
diff --git a/ui/component/fileActions/view.jsx b/ui/component/fileActions/view.jsx
index dff8123f8..5ad70b2c9 100644
--- a/ui/component/fileActions/view.jsx
+++ b/ui/component/fileActions/view.jsx
@@ -35,6 +35,7 @@ type Props = {
doToast: (data: { message: string }) => void,
doDownloadUri: (uri: string) => void,
isMature: boolean,
+ isAPreorder: boolean,
};
export default function FileActions(props: Props) {
@@ -54,6 +55,7 @@ export default function FileActions(props: Props) {
doToast,
doDownloadUri,
isMature,
+ isAPreorder,
} = props;
const {
@@ -115,7 +117,7 @@ export default function FileActions(props: Props) {
{ENABLE_FILE_REACTIONS &&
}
-
+ { !isAPreorder &&
}
diff --git a/ui/component/preorderButton/index.js b/ui/component/preorderButton/index.js
new file mode 100644
index 000000000..f8fb2f42d
--- /dev/null
+++ b/ui/component/preorderButton/index.js
@@ -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);
diff --git a/ui/component/preorderButton/view.jsx b/ui/component/preorderButton/view.jsx
new file mode 100644
index 000000000..e0345eedf
--- /dev/null
+++ b/ui/component/preorderButton/view.jsx
@@ -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 && (
+
)}
+ {preorderTag && hasAlreadyPreordered && !myUpload && (
+
+
)}
+ {preorderTag && myUpload && (
+
+
)}
+ >
+ );
+}
diff --git a/ui/component/preorderContent/index.js b/ui/component/preorderContent/index.js
new file mode 100644
index 000000000..d1f445aa6
--- /dev/null
+++ b/ui/component/preorderContent/index.js
@@ -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));
diff --git a/ui/component/preorderContent/view.jsx b/ui/component/preorderContent/view.jsx
new file mode 100644
index 000000000..0e9dc79cb
--- /dev/null
+++ b/ui/component/preorderContent/view.jsx
@@ -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
,
+ ?(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 (
+
+ );
+}
diff --git a/ui/component/walletFiatAccountHistory/index.js b/ui/component/walletFiatAccountHistory/index.js
index 8fed953a8..deecbf411 100644
--- a/ui/component/walletFiatAccountHistory/index.js
+++ b/ui/component/walletFiatAccountHistory/index.js
@@ -1,3 +1,3 @@
-import FiatAccountHistory from './view';
+import WalletFiatAccountHistory from './view';
-export default FiatAccountHistory;
+export default WalletFiatAccountHistory;
diff --git a/ui/component/walletFiatAccountHistory/view.jsx b/ui/component/walletFiatAccountHistory/view.jsx
index 317e8a14f..b10f09ced 100644
--- a/ui/component/walletFiatAccountHistory/view.jsx
+++ b/ui/component/walletFiatAccountHistory/view.jsx
@@ -8,17 +8,13 @@ type Props = {
transactions: any,
};
-const WalletBalance = (props: Props) => {
+const WalletFiatAccountHistory = (props: Props) => {
// receive transactions from parent component
const { transactions } = props;
- let accountTransactions;
-
- // reverse so most recent payments come first
- if (transactions && transactions.length) {
- accountTransactions = transactions.reverse();
- }
+ let accountTransactions = transactions;
+ // TODO: should add pagination here
// if there are more than 10 transactions, limit it to 10 for the frontend
// if (accountTransactions && accountTransactions.length > 10) {
// accountTransactions.length = 10;
@@ -104,4 +100,4 @@ const WalletBalance = (props: Props) => {
);
};
-export default WalletBalance;
+export default WalletFiatAccountHistory;
diff --git a/ui/component/walletFiatPaymentHistory/view.jsx b/ui/component/walletFiatPaymentHistory/view.jsx
index f52fa61fc..2ada92275 100644
--- a/ui/component/walletFiatPaymentHistory/view.jsx
+++ b/ui/component/walletFiatPaymentHistory/view.jsx
@@ -11,12 +11,28 @@ type Props = {
transactions: any,
};
-const WalletBalance = (props: Props) => {
+const WalletFiatPaymentHistory = (props: Props) => {
// receive transactions from parent component
const { transactions: accountTransactions } = props;
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() {
return Lbryio.call(
'customer',
@@ -53,7 +69,7 @@ const WalletBalance = (props: Props) => {
{__('Date')} |
{<>{__('Receiving Channel Name')}>} |
{__('Tip Location')} |
- {__('Amount (USD)')} |
+ {__('Amount')} |
{__('Card Last 4')} |
{__('Anonymous')} |
@@ -88,7 +104,10 @@ const WalletBalance = (props: Props) => {
/>
{/* how much tipped */}
- ${transaction.tipped_amount / 100} |
+
+ {getSymbol(transaction)}
+ {transaction.tipped_amount / 100} {getCurrencyIso(transaction)}
+ |
{/* TODO: this is incorrect need it per transactions not per user */}
{/* last four of credit card */}
{lastFour} |
@@ -108,4 +127,4 @@ const WalletBalance = (props: Props) => {
);
};
-export default WalletBalance;
+export default WalletFiatPaymentHistory;
diff --git a/ui/constants/modal_types.js b/ui/constants/modal_types.js
index baa1adba8..ba46bc93a 100644
--- a/ui/constants/modal_types.js
+++ b/ui/constants/modal_types.js
@@ -19,6 +19,7 @@ export const REWARD_GENERATED_CODE = 'reward_generated_code';
export const AFFIRM_PURCHASE = 'affirm_purchase';
export const CONFIRM_CLAIM_REVOKE = 'confirm_claim_revoke';
export const SEND_TIP = 'send_tip';
+export const PREORDER_CONTENT = 'preorder_content';
export const CONFIRM_SEND_TIP = 'confirm_send_tip';
export const REPOST = 'repost';
export const SOCIAL_SHARE = 'social_share';
diff --git a/ui/modal/modalPreorderContent/index.js b/ui/modal/modalPreorderContent/index.js
new file mode 100644
index 000000000..6200986d6
--- /dev/null
+++ b/ui/modal/modalPreorderContent/index.js
@@ -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);
diff --git a/ui/modal/modalPreorderContent/view.jsx b/ui/modal/modalPreorderContent/view.jsx
new file mode 100644
index 000000000..4354b4d7f
--- /dev/null
+++ b/ui/modal/modalPreorderContent/view.jsx
@@ -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 {
+ render() {
+ const { uri, doHideModal, checkIfAlreadyPurchased } = this.props;
+
+ return (
+
+
+
+ );
+ }
+}
+
+export default ModalSendTip;
diff --git a/ui/modal/modalRouter/view.jsx b/ui/modal/modalRouter/view.jsx
index 407101658..f51f4daee 100644
--- a/ui/modal/modalRouter/view.jsx
+++ b/ui/modal/modalRouter/view.jsx
@@ -38,6 +38,7 @@ const MAP = Object.freeze({
[MODALS.MIN_CHANNEL_AGE]: lazyImport(() => import('modal/modalMinChannelAge' /* webpackChunkName: "modalMinChannelAge" */)),
[MODALS.MOBILE_SEARCH]: lazyImport(() => import('modal/modalMobileSearch' /* webpackChunkName: "modalMobileSearch" */)),
[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_PREVIEW]: lazyImport(() => import('modal/modalPublishPreview' /* webpackChunkName: "modalPublishPreview" */)),
[MODALS.REPOST]: lazyImport(() => import('modal/modalRepost' /* webpackChunkName: "modalRepost" */)),
diff --git a/ui/page/file/view.jsx b/ui/page/file/view.jsx
index 485e43f94..bc47e69ca 100644
--- a/ui/page/file/view.jsx
+++ b/ui/page/file/view.jsx
@@ -16,6 +16,7 @@ import Button from 'component/button';
import Empty from 'component/common/empty';
import SwipeableDrawer from 'component/swipeableDrawer';
import DrawerExpandButton from 'component/swipeableDrawerExpand';
+import PreorderButton from 'component/preorderButton';
import { useIsMobile, useIsMobileLandscape } from 'effects/use-screensize';
const CommentsList = lazyImport(() => import('component/commentsList' /* webpackChunkName: "comments" */));
@@ -224,6 +225,7 @@ export default function FilePage(props: Props) {
{!isMarkdown && (
+
{claimIsMine && isLivestream && (
{__('Only visible to you')}
diff --git a/ui/redux/actions/wallet.js b/ui/redux/actions/wallet.js
index 134b5199e..dfeecba4d 100644
--- a/ui/redux/actions/wallet.js
+++ b/ui/redux/actions/wallet.js
@@ -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);
+ });
+};
diff --git a/ui/redux/selectors/claims.js b/ui/redux/selectors/claims.js
index 773f89dd7..24774bcce 100644
--- a/ui/redux/selectors/claims.js
+++ b/ui/redux/selectors/claims.js
@@ -680,6 +680,11 @@ export const selectTagsForUri = createCachedSelector(selectMetadataForUri, (meta
return metadata && metadata.tags ? metadata.tags.filter((tag) => !INTERNAL_TAGS.includes(tag)) : [];
})((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 selectFetchingClaimSearch = createSelector(
diff --git a/ui/scss/all.scss b/ui/scss/all.scss
index 062d92ae0..5e218026b 100644
--- a/ui/scss/all.scss
+++ b/ui/scss/all.scss
@@ -46,6 +46,7 @@
@import 'component/notification';
@import 'component/nudge';
@import 'component/pagination';
+@import 'component/preorder-button';
@import 'component/post';
@import 'component/purchase';
@import 'component/placeholder';
diff --git a/ui/scss/component/_preorder-button.scss b/ui/scss/component/_preorder-button.scss
new file mode 100644
index 000000000..314910790
--- /dev/null
+++ b/ui/scss/component/_preorder-button.scss
@@ -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;
+ }
+}