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:
parent
54480438a6
commit
d7dcef9f3a
5 changed files with 153 additions and 26 deletions
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 you’ve 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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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 || [];
|
||||
|
|
Loading…
Add table
Reference in a new issue