diff --git a/static/app-strings.json b/static/app-strings.json index 9b2137f19..68584311c 100644 --- a/static/app-strings.json +++ b/static/app-strings.json @@ -628,6 +628,10 @@ "This Week": "This Week", "This Month": "This Month", "This Year": "This Year", + "Year": "Year", + "Yearly": "Yearly", + "Month": "Month", + "Monthly": "Monthly", "Publishing": "Publishing", "Update published": "Update published", "Livestream Created": "Livestream Created", diff --git a/ui/component/i18nMessage/view.jsx b/ui/component/i18nMessage/view.jsx index c0df2bfb9..b52f1b836 100644 --- a/ui/component/i18nMessage/view.jsx +++ b/ui/component/i18nMessage/view.jsx @@ -7,26 +7,39 @@ type Props = { }; export default function I18nMessage(props: Props) { - const message = __(props.children), + const message = __(props.children), // whole message string 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; } + // split string from variables const messageSubstrings = message.split(regexp), + // interpolated variables tokens = props.tokens; return ( + {/* loop through substrings, interpolate tokens in between them */} {messageSubstrings.map((substring, index) => { - const token = - index < matchingGroups.length ? matchingGroups[index].substring(1, matchingGroups[index].length - 1) : null; // get token without % on each side + // the algorithm is such that, there will always be a variable in between a message substring + // 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 ( {substring} - {token && tokens[token]} + {tokenToUse} ); })} diff --git a/ui/component/membershipSplash/view.jsx b/ui/component/membershipSplash/view.jsx index c22c9f9e3..28cccd0ce 100644 --- a/ui/component/membershipSplash/view.jsx +++ b/ui/component/membershipSplash/view.jsx @@ -4,13 +4,13 @@ import * as PAGES from 'constants/pages'; import Icon from 'component/common/icon'; import Button from 'component/button'; +import I18nMessage from 'component/i18nMessage'; import React from 'react'; import AstronautAndFriends from './astronaut_n_friends.png'; import BadgePremium from './badge_premium.png'; import BadgePremiumPlus from './badge_premium-plus.png'; import OdyseePremium from './odysee_premium.png'; -import I18nMessage from 'component/i18nMessage'; type Props = { pageLocation: string, @@ -50,6 +50,12 @@ export default function MembershipSplash(props: Props) { {__('No ads')} ); + // const livestreamInfo = ( + //
+ // + // {__('Livestreaming')} + //
+ // ); return (
@@ -96,6 +102,8 @@ export default function MembershipSplash(props: Props) { {badgeInfo} + {/* {livestreamInfo} */} + {earlyAcessInfo}
@@ -119,6 +127,8 @@ export default function MembershipSplash(props: Props) { {badgeInfo} + {/* {livestreamInfo} */} + {earlyAcessInfo} {noAdsInfo} diff --git a/ui/page/livestreamSetup/view.jsx b/ui/page/livestreamSetup/view.jsx index 470c2fa9c..8aaaf2a99 100644 --- a/ui/page/livestreamSetup/view.jsx +++ b/ui/page/livestreamSetup/view.jsx @@ -17,7 +17,7 @@ import Card from 'component/common/card'; import ClaimList from 'component/claimList'; import usePersistedState from 'effects/use-persisted-state'; 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 = { hasChannels: boolean, @@ -194,8 +194,10 @@ export default function LivestreamSetupPage(props: Props) { {/* no livestreaming privs because no premium membership */} {!livestreamEnabled && !odyseeMembership && ( -
-

Join Odysee Premium to be able to livestream

+
+

+ {__('To stream on Odysee, please join Odysee Premium or have 50 Credits as support on your channel')} +

)} diff --git a/ui/page/odyseeMembership/view.jsx b/ui/page/odyseeMembership/view.jsx index 00135a51a..725a6de1e 100644 --- a/ui/page/odyseeMembership/view.jsx +++ b/ui/page/odyseeMembership/view.jsx @@ -14,8 +14,10 @@ import MembershipSplash from 'component/membershipSplash'; import Button from 'component/button'; import ChannelSelector from 'component/channelSelector'; import PremiumBadge from 'component/common/premium-badge'; +import I18nMessage from 'component/i18nMessage'; import useGetUserMemberships from 'effects/use-get-user-memberships'; import usePersistedState from 'effects/use-persisted-state'; +import { fetchLocaleApi } from 'locale'; let stripeEnvironment = getStripeEnvironment(); @@ -51,25 +53,19 @@ const OdyseeMembershipPage = (props: Props) => { user, } = props; - const shouldUseEuro = localStorage.getItem('gdprRequired'); - let currencyToUse; - if (shouldUseEuro === 'true') { - currencyToUse = 'eur'; - } else { - currencyToUse = 'usd'; - } - 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); @@ -124,6 +120,7 @@ const OdyseeMembershipPage = (props: Props) => { setUserMemberships(response); } catch (err) { + setApiError(true); console.log(err); } setFetchUserMemberships(false); @@ -133,7 +130,9 @@ const OdyseeMembershipPage = (props: Props) => { 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 @@ -145,6 +144,7 @@ const OdyseeMembershipPage = (props: Props) => { }, 'post' ); + // hardcoded to first card const hasAPaymentCard = Boolean(response && response.PaymentMethods && response.PaymentMethods[0]); @@ -154,6 +154,7 @@ const OdyseeMembershipPage = (props: Props) => { if (err.message === customerDoesntExistError) { setCardSaved(false); } else { + setApiError(true); console.log(err); } } @@ -177,6 +178,16 @@ const OdyseeMembershipPage = (props: Props) => { } else { 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) { console.log(err); } @@ -186,18 +197,28 @@ const OdyseeMembershipPage = (props: Props) => { // 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; + 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', {}, 'post'); + await Lbryio.call( + 'membership', + 'clear', + { + environment: 'test', + }, + 'post' + ); // $FlowFixMe location.reload(); }; @@ -208,44 +229,78 @@ const OdyseeMembershipPage = (props: Props) => { // 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 = - 'Your badge will be shown for your ' + - userChannelName + - ' channel in all areas of the app, and can be added to two additional channels in the future for free. '; + featureString = ( + {userChannelName}, + }} + > + 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. + + ); } else if (plan === 'Premium+' && !noChannelsOrIncognitoMode) { - featureString = - 'The no ads feature applies site-wide for all channels and your badge will be shown for your ' + - userChannelName + - ' channel in all areas of the app, and can be added to two additional channels in the future for free. '; + // user has channel selected + featureString = ( + {userChannelName}, + }} + > + 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. + + ); } 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.'; } - let purchaseString = - `You are purchasing a ${interval}ly membership, that is active immediately ` + - `and will renew ${interval}ly at a price of ${currencyToUse.toUpperCase()} ${ - currencyToUse === 'usd' ? '$' : '€' - }${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.'; + const priceDisplayString = __( + 'You are purchasing a %displayInterval% %plan% membership, ' + + 'that is active immediately and will renew %displayInterval% at a price of %displayCurrency% %currencySymbol%' + + price / 100 + + '. ', + { + 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) { @@ -283,23 +338,27 @@ const OdyseeMembershipPage = (props: Props) => { openModal(MODALS.CONFIRM_ODYSEE_MEMBERSHIP, { membershipId, hasMembership, - purchaseString: cancellationString, + purchaseString: __(cancellationString), populateMembershipData, }); }; - function convertPriceToString(price) { + function convertIntervalVariableToString(price) { const interval = price.recurring.interval; if (interval === 'year') { - return 'Yearly'; + return __('Yearly'); } else if (interval === 'month') { - return 'Monthly'; + return __('Monthly'); } } - function capitalizeWord(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + function capitalizedInterval(planInterval) { + if (planInterval === 'year') { + return __('Year'); + } else { + return __('Month'); + } } function buildCurrencyDisplay(priceObject) { @@ -323,13 +382,15 @@ const OdyseeMembershipPage = (props: Props) => { const planValue = params.plan; // description to be shown under plan name - function getPlanDescription(plan) { + function getPlanDescription(plan, active?) { 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 - } else { + } else if (active) { 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 React.useEffect(() => { if (!stillWaitingFromBackend && planValue && cardSaved) { - setTimeout(function () { + const delayTimeout = setTimeout(function () { // clear query params window.history.replaceState(null, null, window.location.pathname); @@ -349,6 +410,8 @@ const OdyseeMembershipPage = (props: Props) => { // $FlowFixMe document.querySelector('[plan="' + plan + '"][interval="' + interval + '"]').click(); }, timeoutValue); + + return () => clearTimeout(delayTimeout); } }, [stillWaitingFromBackend, planValue, cardSaved]); @@ -363,15 +426,16 @@ const OdyseeMembershipPage = (props: Props) => {
  • {__( - `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.` )} -
  • - {__( - `The yearly Premium+ membership has a discount compared to monthly, and Premium is only available yearly.` - )} -
  • -
  • {__(`These are limited time rates, so get in early!`)}
  • +
  • + {__( + `The yearly Premium+ membership has a discount compared to monthly, and Premium is only available yearly.` + )} +
  • +
  • {__(`These are limited time rates, so get in early!`)}
  • +
  • {__( `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) => { <> {/** splash frontend **/} - {!stillWaitingFromBackend && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? ( + {!stillWaitingFromBackend && !apiError && purchasedMemberships.length === 0 && !planValue && !hasShownModal ? ( ) : ( /** odysee membership page **/ @@ -437,19 +501,21 @@ const OdyseeMembershipPage = (props: Props) => { {purchasedMemberships && !purchasedMemberships.includes(membershipOption.Membership.id) && ( <>
    + {/* plan title */}

    - {membershipOption.Membership.name} + {__(membershipOption.Membership.name)}

    {/* plan description */}

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

    <> + {/* display different plans */} {membershipOption.Prices.map((price) => ( <> - {/* dont show a monthly Premium membership option */} + {/* dont show a monthly Premium membership option (yearly only) */} {!( price.recurring.interval === 'month' && membershipOption.Membership.name === 'Premium' @@ -458,11 +524,11 @@ const OdyseeMembershipPage = (props: Props) => { {price.currency === currencyToUse && (

    - Interval: {convertPriceToString(price)} + {__('Interval')}: {convertIntervalVariableToString(price)}

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

    )} {/** loading section **/} - {stillWaitingFromBackend && ( + {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 && ( <> diff --git a/ui/page/settingsStripeCard/view.jsx b/ui/page/settingsStripeCard/view.jsx index 48042c5f0..6a273aacd 100644 --- a/ui/page/settingsStripeCard/view.jsx +++ b/ui/page/settingsStripeCard/view.jsx @@ -14,6 +14,8 @@ import { STRIPE_PUBLIC_KEY } from 'config'; import { getStripeEnvironment } from 'util/stripe'; 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 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 { let doToast = this.props.doToast; - const script = document.createElement('script'); - script.src = 'https://js.stripe.com/v3/'; - script.async = true; + // only add script if it doesn't already exist + const stripeScriptExists = document.querySelectorAll(`script[src="${STRIPE_PLUGIN_SRC}"]`).length > 0; - // $FlowFixMe - document.body.appendChild(script); + if (!stripeScriptExists) { + const script = document.createElement('script'); + script.src = STRIPE_PLUGIN_SRC; + script.async = true; + + // $FlowFixMe + document.body.appendChild(script); + } // public key of the stripe account let publicKey = STRIPE_PUBLIC_KEY; @@ -426,7 +433,7 @@ class SettingsStripeCard extends React.Component { label={__('View Transactions')} icon={ICONS.SETTINGS} navigate={`/$/${PAGES.WALLET}?fiatType=outgoing&tab=fiat-payment-history¤cy=fiat`} - style={{marginLeft: '10px'}} + style={{ marginLeft: '10px' }} /> } diff --git a/ui/scss/component/_comment-badge.scss b/ui/scss/component/_comment-badge.scss index 2cb4e336e..d51e20961 100644 --- a/ui/scss/component/_comment-badge.scss +++ b/ui/scss/component/_comment-badge.scss @@ -8,8 +8,9 @@ } } -// fix contrast on hover of channel selector, couldn't come up with a better way -div[role='menuitem'] .channel__list-item .comment__badge svg { +// fix badge becoming red when hovered +div[role='menuitem'] .channel__list-item .comment__badge svg, +.comment__badge svg { stroke: unset !important; } @@ -17,3 +18,8 @@ div[role='menuitem'] .channel__list-item .comment__badge svg { .icon--PremiumPlus { filter: brightness(0.92); } + +// increase sidebar brightness +span > div.navigation__subscription-title > span > span.comment__badge { + filter: brightness(1.2); +} diff --git a/ui/scss/component/_membership.scss b/ui/scss/component/_membership.scss index d67cc09d0..3c255eddf 100644 --- a/ui/scss/component/_membership.scss +++ b/ui/scss/component/_membership.scss @@ -66,3 +66,12 @@ margin-bottom: 21px; } + +.getPaymentCard { + font-size: 19px; + margin-bottom: 10px; +} + +.membership-bolded { + color: var(--color-text); +} diff --git a/web/component/ads/view.jsx b/web/component/ads/view.jsx index f4acf9473..0fdfe8331 100644 --- a/web/component/ads/view.jsx +++ b/web/component/ads/view.jsx @@ -1,8 +1,7 @@ // @flow -import { DOMAIN, SHOW_ADS } from 'config'; +import { SHOW_ADS } from 'config'; import * as PAGES from 'constants/pages'; import React, { useEffect } from 'react'; -import { withRouter } from 'react-router'; import I18nMessage from 'component/i18nMessage'; import Button from 'component/button'; import classnames from 'classnames'; @@ -34,13 +33,11 @@ const IS_ANDROID = /Android/i.test(navigator.userAgent); // const isFirefoxAndroid = IS_ANDROID && IS_FIREFOX; type Props = { - location: { pathname: string }, type: string, tileLayout?: boolean, small: boolean, claim: Claim, isMature: boolean, - triggerBlacklist: boolean, userHasPremiumPlus: boolean, className?: string, }; @@ -124,16 +121,12 @@ function Ads(props: Props) { const adsSignInDriver = ( + sign_up_for_premium: ( +