/* eslint-disable no-console */ // @flow import React from 'react'; import moment from 'moment'; import Page from 'component/page'; import Spinner from 'component/spinner'; import { Lbryio } from 'lbryinc'; import { getStripeEnvironment } from 'util/stripe'; import { ODYSEE_CHANNEL } from 'constants/channels'; import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; import * as MODALS from 'constants/modal_types'; import Card from 'component/common/card'; import MembershipSplash from 'component/membershipSplash'; import Button from 'component/button'; import ChannelSelector from 'component/channelSelector'; import PremiumBadge from 'component/premiumBadge'; import I18nMessage from 'component/i18nMessage'; import useGetUserMemberships from 'effects/use-get-user-memberships'; import usePersistedState from 'effects/use-persisted-state'; const stripeEnvironment = getStripeEnvironment(); const isDev = process.env.NODE_ENV !== 'production'; function log(...args) { // @if process.env.LOG_MEMBERSHIP='true' console.log(args); // @endif } type Props = { history: { action: string, push: (string) => void, replace: (string) => void }, location: { search: string, pathname: string }, totalBalance: ?number, openModal: (string, {}) => void, activeChannelClaim: ?ChannelClaim, channels: ?Array, claimsByUri: { [string]: any }, fetchUserMemberships: (claimIdCsv: string) => void, incognito: boolean, updateUserOdyseeMembershipStatus: () => void, user: ?User, locale: ?LocaleInfo, preferredCurrency: ?string, }; const OdyseeMembershipPage = (props: Props) => { const { openModal, activeChannelClaim, channels, claimsByUri, fetchUserMemberships, updateUserOdyseeMembershipStatus, incognito, user, locale, preferredCurrency, } = props; const userChannelName = activeChannelClaim ? activeChannelClaim.name : ''; const userChannelClaimId = activeChannelClaim && activeChannelClaim.claim_id; const [cardSaved, setCardSaved] = React.useState(); const [membershipOptions, setMembershipOptions] = React.useState(); const [userMemberships, setUserMemberships] = React.useState(); const [currencyToUse, setCurrencyToUse] = React.useState('usd'); const [canceledMemberships, setCanceledMemberships] = React.useState(); const [activeMemberships, setActiveMemberships] = React.useState(); const [purchasedMemberships, setPurchasedMemberships] = React.useState([]); const [hasShownModal, setHasShownModal] = React.useState(false); const [shouldFetchUserMemberships, setFetchUserMemberships] = React.useState(true); const [apiError, setApiError] = React.useState(false); const [showHelp, setShowHelp] = usePersistedState('premium-help-seen', true); const hasMembership = activeMemberships && activeMemberships.length > 0; const channelUrls = channels && channels.map((channel) => channel.permanent_url); // check if membership data for user is already fetched, if it's needed then fetch it useGetUserMemberships(shouldFetchUserMemberships, channelUrls, claimsByUri, (value) => { fetchUserMemberships(value); setFetchUserMemberships(false); }); async function populateMembershipData() { try { // show the memberships the user is subscribed to const response = await Lbryio.call( 'membership', 'mine', { environment: stripeEnvironment, }, 'post' ); log('mine response'); log(response); let activeMemberships = []; let canceledMemberships = []; let purchasedMemberships = []; for (const membership of response) { // if it's autorenewing it's considered 'active' const isActive = membership.Membership.auto_renew; if (isActive) { activeMemberships.push(membership); } else { canceledMemberships.push(membership); } purchasedMemberships.push(membership.Membership.membership_id); } // hide the other membership options if there's already a purchased membership if (activeMemberships.length > 0) { setMembershipOptions(false); } setActiveMemberships(activeMemberships); setCanceledMemberships(canceledMemberships); setPurchasedMemberships(purchasedMemberships); // update the state to show the badge fetchUserMemberships(userChannelClaimId || ''); setUserMemberships(response); } catch (err) { setApiError(true); console.log(err); } setFetchUserMemberships(false); } React.useEffect(() => { if (!shouldFetchUserMemberships) setFetchUserMemberships(true); }, [shouldFetchUserMemberships]); // make calls to backend and populate all the data for the frontend React.useEffect(function () { // TODO: this should be refactored to make these calls in parallel (async function () { try { // check if there is a payment method const response = await Lbryio.call( 'customer', 'status', { environment: stripeEnvironment, }, 'post' ); log('customer/status response'); log(response); // hardcoded to first card const hasAPaymentCard = Boolean(response && response.PaymentMethods && response.PaymentMethods[0]); setCardSaved(hasAPaymentCard); } catch (err) { const customerDoesntExistError = 'user as customer is not setup yet'; if (err.message === customerDoesntExistError) { setCardSaved(false); } else { setApiError(true); console.log(err); } } try { // check the available membership for odysee.com const response = await Lbryio.call( 'membership', 'list', { environment: stripeEnvironment, // Using @odysee's channel info as memberships are only for @odysee. channel_id: ODYSEE_CHANNEL.ID, channel_name: ODYSEE_CHANNEL.NAME, }, 'post' ); log('membership/list response'); log(response); // hide other options if there's already a membership if (activeMemberships && activeMemberships.length > 0) { setMembershipOptions(false); } else { setMembershipOptions(response); } } catch (err) { setApiError(true); console.log(err); } // use currency if set on client, otherwise use USD by default or EUR if in Europe if (preferredCurrency) { setCurrencyToUse(preferredCurrency.toLowerCase()); } else { if (locale?.continent === 'EU') { setCurrencyToUse('eur'); } } populateMembershipData(); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // we are still waiting from the backend if any of these are undefined const stillWaitingFromBackend = purchasedMemberships === undefined || cardSaved === undefined || membershipOptions === undefined || userMemberships === undefined || currencyToUse === undefined; const formatDate = function (date) { return moment(new Date(date)).format('MMMM DD YYYY'); }; // clear membership data const deleteData = async function () { await Lbryio.call( 'membership', 'clear', { environment: 'test', }, 'post' ); // $FlowFixMe location.reload(); }; // dont pass channel name and id when calling purchase const noChannelsOrIncognitoMode = incognito || !channels; // TODO: can clean this up, some repeating text function buildPurchaseString(price, interval, plan) { let featureString = ''; // generate different strings depending on other conditions if (plan === 'Premium' && !noChannelsOrIncognitoMode) { featureString = ( {userChannelName} }}> Your badge will be shown for your %channel_name% channel in all areas of the app, and can be added to two additional channels in the future for free. ); } else if (plan === 'Premium+' && !noChannelsOrIncognitoMode) { // user has channel selected featureString = ( {userChannelName} }}> The no ads feature applies site-wide for all channels and your badge will be shown for your %channel_name% channel in all areas of the app, and can be added to two additional channels in the future for free. ); } else if (plan === 'Premium' && !channels) { // user has no channels featureString = __( 'You currently have no channels. To show your badge on a channel, please create a channel first. If you register a channel later you will be able to show a badge for up to three channels.' ); } else if (plan === 'Premium+' && !channels) { // user has no channels featureString = __( 'The no ads feature applies site-wide. You currently have no channels. To show your badge on a channel, please create a channel first. If you register a channel later you will be able to show a badge for up to three channels.' ); } else if (plan === 'Premium' && incognito) { // user has incognito selected featureString = __( 'You currently have no channel selected and will not have a badge be visible, if you want to show a badge you can select a channel now, or you can show a badge for up to three channels in the future for free.' ); } else if (plan === 'Premium+' && incognito) { // user has incognito selected featureString = __( 'The no ads feature applies site-wide. You currently have no channel selected and will not have a badge be visible, if you want to show a badge you can select a channel now, or you can show a badge for up to three channels in the future for free.' ); } const priceDisplayString = ( {interval === 'month' ? __('monthly') : __('yearly')} ), monthly_yearly: interval === 'month' ? __('monthly') : __('yearly'), price: ( {`${currencyToUse.toUpperCase()} ${currencyToUse === 'usd' ? '$' : '€'}${ price / 100 }`} ), }} > You are purchasing a %monthly_yearly_bolded% %plan% membership that is active immediately and will renew %monthly_yearly% at a price of %price%. ); const noRefund = __( 'You can cancel Premium at any time (no refunds) and you can also close this window and choose a different membership option.' ); return ( <> {priceDisplayString} {featureString} {noRefund} ); } const purchaseMembership = function (e, membershipOption, price) { e.preventDefault(); e.stopPropagation(); const planName = membershipOption.Membership.name; const membershipId = e.currentTarget.getAttribute('membership-id'); const priceId = e.currentTarget.getAttribute('price-id'); const purchaseString = buildPurchaseString(price.unit_amount, price.recurring.interval, planName); openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, { membershipId, userChannelClaimId: noChannelsOrIncognitoMode ? undefined : userChannelClaimId, userChannelName: noChannelsOrIncognitoMode ? undefined : userChannelName, priceId, purchaseString, plan: planName, populateMembershipData, setMembershipOptions, updateUserOdyseeMembershipStatus, user, }); }; const cancelMembership = async function (e, membership) { const membershipId = e.currentTarget.getAttribute('membership-id'); const cancellationString = 'You are cancelling your Odysee Premium. You will still have access to all the paid ' + 'features until the point of the expiration of your current membership, at which point you will not be charged ' + 'again and your membership will no longer be active. At this time, there is no way to subscribe to another membership if you cancel and there are no refunds.'; openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, { membershipId, hasMembership, purchaseString: __(cancellationString), populateMembershipData, }); }; function convertIntervalVariableToString(price) { const interval = price.recurring.interval; if (interval === 'year') { return __('Yearly'); } else if (interval === 'month') { return __('Monthly'); } } function capitalizedInterval(planInterval) { if (planInterval === 'year') { return __('Year'); } else { return __('Month'); } } function buildCurrencyDisplay(priceObject) { let currencySymbol; if (priceObject.currency === 'eur') { currencySymbol = '€'; } else if (priceObject.currency === 'usd') { currencySymbol = '$'; } const currency = priceObject.currency.toUpperCase(); return currency + ' ' + currencySymbol; } const urlSearchParams = new URLSearchParams(window.location.search); const params = Object.fromEntries(urlSearchParams.entries()); const { interval, plan } = params; const planValue = params.plan; // description to be shown under plan name function getPlanDescription(plan, active?) { if (plan === 'Premium') { return 'Badge on profile, automatic rewards confirmation, and early access to new features'; // if there's more plans added this needs to be expanded } else if (active) { return 'All Premium features, and no ads'; } else { return 'Badge on profile, automatic rewards confirmation, early access to new features, and no ads'; } } // add a bit of a delay otherwise it's a bit jarring const timeoutValue = 300; // if user already selected plan, wait a bit (so it's not jarring) and open modal React.useEffect(() => { if (!stillWaitingFromBackend && planValue && cardSaved) { const delayTimeout = setTimeout(function () { // clear query params window.history.replaceState(null, null, window.location.pathname); setHasShownModal(true); // open confirm purchase // $FlowFixMe document.querySelector('[plan="' + plan + '"][interval="' + interval + '"]').click(); }, timeoutValue); return () => clearTimeout(delayTimeout); } }, [stillWaitingFromBackend, planValue, cardSaved]); const helpText = (

{__( 'First of all, thank you for considering or purchasing a membership, it means a ton to us! A few important details to know:' )}

); return ( <> {/** splash frontend **/} {!stillWaitingFromBackend && !apiError && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? ( ) : ( /** odysee membership page **/
{!stillWaitingFromBackend && cardSaved !== false && ( <>

{__('Odysee Premium')}

{/* let user switch channel */}
{/* explainer help text */} setShowHelp(!showHelp)} /> } title={__('Get More Information')} subtitle={__('Expand to learn more about how Odysee Premium works')} actions={showHelp && helpText} className={'premium-explanation-text'} />
)} {/** available memberships **/} {/* if they have a card and don't have a membership yet */} {!stillWaitingFromBackend && membershipOptions && purchasedMemberships.length < 1 && cardSaved !== false && ( <>

{__('Available Memberships')}

{membershipOptions.map((membershipOption, i) => ( <>
{purchasedMemberships && !purchasedMemberships.includes(membershipOption.Membership.id) && ( <>
{/* plan title */}

{membershipOption.Membership.name}

{/* plan description */}

{__(getPlanDescription(membershipOption.Membership.name))}

<> {/* display different plans */} {membershipOption.Prices.map((price) => ( <> {/* dont show a monthly Premium membership option (yearly only) */} {!( price.recurring.interval === 'month' && membershipOption.Membership.name === 'Premium' ) && ( <> {price.currency === currencyToUse && (

{__('Interval')}: {convertIntervalVariableToString(price)}

{__('Price')}: {buildCurrencyDisplay(price)} {price.unit_amount / 100}/{capitalizedInterval(price.recurring.interval)}

)} )} ))}
)}
))}
)} {!stillWaitingFromBackend && cardSaved === true && ( <>

{__('Your Active Memberships')}

{/** * list of active memberships from user ***/}
{/*

Active Memberships

*/} {!stillWaitingFromBackend && activeMemberships && activeMemberships.length === 0 && (

{__('You currently have no active memberships')}

)} {/** active memberships **/} {!stillWaitingFromBackend && activeMemberships && activeMemberships.map((membership) => ( <>
{/* membership name */}

{membership.MembershipDetails.name}

{/* description section */}

{__(getPlanDescription(membership.MembershipDetails.name, 'active'))}

{/* registered on */}

{__('Registered On')}: {formatDate(membership.Membership.created_at)}

{/* autorenews at */}

{__('Auto-Renews On')}:{' '} {formatDate(membership.Subscription.current_period_end * 1000)}

{/* cancel membership button */}
))}
<> {/** canceled memberships **/}

{__('Canceled Memberships')}

{canceledMemberships && canceledMemberships.length === 0 && (

{__('You currently have no canceled memberships')}

)} {canceledMemberships && canceledMemberships.map((membership) => ( <>

{membership.MembershipDetails.name}

{__('Registered On')}: {formatDate(membership.Membership.created_at)}

{__('Canceled On')}: {formatDate(membership.Subscription.canceled_at * 1000)}

{__('Still Valid Until')}: {formatDate(membership.Membership.expires)}

))}
)} {/** send user to add card if they don't have one yet */} {!stillWaitingFromBackend && cardSaved === false && (

{__('Please save a card as a payment method so you can join Odysee Premium')}

)} {/** loading section **/} {stillWaitingFromBackend && !apiError && (
)} {/** loading section **/} {stillWaitingFromBackend && apiError && (

{__('Sorry, there was an error, please contact support or try again later')}

)} {/** clear membership data (only available on dev) **/} {isDev && cardSaved && purchasedMemberships.length > 0 && ( <>

Clear Membership Data (Only Available On Dev)

)}
)}
); }; export default OdyseeMembershipPage;