Various touchups/fixes of membership functionality (#1066)
* various cleanups * more touchups * select currency to use based on location * fix sidebar * fixing strings and other touchups * refactor and do proper string interpolation * fix stripe error * text bugfix * Adjust help Co-authored-by: Thomas Zarebczan <thomas.zarebczan@gmail.com>
This commit is contained in:
parent
99f87e95e3
commit
4a9ba6555b
9 changed files with 218 additions and 89 deletions
|
@ -628,6 +628,10 @@
|
||||||
"This Week": "This Week",
|
"This Week": "This Week",
|
||||||
"This Month": "This Month",
|
"This Month": "This Month",
|
||||||
"This Year": "This Year",
|
"This Year": "This Year",
|
||||||
|
"Year": "Year",
|
||||||
|
"Yearly": "Yearly",
|
||||||
|
"Month": "Month",
|
||||||
|
"Monthly": "Monthly",
|
||||||
"Publishing": "Publishing",
|
"Publishing": "Publishing",
|
||||||
"Update published": "Update published",
|
"Update published": "Update published",
|
||||||
"Livestream Created": "Livestream Created",
|
"Livestream Created": "Livestream Created",
|
||||||
|
|
|
@ -7,26 +7,39 @@ type Props = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function I18nMessage(props: Props) {
|
export default function I18nMessage(props: Props) {
|
||||||
const message = __(props.children),
|
const message = __(props.children), // whole message string
|
||||||
regexp = /%\w+%/g,
|
regexp = /%\w+%/g,
|
||||||
matchingGroups = message.match(regexp);
|
interpolatedVariables = message.match(regexp);
|
||||||
|
|
||||||
if (!matchingGroups) {
|
// if there's no variable to interpolate then just send message
|
||||||
|
// otherwise algo to build the element below
|
||||||
|
if (!interpolatedVariables) {
|
||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// split string from variables
|
||||||
const messageSubstrings = message.split(regexp),
|
const messageSubstrings = message.split(regexp),
|
||||||
|
// interpolated variables
|
||||||
tokens = props.tokens;
|
tokens = props.tokens;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
|
{/* loop through substrings, interpolate tokens in between them */}
|
||||||
{messageSubstrings.map((substring, index) => {
|
{messageSubstrings.map((substring, index) => {
|
||||||
const token =
|
// the algorithm is such that, there will always be a variable in between a message substring
|
||||||
index < matchingGroups.length ? matchingGroups[index].substring(1, matchingGroups[index].length - 1) : null; // get token without % on each side
|
// so when you use the index you actually grab the proper token
|
||||||
|
const matchingToken = interpolatedVariables.length && interpolatedVariables[index];
|
||||||
|
|
||||||
|
// get token name without % on each side
|
||||||
|
const tokenVariableName = matchingToken && matchingToken.substring(1, matchingToken.length - 1);
|
||||||
|
|
||||||
|
// select token to use if it matches
|
||||||
|
const tokenToUse = tokenVariableName && tokens[tokenVariableName];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={index}>
|
<React.Fragment key={index}>
|
||||||
{substring}
|
{substring}
|
||||||
{token && tokens[token]}
|
{tokenToUse}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
|
@ -4,13 +4,13 @@ import * as PAGES from 'constants/pages';
|
||||||
|
|
||||||
import Icon from 'component/common/icon';
|
import Icon from 'component/common/icon';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import AstronautAndFriends from './astronaut_n_friends.png';
|
import AstronautAndFriends from './astronaut_n_friends.png';
|
||||||
import BadgePremium from './badge_premium.png';
|
import BadgePremium from './badge_premium.png';
|
||||||
import BadgePremiumPlus from './badge_premium-plus.png';
|
import BadgePremiumPlus from './badge_premium-plus.png';
|
||||||
import OdyseePremium from './odysee_premium.png';
|
import OdyseePremium from './odysee_premium.png';
|
||||||
import I18nMessage from 'component/i18nMessage';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
pageLocation: string,
|
pageLocation: string,
|
||||||
|
@ -50,6 +50,12 @@ export default function MembershipSplash(props: Props) {
|
||||||
{__('No ads')}
|
{__('No ads')}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
// const livestreamInfo = (
|
||||||
|
// <div className="membership-splash__info-content">
|
||||||
|
// <Icon icon={ICONS.NO_ADS} />
|
||||||
|
// {__('Livestreaming')}
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="membership-splash">
|
<div className="membership-splash">
|
||||||
|
@ -96,6 +102,8 @@ export default function MembershipSplash(props: Props) {
|
||||||
|
|
||||||
{badgeInfo}
|
{badgeInfo}
|
||||||
|
|
||||||
|
{/* {livestreamInfo} */}
|
||||||
|
|
||||||
{earlyAcessInfo}
|
{earlyAcessInfo}
|
||||||
|
|
||||||
<div className="membership-splash__info-button">
|
<div className="membership-splash__info-button">
|
||||||
|
@ -119,6 +127,8 @@ export default function MembershipSplash(props: Props) {
|
||||||
</section>
|
</section>
|
||||||
{badgeInfo}
|
{badgeInfo}
|
||||||
|
|
||||||
|
{/* {livestreamInfo} */}
|
||||||
|
|
||||||
{earlyAcessInfo}
|
{earlyAcessInfo}
|
||||||
|
|
||||||
{noAdsInfo}
|
{noAdsInfo}
|
||||||
|
|
|
@ -17,7 +17,7 @@ import Card from 'component/common/card';
|
||||||
import ClaimList from 'component/claimList';
|
import ClaimList from 'component/claimList';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
|
import { LIVESTREAM_RTMP_URL } from 'constants/livestream';
|
||||||
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from '../../../config';
|
import { ENABLE_NO_SOURCE_CLAIMS, CHANNEL_STAKED_LEVEL_LIVESTREAM } from 'config';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
hasChannels: boolean,
|
hasChannels: boolean,
|
||||||
|
@ -194,8 +194,10 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
<Page>
|
<Page>
|
||||||
{/* no livestreaming privs because no premium membership */}
|
{/* no livestreaming privs because no premium membership */}
|
||||||
{!livestreamEnabled && !odyseeMembership && (
|
{!livestreamEnabled && !odyseeMembership && (
|
||||||
<div>
|
<div style={{ marginTop: '11px' }}>
|
||||||
<h2 className={''}>Join Odysee Premium to be able to livestream</h2>
|
<h2 style={{ marginBottom: '15px' }}>
|
||||||
|
{__('To stream on Odysee, please join Odysee Premium or have 50 Credits as support on your channel')}
|
||||||
|
</h2>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
|
@ -203,6 +205,7 @@ export default function LivestreamSetupPage(props: Props) {
|
||||||
icon={ICONS.FINANCE}
|
icon={ICONS.FINANCE}
|
||||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
||||||
className="membership_button"
|
className="membership_button"
|
||||||
|
style={{ maxWidth: '238px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -14,8 +14,10 @@ import MembershipSplash from 'component/membershipSplash';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import ChannelSelector from 'component/channelSelector';
|
import ChannelSelector from 'component/channelSelector';
|
||||||
import PremiumBadge from 'component/common/premium-badge';
|
import PremiumBadge from 'component/common/premium-badge';
|
||||||
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
import useGetUserMemberships from 'effects/use-get-user-memberships';
|
||||||
import usePersistedState from 'effects/use-persisted-state';
|
import usePersistedState from 'effects/use-persisted-state';
|
||||||
|
import { fetchLocaleApi } from 'locale';
|
||||||
|
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
let stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
@ -51,25 +53,19 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
user,
|
user,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const shouldUseEuro = localStorage.getItem('gdprRequired');
|
|
||||||
let currencyToUse;
|
|
||||||
if (shouldUseEuro === 'true') {
|
|
||||||
currencyToUse = 'eur';
|
|
||||||
} else {
|
|
||||||
currencyToUse = 'usd';
|
|
||||||
}
|
|
||||||
|
|
||||||
const userChannelName = activeChannelClaim ? activeChannelClaim.name : '';
|
const userChannelName = activeChannelClaim ? activeChannelClaim.name : '';
|
||||||
const userChannelClaimId = activeChannelClaim && activeChannelClaim.claim_id;
|
const userChannelClaimId = activeChannelClaim && activeChannelClaim.claim_id;
|
||||||
|
|
||||||
const [cardSaved, setCardSaved] = React.useState();
|
const [cardSaved, setCardSaved] = React.useState();
|
||||||
const [membershipOptions, setMembershipOptions] = React.useState();
|
const [membershipOptions, setMembershipOptions] = React.useState();
|
||||||
const [userMemberships, setUserMemberships] = React.useState();
|
const [userMemberships, setUserMemberships] = React.useState();
|
||||||
|
const [currencyToUse, setCurrencyToUse] = React.useState('usd');
|
||||||
const [canceledMemberships, setCanceledMemberships] = React.useState();
|
const [canceledMemberships, setCanceledMemberships] = React.useState();
|
||||||
const [activeMemberships, setActiveMemberships] = React.useState();
|
const [activeMemberships, setActiveMemberships] = React.useState();
|
||||||
const [purchasedMemberships, setPurchasedMemberships] = React.useState([]);
|
const [purchasedMemberships, setPurchasedMemberships] = React.useState([]);
|
||||||
const [hasShownModal, setHasShownModal] = React.useState(false);
|
const [hasShownModal, setHasShownModal] = React.useState(false);
|
||||||
const [shouldFetchUserMemberships, setFetchUserMemberships] = React.useState(true);
|
const [shouldFetchUserMemberships, setFetchUserMemberships] = React.useState(true);
|
||||||
|
const [apiError, setApiError] = React.useState(false);
|
||||||
|
|
||||||
const [showHelp, setShowHelp] = usePersistedState('premium-help-seen', true);
|
const [showHelp, setShowHelp] = usePersistedState('premium-help-seen', true);
|
||||||
|
|
||||||
|
@ -124,6 +120,7 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
|
|
||||||
setUserMemberships(response);
|
setUserMemberships(response);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
setApiError(true);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
setFetchUserMemberships(false);
|
setFetchUserMemberships(false);
|
||||||
|
@ -133,7 +130,9 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
if (!shouldFetchUserMemberships) setFetchUserMemberships(true);
|
if (!shouldFetchUserMemberships) setFetchUserMemberships(true);
|
||||||
}, [shouldFetchUserMemberships]);
|
}, [shouldFetchUserMemberships]);
|
||||||
|
|
||||||
|
// make calls to backend and populate all the data for the frontend
|
||||||
React.useEffect(function () {
|
React.useEffect(function () {
|
||||||
|
// TODO: this should be refactored to make these calls in parallel
|
||||||
(async function () {
|
(async function () {
|
||||||
try {
|
try {
|
||||||
// check if there is a payment method
|
// check if there is a payment method
|
||||||
|
@ -145,6 +144,7 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
},
|
},
|
||||||
'post'
|
'post'
|
||||||
);
|
);
|
||||||
|
|
||||||
// hardcoded to first card
|
// hardcoded to first card
|
||||||
const hasAPaymentCard = Boolean(response && response.PaymentMethods && response.PaymentMethods[0]);
|
const hasAPaymentCard = Boolean(response && response.PaymentMethods && response.PaymentMethods[0]);
|
||||||
|
|
||||||
|
@ -154,6 +154,7 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
if (err.message === customerDoesntExistError) {
|
if (err.message === customerDoesntExistError) {
|
||||||
setCardSaved(false);
|
setCardSaved(false);
|
||||||
} else {
|
} else {
|
||||||
|
setApiError(true);
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -177,6 +178,16 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
} else {
|
} else {
|
||||||
setMembershipOptions(response);
|
setMembershipOptions(response);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setApiError(true);
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// use EUR if from European continent
|
||||||
|
const localeResponse = await fetchLocaleApi();
|
||||||
|
const isFromEurope = localeResponse?.data?.continent === 'EU';
|
||||||
|
if (isFromEurope) setCurrencyToUse('eur');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
}
|
}
|
||||||
|
@ -186,18 +197,28 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// we are still waiting from the backend if any of these are undefined
|
||||||
const stillWaitingFromBackend =
|
const stillWaitingFromBackend =
|
||||||
purchasedMemberships === undefined ||
|
purchasedMemberships === undefined ||
|
||||||
cardSaved === undefined ||
|
cardSaved === undefined ||
|
||||||
membershipOptions === undefined ||
|
membershipOptions === undefined ||
|
||||||
userMemberships === undefined;
|
userMemberships === undefined ||
|
||||||
|
currencyToUse === undefined;
|
||||||
|
|
||||||
const formatDate = function (date) {
|
const formatDate = function (date) {
|
||||||
return moment(new Date(date)).format('MMMM DD YYYY');
|
return moment(new Date(date)).format('MMMM DD YYYY');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// clear membership data
|
||||||
const deleteData = async function () {
|
const deleteData = async function () {
|
||||||
await Lbryio.call('membership', 'clear', {}, 'post');
|
await Lbryio.call(
|
||||||
|
'membership',
|
||||||
|
'clear',
|
||||||
|
{
|
||||||
|
environment: 'test',
|
||||||
|
},
|
||||||
|
'post'
|
||||||
|
);
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
@ -208,44 +229,78 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
// TODO: can clean this up, some repeating text
|
// TODO: can clean this up, some repeating text
|
||||||
function buildPurchaseString(price, interval, plan) {
|
function buildPurchaseString(price, interval, plan) {
|
||||||
let featureString = '';
|
let featureString = '';
|
||||||
|
|
||||||
// generate different strings depending on other conditions
|
// generate different strings depending on other conditions
|
||||||
if (plan === 'Premium' && !noChannelsOrIncognitoMode) {
|
if (plan === 'Premium' && !noChannelsOrIncognitoMode) {
|
||||||
featureString =
|
featureString = (
|
||||||
'Your badge will be shown for your ' +
|
<I18nMessage
|
||||||
userChannelName +
|
tokens={{
|
||||||
' channel in all areas of the app, and can be added to two additional channels in the future for free. ';
|
user_channel_name: <b className="membership-bolded">{userChannelName}</b>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Your badge will be shown for your %user_channel_name% channel in all areas of the app, and can be added to two
|
||||||
|
additional channels in the future for free.
|
||||||
|
</I18nMessage>
|
||||||
|
);
|
||||||
} else if (plan === 'Premium+' && !noChannelsOrIncognitoMode) {
|
} else if (plan === 'Premium+' && !noChannelsOrIncognitoMode) {
|
||||||
featureString =
|
// user has channel selected
|
||||||
'The no ads feature applies site-wide for all channels and your badge will be shown for your ' +
|
featureString = (
|
||||||
userChannelName +
|
<I18nMessage
|
||||||
' channel in all areas of the app, and can be added to two additional channels in the future for free. ';
|
tokens={{
|
||||||
|
user_channel_name: <b className="membership-bolded">{userChannelName}</b>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
The no ads feature applies site-wide for all channels and your badge will be shown for your
|
||||||
|
%user_channel_name% channel in all areas of the app, and can be added to two additional channels in the future
|
||||||
|
for free.
|
||||||
|
</I18nMessage>
|
||||||
|
);
|
||||||
} else if (plan === 'Premium' && !channels) {
|
} else if (plan === 'Premium' && !channels) {
|
||||||
|
// user has no channels
|
||||||
featureString =
|
featureString =
|
||||||
'You currently have no channels. To show your badge on a channel, please create a channel first. ' +
|
'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.';
|
'If you register a channel later you will be able to show a badge for up to three channels.';
|
||||||
} else if (plan === 'Premium+' && !channels) {
|
} else if (plan === 'Premium+' && !channels) {
|
||||||
|
// user has no channels
|
||||||
featureString =
|
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. ' +
|
'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.';
|
'If you register a channel later you will be able to show a badge for up to three channels.';
|
||||||
} else if (plan === 'Premium' && incognito) {
|
} else if (plan === 'Premium' && incognito) {
|
||||||
|
// user has incognito selected
|
||||||
featureString =
|
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, ' +
|
'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.';
|
'or you can show a badge for up to three channels in the future for free.';
|
||||||
} else if (plan === 'Premium+' && incognito) {
|
} else if (plan === 'Premium+' && incognito) {
|
||||||
|
// user has incognito selected
|
||||||
featureString =
|
featureString =
|
||||||
'The no ads feature applies site-wide. You currently have no channel selected and will not have a badge be visible, ' +
|
'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.';
|
'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.';
|
||||||
}
|
}
|
||||||
|
|
||||||
let purchaseString =
|
const priceDisplayString = __(
|
||||||
`You are purchasing a ${interval}ly membership, that is active immediately ` +
|
'You are purchasing a %displayInterval% %plan% membership, ' +
|
||||||
`and will renew ${interval}ly at a price of ${currencyToUse.toUpperCase()} ${
|
'that is active immediately and will renew %displayInterval% at a price of %displayCurrency% %currencySymbol%' +
|
||||||
currencyToUse === 'usd' ? '$' : '€'
|
price / 100 +
|
||||||
}${price / 100}. ` +
|
'. ',
|
||||||
featureString +
|
{
|
||||||
'You can cancel Premium at any time (no refunds) and you can also close this window and choose a different membership option.';
|
displayInterval: interval + 'ly',
|
||||||
|
displayCurrency: currencyToUse.toUpperCase(),
|
||||||
|
currencySymbol: currencyToUse === 'usd' ? '$' : '€',
|
||||||
|
plan,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return __(purchaseString);
|
let purchaseString = (
|
||||||
|
<>
|
||||||
|
{priceDisplayString}
|
||||||
|
{featureString}
|
||||||
|
{__(
|
||||||
|
' You can cancel Premium at any time (no refunds) and you can also close this window and choose a different membership option.'
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return purchaseString;
|
||||||
}
|
}
|
||||||
|
|
||||||
const purchaseMembership = function (e, membershipOption, price) {
|
const purchaseMembership = function (e, membershipOption, price) {
|
||||||
|
@ -283,23 +338,27 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, {
|
openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, {
|
||||||
membershipId,
|
membershipId,
|
||||||
hasMembership,
|
hasMembership,
|
||||||
purchaseString: cancellationString,
|
purchaseString: __(cancellationString),
|
||||||
populateMembershipData,
|
populateMembershipData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function convertPriceToString(price) {
|
function convertIntervalVariableToString(price) {
|
||||||
const interval = price.recurring.interval;
|
const interval = price.recurring.interval;
|
||||||
|
|
||||||
if (interval === 'year') {
|
if (interval === 'year') {
|
||||||
return 'Yearly';
|
return __('Yearly');
|
||||||
} else if (interval === 'month') {
|
} else if (interval === 'month') {
|
||||||
return 'Monthly';
|
return __('Monthly');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function capitalizeWord(string) {
|
function capitalizedInterval(planInterval) {
|
||||||
return string.charAt(0).toUpperCase() + string.slice(1);
|
if (planInterval === 'year') {
|
||||||
|
return __('Year');
|
||||||
|
} else {
|
||||||
|
return __('Month');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCurrencyDisplay(priceObject) {
|
function buildCurrencyDisplay(priceObject) {
|
||||||
|
@ -323,13 +382,15 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
const planValue = params.plan;
|
const planValue = params.plan;
|
||||||
|
|
||||||
// description to be shown under plan name
|
// description to be shown under plan name
|
||||||
function getPlanDescription(plan) {
|
function getPlanDescription(plan, active?) {
|
||||||
if (plan === 'Premium') {
|
if (plan === 'Premium') {
|
||||||
return 'Badge on profile, exclusive and early access to features';
|
return 'Badge on profile, livestreaming, automatic rewards confirmation, and early access to new features';
|
||||||
|
|
||||||
// if there's more plans added this needs to be expanded
|
// if there's more plans added this needs to be expanded
|
||||||
} else {
|
} else if (active) {
|
||||||
return 'All Premium features, and no ads';
|
return 'All Premium features, and no ads';
|
||||||
|
} else {
|
||||||
|
return 'Badge on profile, livestreaming, automatic rewards confirmation, early access to new features, and no ads';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -339,7 +400,7 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
// if user already selected plan, wait a bit (so it's not jarring) and open modal
|
// if user already selected plan, wait a bit (so it's not jarring) and open modal
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!stillWaitingFromBackend && planValue && cardSaved) {
|
if (!stillWaitingFromBackend && planValue && cardSaved) {
|
||||||
setTimeout(function () {
|
const delayTimeout = setTimeout(function () {
|
||||||
// clear query params
|
// clear query params
|
||||||
window.history.replaceState(null, null, window.location.pathname);
|
window.history.replaceState(null, null, window.location.pathname);
|
||||||
|
|
||||||
|
@ -349,6 +410,8 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
document.querySelector('[plan="' + plan + '"][interval="' + interval + '"]').click();
|
document.querySelector('[plan="' + plan + '"][interval="' + interval + '"]').click();
|
||||||
}, timeoutValue);
|
}, timeoutValue);
|
||||||
|
|
||||||
|
return () => clearTimeout(delayTimeout);
|
||||||
}
|
}
|
||||||
}, [stillWaitingFromBackend, planValue, cardSaved]);
|
}, [stillWaitingFromBackend, planValue, cardSaved]);
|
||||||
|
|
||||||
|
@ -363,15 +426,16 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
{__(
|
{__(
|
||||||
`Early access and exclusive features include: livestreaming and the ability to post odysee hyperlinks and images in comments + blogs. More to come later.`
|
`Early access and exclusive features include: livestreaming and the ability to post odysee hyperlinks and images in comments + blogs. Account is also automatically eligible for Rewards. More to come later.`
|
||||||
)}
|
)}
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
{__(
|
{__(
|
||||||
`The yearly Premium+ membership has a discount compared to monthly, and Premium is only available yearly.`
|
`The yearly Premium+ membership has a discount compared to monthly, and Premium is only available yearly.`
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
<li>{__(`These are limited time rates, so get in early!`)}</li>
|
<li>{__(`These are limited time rates, so get in early!`)}</li>
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
{__(
|
{__(
|
||||||
`There may be higher tiers available in the future for creators and anyone else who wants to support us.`
|
`There may be higher tiers available in the future for creators and anyone else who wants to support us.`
|
||||||
|
@ -392,7 +456,7 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
<>
|
<>
|
||||||
<Page className="premium-wrapper">
|
<Page className="premium-wrapper">
|
||||||
{/** splash frontend **/}
|
{/** splash frontend **/}
|
||||||
{!stillWaitingFromBackend && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? (
|
{!stillWaitingFromBackend && !apiError && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? (
|
||||||
<MembershipSplash pageLocation={'confirmPage'} currencyToUse={currencyToUse} />
|
<MembershipSplash pageLocation={'confirmPage'} currencyToUse={currencyToUse} />
|
||||||
) : (
|
) : (
|
||||||
/** odysee membership page **/
|
/** odysee membership page **/
|
||||||
|
@ -437,19 +501,21 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
{purchasedMemberships && !purchasedMemberships.includes(membershipOption.Membership.id) && (
|
{purchasedMemberships && !purchasedMemberships.includes(membershipOption.Membership.id) && (
|
||||||
<>
|
<>
|
||||||
<div className="premium-option">
|
<div className="premium-option">
|
||||||
|
{/* plan title */}
|
||||||
<h4 className="membership_title">
|
<h4 className="membership_title">
|
||||||
{membershipOption.Membership.name}
|
{__(membershipOption.Membership.name)}
|
||||||
<PremiumBadge membership={membershipOption.Membership.name} />
|
<PremiumBadge membership={membershipOption.Membership.name} />
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{/* plan description */}
|
{/* plan description */}
|
||||||
<h4 className="membership_subtitle">
|
<h4 className="membership_subtitle">
|
||||||
{getPlanDescription(membershipOption.Membership.name)}
|
{__(getPlanDescription(membershipOption.Membership.name))}
|
||||||
</h4>
|
</h4>
|
||||||
<>
|
<>
|
||||||
|
{/* display different plans */}
|
||||||
{membershipOption.Prices.map((price) => (
|
{membershipOption.Prices.map((price) => (
|
||||||
<>
|
<>
|
||||||
{/* dont show a monthly Premium membership option */}
|
{/* dont show a monthly Premium membership option (yearly only) */}
|
||||||
{!(
|
{!(
|
||||||
price.recurring.interval === 'month' &&
|
price.recurring.interval === 'month' &&
|
||||||
membershipOption.Membership.name === 'Premium'
|
membershipOption.Membership.name === 'Premium'
|
||||||
|
@ -458,11 +524,11 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
{price.currency === currencyToUse && (
|
{price.currency === currencyToUse && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>Interval:</b> {convertPriceToString(price)}
|
<b>{__('Interval')}:</b> {convertIntervalVariableToString(price)}
|
||||||
</h4>
|
</h4>
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>Price:</b> {buildCurrencyDisplay(price)}
|
<b>{__('Price')}:</b> {buildCurrencyDisplay(price)}
|
||||||
{price.unit_amount / 100}/{capitalizeWord(price.recurring.interval)}
|
{price.unit_amount / 100}/{capitalizedInterval(price.recurring.interval)}
|
||||||
</h4>
|
</h4>
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
|
@ -471,7 +537,9 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
membership-subscription-period={membershipOption.Membership.type}
|
membership-subscription-period={membershipOption.Membership.type}
|
||||||
price-id={price.id}
|
price-id={price.id}
|
||||||
className="membership_button"
|
className="membership_button"
|
||||||
label={__('Join via ' + price.recurring.interval + 'ly membership')}
|
label={__('Join via %displayInterval% membership', {
|
||||||
|
displayInterval: price.recurring.interval,
|
||||||
|
})}
|
||||||
icon={ICONS.FINANCE}
|
icon={ICONS.FINANCE}
|
||||||
interval={price.recurring.interval}
|
interval={price.recurring.interval}
|
||||||
plan={membershipOption.Membership.name}
|
plan={membershipOption.Membership.name}
|
||||||
|
@ -521,27 +589,33 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
|
|
||||||
{/* description section */}
|
{/* description section */}
|
||||||
<h4 className="membership_subtitle">
|
<h4 className="membership_subtitle">
|
||||||
{getPlanDescription(membership.MembershipDetails.name)}
|
{__(getPlanDescription(membership.MembershipDetails.name, 'active'))}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
{/* registered on */}
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Registered On:')}</b> {formatDate(membership.Membership.created_at)}
|
<b>{__('Registered On:')}</b> {formatDate(membership.Membership.created_at)}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
{/* autorenews at */}
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Auto-Renews On')}:</b>{' '}
|
<b>{__('Auto-Renews On')}:</b>{' '}
|
||||||
{formatDate(membership.Subscription.current_period_end * 1000)}
|
{formatDate(membership.Subscription.current_period_end * 1000)}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
{!stillWaitingFromBackend && membership.type === 'yearly' && (
|
{!stillWaitingFromBackend && membership.type === 'yearly' && (
|
||||||
<>
|
<>
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Membership Period Options:')}</b> {__('Yearly')}
|
<b>{__('Membership Period Options:')}</b> {__('Yearly')}
|
||||||
</h4>
|
</h4>
|
||||||
|
{/* TODO: this looks wrong, should support EUR as well */}
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
${(membership.cost_usd * 12) / 100} {__('USD For A One Year Membership')} ($
|
${(membership.cost_usd * 12) / 100} {__('USD For A One Year Membership')} ($
|
||||||
{membership.cost_usd / 100} {__('Per Month')})
|
{membership.cost_usd / 100} {__('Per Month')})
|
||||||
</h4>
|
</h4>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{/* cancel membership button */}
|
||||||
<Button
|
<Button
|
||||||
button="alt"
|
button="alt"
|
||||||
membership-id={membership.Membership.membership_id}
|
membership-id={membership.Membership.membership_id}
|
||||||
|
@ -576,13 +650,13 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
|
|
||||||
<div className="premium-option">
|
<div className="premium-option">
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Registered On:')}</b> {formatDate(membership.Membership.created_at)}
|
<b>{__('Registered On')}:</b> {formatDate(membership.Membership.created_at)}
|
||||||
</h4>
|
</h4>
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Canceled On:')}</b> {formatDate(membership.Subscription.canceled_at * 1000)}
|
<b>{__('Canceled On')}:</b> {formatDate(membership.Subscription.canceled_at * 1000)}
|
||||||
</h4>
|
</h4>
|
||||||
<h4 className="membership_info">
|
<h4 className="membership_info">
|
||||||
<b>{__('Still Valid Until:')}</b> {formatDate(membership.Membership.expires)}
|
<b>{__('Still Valid Until')}:</b> {formatDate(membership.Membership.expires)}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -597,28 +671,38 @@ const OdyseeMembershipPage = (props: Props) => {
|
||||||
<div>
|
<div>
|
||||||
<br />
|
<br />
|
||||||
<h2 className={'getPaymentCard'}>
|
<h2 className={'getPaymentCard'}>
|
||||||
{__(
|
{__('Please save a card as a payment method so you can join Odysee Premium')}
|
||||||
'Please save a card as a payment method so you can join Odysee Premium. After the card is added, click Back.'
|
|
||||||
)}
|
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
<h2 className={'getPaymentCard'}>{__('After the card is added, click Back')}</h2>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
button="primary"
|
button="primary"
|
||||||
label={__('Add A Card')}
|
label={__('Add A Card')}
|
||||||
icon={ICONS.SETTINGS}
|
icon={ICONS.SETTINGS}
|
||||||
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
navigate={`/$/${PAGES.SETTINGS_STRIPE_CARD}`}
|
||||||
className="membership_button"
|
className="membership_button"
|
||||||
|
style={{ maxWidth: '151px' }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/** loading section **/}
|
{/** loading section **/}
|
||||||
{stillWaitingFromBackend && (
|
{stillWaitingFromBackend && !apiError && (
|
||||||
<div className="main--empty">
|
<div className="main--empty">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/** loading section **/}
|
||||||
|
{stillWaitingFromBackend && apiError && (
|
||||||
|
<div className="main--empty">
|
||||||
|
<h1 style={{ fontSize: '19px' }}>
|
||||||
|
{__('Sorry, there was an error, please contact support or try again later')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/** clear membership data (only available on dev) **/}
|
{/** clear membership data (only available on dev) **/}
|
||||||
{isDev && cardSaved && purchasedMemberships.length > 0 && (
|
{isDev && cardSaved && purchasedMemberships.length > 0 && (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -14,6 +14,8 @@ import { STRIPE_PUBLIC_KEY } from 'config';
|
||||||
import { getStripeEnvironment } from 'util/stripe';
|
import { getStripeEnvironment } from 'util/stripe';
|
||||||
let stripeEnvironment = getStripeEnvironment();
|
let stripeEnvironment = getStripeEnvironment();
|
||||||
|
|
||||||
|
const STRIPE_PLUGIN_SRC = 'https://js.stripe.com/v3/';
|
||||||
|
|
||||||
const APIS_DOWN_ERROR_RESPONSE = __('There was an error from the server, please try again later');
|
const APIS_DOWN_ERROR_RESPONSE = __('There was an error from the server, please try again later');
|
||||||
const CARD_SETUP_ERROR_RESPONSE = __('There was an error getting your card setup, please try again later');
|
const CARD_SETUP_ERROR_RESPONSE = __('There was an error getting your card setup, please try again later');
|
||||||
|
|
||||||
|
@ -57,12 +59,17 @@ class SettingsStripeCard extends React.Component<Props, State> {
|
||||||
|
|
||||||
let doToast = this.props.doToast;
|
let doToast = this.props.doToast;
|
||||||
|
|
||||||
|
// only add script if it doesn't already exist
|
||||||
|
const stripeScriptExists = document.querySelectorAll(`script[src="${STRIPE_PLUGIN_SRC}"]`).length > 0;
|
||||||
|
|
||||||
|
if (!stripeScriptExists) {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.src = 'https://js.stripe.com/v3/';
|
script.src = STRIPE_PLUGIN_SRC;
|
||||||
script.async = true;
|
script.async = true;
|
||||||
|
|
||||||
// $FlowFixMe
|
// $FlowFixMe
|
||||||
document.body.appendChild(script);
|
document.body.appendChild(script);
|
||||||
|
}
|
||||||
|
|
||||||
// public key of the stripe account
|
// public key of the stripe account
|
||||||
let publicKey = STRIPE_PUBLIC_KEY;
|
let publicKey = STRIPE_PUBLIC_KEY;
|
||||||
|
|
|
@ -8,8 +8,9 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix contrast on hover of channel selector, couldn't come up with a better way
|
// fix badge becoming red when hovered
|
||||||
div[role='menuitem'] .channel__list-item .comment__badge svg {
|
div[role='menuitem'] .channel__list-item .comment__badge svg,
|
||||||
|
.comment__badge svg {
|
||||||
stroke: unset !important;
|
stroke: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,3 +18,8 @@ div[role='menuitem'] .channel__list-item .comment__badge svg {
|
||||||
.icon--PremiumPlus {
|
.icon--PremiumPlus {
|
||||||
filter: brightness(0.92);
|
filter: brightness(0.92);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// increase sidebar brightness
|
||||||
|
span > div.navigation__subscription-title > span > span.comment__badge {
|
||||||
|
filter: brightness(1.2);
|
||||||
|
}
|
||||||
|
|
|
@ -66,3 +66,12 @@
|
||||||
|
|
||||||
margin-bottom: 21px;
|
margin-bottom: 21px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.getPaymentCard {
|
||||||
|
font-size: 19px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-bolded {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
// @flow
|
// @flow
|
||||||
import { DOMAIN, SHOW_ADS } from 'config';
|
import { SHOW_ADS } from 'config';
|
||||||
import * as PAGES from 'constants/pages';
|
import * as PAGES from 'constants/pages';
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { withRouter } from 'react-router';
|
|
||||||
import I18nMessage from 'component/i18nMessage';
|
import I18nMessage from 'component/i18nMessage';
|
||||||
import Button from 'component/button';
|
import Button from 'component/button';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
@ -34,13 +33,11 @@ const IS_ANDROID = /Android/i.test(navigator.userAgent);
|
||||||
// const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX;
|
// const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX;
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
location: { pathname: string },
|
|
||||||
type: string,
|
type: string,
|
||||||
tileLayout?: boolean,
|
tileLayout?: boolean,
|
||||||
small: boolean,
|
small: boolean,
|
||||||
claim: Claim,
|
claim: Claim,
|
||||||
isMature: boolean,
|
isMature: boolean,
|
||||||
triggerBlacklist: boolean,
|
|
||||||
userHasPremiumPlus: boolean,
|
userHasPremiumPlus: boolean,
|
||||||
className?: string,
|
className?: string,
|
||||||
};
|
};
|
||||||
|
@ -124,16 +121,12 @@ function Ads(props: Props) {
|
||||||
const adsSignInDriver = (
|
const adsSignInDriver = (
|
||||||
<I18nMessage
|
<I18nMessage
|
||||||
tokens={{
|
tokens={{
|
||||||
log_in_to_domain: (
|
sign_up_for_premium: (
|
||||||
<Button
|
<Button button="link" label={__('Get Odysee Premium+')} navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`} />
|
||||||
button="link"
|
|
||||||
label={__('Get Odysee Premium+', { domain: DOMAIN })}
|
|
||||||
navigate={`/$/${PAGES.ODYSEE_MEMBERSHIP}`}
|
|
||||||
/>
|
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Hate these? %log_in_to_domain% for an ad free experience.
|
Hate these? %sign_up_for_premium% for an ad free experience.
|
||||||
</I18nMessage>
|
</I18nMessage>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -162,4 +155,4 @@ function Ads(props: Props) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withRouter(Ads);
|
export default Ads;
|
||||||
|
|
Loading…
Reference in a new issue