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:
mayeaux 2022-03-15 03:38:45 +01:00 committed by GitHub
parent 99f87e95e3
commit 4a9ba6555b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 218 additions and 89 deletions

View file

@ -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",

View file

@ -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>
); );
})} })}

View file

@ -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}

View file

@ -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>
)} )}

View file

@ -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 && (
<> <>

View file

@ -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;

View file

@ -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);
}

View file

@ -66,3 +66,12 @@
margin-bottom: 21px; margin-bottom: 21px;
} }
.getPaymentCard {
font-size: 19px;
margin-bottom: 10px;
}
.membership-bolded {
color: var(--color-text);
}

View file

@ -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;