diff --git a/ghost/portal/src/components/common/ProductsSection.js b/ghost/portal/src/components/common/ProductsSection.js index 946243b1bc..d500e11176 100644 --- a/ghost/portal/src/components/common/ProductsSection.js +++ b/ghost/portal/src/components/common/ProductsSection.js @@ -142,6 +142,15 @@ export const ProductsSectionStyles = ({site}) => { min-height: 56px; } + .gh-portal-product-card-name-trial { + display: flex; + align-items: center; + } + + .gh-portal-product-card-name-trial .gh-portal-discount-label { + margin-top: -4px; + } + .gh-portal-product-card-details { flex: 1; display: flex; @@ -160,6 +169,14 @@ export const ProductsSectionStyles = ({site}) => { color: var(--brandcolor); } + .gh-portal-discount-label-trial { + color: var(--brandcolor); + font-weight: 600; + font-size: 1.3rem; + line-height: 1; + margin-top: 4px; + } + .gh-portal-discount-label { position: relative; font-size: 1.25rem; @@ -188,7 +205,26 @@ export const ProductsSectionStyles = ({site}) => { opacity: 0.2; } + .gh-portal-product-card-price-trial { + display: flex; + flex-direction: row; + align-items: flex-end; + justify-content: space-between; + flex-wrap: wrap; + row-gap: 10px; + column-gap: 4px; + width: 100%; + } + .gh-portal-product-card-pricecontainer { + display: flex; + flex-direction: column; + align-items: flex-start; + width: 100%; + margin-top: 16px; + } + + .gh-portal-product-card-pricecontainer-old { display: flex; flex-direction: row; align-items: flex-end; @@ -478,6 +514,7 @@ export const ProductsSectionStyles = ({site}) => { `; }; +const freeTrialFlag = 1; const ProductsContext = React.createContext({ selectedInterval: 'month', selectedProduct: 'free', @@ -528,6 +565,16 @@ function ProductCardAlternatePrice({price}) { ); } +function ProductCardTrialDays({trialDays}) { + if (trialDays) { + return ( + {trialDays} days free + ); + } + + return null; +} + function ProductCardPrice({product}) { const {selectedInterval} = useContext(ProductsContext); const monthlyPrice = product.monthlyPrice; @@ -542,23 +589,28 @@ function ProductCardPrice({product}) { const yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount); const currencySymbol = getCurrencySymbol(activePrice.currency); - if (trialDays) { + if (freeTrialFlag) { return ( <>
-
- {trialDays} days free +
+
+ 1 ? ' long' : '')}>{currencySymbol} + {formatNumber(getStripeAmount(activePrice.amount))} + /{activePrice.interval} +
+
- {(selectedInterval === 'year' ? : '')} + {(selectedInterval === 'year' ? : '')}
- Then {currencySymbol}{formatNumber(getStripeAmount(activePrice.amount))}/{activePrice.interval} + {/* Then {currencySymbol}{formatNumber(getStripeAmount(activePrice.amount))}/{activePrice.interval} */} ); } return ( -
+
1 ? ' long' : '')}>{currencySymbol} {formatNumber(getStripeAmount(activePrice.amount))} @@ -615,7 +667,7 @@ function FreeProductCard({products, handleChooseSignup}) {

{getFreeTierTitle({site})}

{(!hasOnlyFree ? -
+
1 ? ' long' : '')}>{currencySymbol} 0 @@ -651,6 +703,7 @@ function FreeProductCard({products, handleChooseSignup}) { function ProductCard({product, products, selectedInterval, handleChooseSignup}) { const {selectedProduct, setSelectedProduct} = useContext(ProductsContext); const {action} = useContext(AppContext); + const trialDays = product.trial_days; const cardClass = selectedProduct === product.id ? 'gh-portal-product-card checked' : 'gh-portal-product-card'; const noOfProducts = products?.filter((d) => { @@ -668,6 +721,41 @@ function ProductCard({product, products, selectedInterval, handleChooseSignup}) productDescription = 'Full access'; } + if (freeTrialFlag) { + return ( + <> +
{ + e.stopPropagation(); + setSelectedProduct(product.id); + }}> +
+

{product.name}

+ +
+
+
+
+ {productDescription} +
+ +
+
+ +
+
+
+ + ); + } + return ( <>
{ @@ -699,7 +787,7 @@ function ProductCard({product, products, selectedInterval, handleChooseSignup})
- ); + ); } function ProductCards({products, selectedInterval, handleChooseSignup}) { @@ -715,7 +803,7 @@ function ProductCards({products, selectedInterval, handleChooseSignup}) { }); } -function YearlyDiscount({discount}) { +function YearlyDiscount({discount, trialDays}) { const {site} = useContext(AppContext); const {portal_plans: portalPlans} = site; @@ -723,6 +811,14 @@ function YearlyDiscount({discount}) { return null; } + if (freeTrialFlag) { + return ( + <> + {discount}% discount + + ); + } + return ( <> {discount}% discount diff --git a/ghost/portal/src/components/pages/OfferPage.js b/ghost/portal/src/components/pages/OfferPage.js index e1c1061303..875e8769fe 100644 --- a/ghost/portal/src/components/pages/OfferPage.js +++ b/ghost/portal/src/components/pages/OfferPage.js @@ -87,7 +87,7 @@ export const OfferPageStyles = ({site}) => { min-height: 0; } -.offer .gh-portal-product-card .gh-portal-product-card-pricecontainer { +.offer .gh-portal-product-card .gh-portal-product-card-pricecontainer:not(.offer-type-trial) { margin-top: 0px; } @@ -304,8 +304,13 @@ export default class OfferPage extends React.Component { renderSubmitButton() { const {action, brandColor} = this.context; + const {pageData: offer} = this.context; let label = 'Continue'; + if (offer.type === 'trial') { + label = 'Start ' + offer.amount + '-day free trial'; + } + let isRunning = false; if (action === 'signup:running') { label = 'Sending...'; @@ -458,9 +463,7 @@ export default class OfferPage extends React.Component { renewsLabel = `Renews at ${originalPrice}.`; } if (discountDuration === 'trial') { - return ( -

Then {getCurrencySymbol(price.currency)}{formatNumber(price.amount / 100)}/{price.interval}

- ); + return null; } return (

{this.getOffAmount({offer})} off {durationLabel}. {renewsLabel}

@@ -483,9 +486,10 @@ export default class OfferPage extends React.Component { renderUpdatedTierPrice({offer, currencyClass, updatedPrice, price}) { if (offer.type === 'trial') { return ( -
+
- {offer.amount} days free + {getCurrencySymbol(price.currency)} + {formatNumber(this.renderRoundedPrice(updatedPrice))}
); diff --git a/ghost/portal/src/components/pages/SignupPage.js b/ghost/portal/src/components/pages/SignupPage.js index e88f76fad9..9a0f480ac9 100644 --- a/ghost/portal/src/components/pages/SignupPage.js +++ b/ghost/portal/src/components/pages/SignupPage.js @@ -6,7 +6,7 @@ import NewsletterSelectionPage from './NewsletterSelectionPage'; import ProductsSection from '../common/ProductsSection'; import InputForm from '../common/InputForm'; import {ValidateInputForm} from '../../utils/form'; -import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice, hasMultipleNewsletters} from '../../utils/helpers'; +import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice, hasMultipleNewsletters, hasFreeTrialTier} from '../../utils/helpers'; import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg'; const React = require('react'); @@ -193,6 +193,13 @@ footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message { margin-bottom: 12px; } +.gh-portal-free-trial-notification { + max-width: 480px; + text-align: center; + margin: 24px auto; + color: var(--grey4); +} + @media (min-width: 480px) { } @@ -448,18 +455,31 @@ class SignupPage extends React.Component { ); } + renderFreeTrialMessage() { + const {site} = this.context; + if (hasFreeTrialTier({site})) { + return ( +

After a free trial ends, you will be charged regular price for the tier you’ve chosen. You can always cancel before then.

+ ); + } + return null; + } + renderLoginMessage() { const {brandColor, onAction} = this.context; return ( -
-
Already a member?
- +
+ {this.renderFreeTrialMessage()} +
+
Already a member?
+ +
); } diff --git a/ghost/portal/src/tests/SignupFlow.test.js b/ghost/portal/src/tests/SignupFlow.test.js index e124ec1f60..b54c1d8cd8 100644 --- a/ghost/portal/src/tests/SignupFlow.test.js +++ b/ghost/portal/src/tests/SignupFlow.test.js @@ -672,7 +672,7 @@ describe('Signup', () => { expect(nameInput).toHaveValue('Jamie Larsen'); fireEvent.click(tierContainer[0]); - const labelText = popupIframeDocument.querySelector('.gh-portal-discount-label'); + const labelText = popupIframeDocument.querySelector('.gh-portal-discount-label-trial'); await waitFor(() => { expect(labelText).toBeInTheDocument(); }); diff --git a/ghost/portal/src/utils/helpers.js b/ghost/portal/src/utils/helpers.js index 33633a5b2f..adf551befa 100644 --- a/ghost/portal/src/utils/helpers.js +++ b/ghost/portal/src/utils/helpers.js @@ -367,6 +367,13 @@ export function getSiteProducts({site, pageQuery}) { return products; } +export function hasFreeTrialTier({site}) { + const tiers = getSiteProducts({site}); + return tiers.some((tier) => { + return !!tier?.trial_days; + }); +} + export function getFreeProductBenefits({site}) { const freeProduct = getFreeProduct({site}); return freeProduct?.benefits || [];