0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-18 02:21:47 -05:00

Free trial signup design (#258)

* Added static HTML for free trial notification

refs https://github.com/TryGhost/Team/issues/1724

* Updated tier card design

refs https://github.com/TryGhost/Team/issues/1724

* Updated tier card design

refs https://github.com/TryGhost/Team/issues/1805

* Updated card design for free trial offers

refs https://github.com/TryGhost/Team/issues/1810

* Added trial days explanation

refs https://github.com/TryGhost/Team/issues/1810

* Switched class to className

refs https://github.com/TryGhost/Team/issues/1805

* Updated test

* Turned off the free trial feature flag

* Turned on the flag

Co-authored-by: Rishabh <zrishabhgarg@gmail.com>
This commit is contained in:
Djordje Vlaisavljevic 2022-08-18 17:55:56 +02:00 committed by GitHub
parent 54480438a6
commit d7dcef9f3a
5 changed files with 153 additions and 26 deletions

View file

@ -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 (
<span className="gh-portal-discount-label">{trialDays} days free</span>
);
}
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 (
<>
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-price">
<span className="amount trial-duration">{trialDays} days free</span>
<div className="gh-portal-product-card-price-trial">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount">{formatNumber(getStripeAmount(activePrice.amount))}</span>
<span className="billing-period">/{activePrice.interval}</span>
</div>
<ProductCardTrialDays trialDays={trialDays} />
</div>
{(selectedInterval === 'year' ? <YearlyDiscount discount={yearlyDiscount} /> : '')}
{(selectedInterval === 'year' ? <YearlyDiscount discount={yearlyDiscount} trialDays={trialDays} /> : '')}
<ProductCardAlternatePrice price={alternatePrice} />
</div>
<span className="after-trial-amount">Then {currencySymbol}{formatNumber(getStripeAmount(activePrice.amount))}/{activePrice.interval}</span>
{/* <span className="after-trial-amount">Then {currencySymbol}{formatNumber(getStripeAmount(activePrice.amount))}/{activePrice.interval}</span> */}
</>
);
}
return (
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-card-pricecontainer-old">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount">{formatNumber(getStripeAmount(activePrice.amount))}</span>
@ -615,7 +667,7 @@ function FreeProductCard({products, handleChooseSignup}) {
<div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
{(!hasOnlyFree ?
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-card-pricecontainer free-trial-disabled">
<div className="gh-portal-product-price">
<span className={'currency-sign' + (currencySymbol.length > 1 ? ' long' : '')}>{currencySymbol}</span>
<span className="amount">0</span>
@ -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 (
<>
<div className={cardClass} key={product.id} onClick={(e) => {
e.stopPropagation();
setSelectedProduct(product.id);
}}>
<div className='gh-portal-product-card-header'>
<h4 className="gh-portal-product-name">{product.name}</h4>
<ProductCardPrice product={product} />
</div>
<div className='gh-portal-product-card-details'>
<div className='gh-portal-product-card-detaildata'>
<div className="gh-portal-product-description">
{productDescription}
</div>
<ProductBenefitsContainer product={product} />
</div>
<div className='gh-portal-btn-product'>
<button
disabled={disabled}
className='gh-portal-btn'
onClick={(e) => {
const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct: product.id});
handleChooseSignup(e, selectedPrice.id);
}}>
{((selectedProduct === product.id && disabled) ? <LoaderIcon className='gh-portal-loadingicon' /> : (noOfProducts > 1 ? (trialDays > 0 ? 'Start ' + trialDays + '-day free trial' : 'Choose') : 'Continue'))}
</button>
</div>
</div>
</div>
</>
);
}
return (
<>
<div className={cardClass} key={product.id} onClick={(e) => {
@ -699,7 +787,7 @@ function ProductCard({product, products, selectedInterval, handleChooseSignup})
</div>
</div>
</>
);
);
}
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 (
<>
<span className="gh-portal-discount-label-trial">{discount}% discount</span>
</>
);
}
return (
<>
<span className="gh-portal-discount-label">{discount}% discount</span>

View file

@ -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 (
<p className="after-trial-amount">Then {getCurrencySymbol(price.currency)}{formatNumber(price.amount / 100)}/{price.interval}</p>
);
return null;
}
return (
<p className="footnote">{this.getOffAmount({offer})} off {durationLabel}. {renewsLabel}</p>
@ -483,9 +486,10 @@ export default class OfferPage extends React.Component {
renderUpdatedTierPrice({offer, currencyClass, updatedPrice, price}) {
if (offer.type === 'trial') {
return (
<div className="gh-portal-product-card-pricecontainer">
<div className="gh-portal-product-card-pricecontainer offer-type-trial">
<div className="gh-portal-product-price">
<span className="amount trial-duration">{offer.amount} days free</span>
<span className={'currency-sign ' + currencyClass}>{getCurrencySymbol(price.currency)}</span>
<span className="amount">{formatNumber(this.renderRoundedPrice(updatedPrice))}</span>
</div>
</div>
);

View file

@ -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 (
<p className='gh-portal-free-trial-notification'>After a free trial ends, you will be charged regular price for the tier youve chosen. You can always cancel before then.</p>
);
}
return null;
}
renderLoginMessage() {
const {brandColor, onAction} = this.context;
return (
<div className='gh-portal-signup-message'>
<div>Already a member?</div>
<button
className='gh-portal-btn gh-portal-btn-link'
style={{color: brandColor}}
onClick={() => onAction('switchPage', {page: 'signin'})}
>
<span>Sign in</span>
</button>
<div>
{this.renderFreeTrialMessage()}
<div className='gh-portal-signup-message'>
<div>Already a member?</div>
<button
className='gh-portal-btn gh-portal-btn-link'
style={{color: brandColor}}
onClick={() => onAction('switchPage', {page: 'signin'})}
>
<span>Sign in</span>
</button>
</div>
</div>
);
}

View file

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

View file

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