mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-25 02:31:59 -05:00
Added description and benefits for free tier (#210)
refs https://github.com/TryGhost/Team/issues/1037 Free tier is now setup the same way as other tiers, to allow custom description/benefits. This change - - adds custom description and benefits UI for free tier when tiers beta is enabled - updates fixtures structure - fixes react overlay error for fast refresh Co-authored-by: Peter Zimon <peter.zimon@gmail.com>
This commit is contained in:
parent
542e585f89
commit
7cf843d3c7
8 changed files with 607 additions and 374 deletions
|
@ -85,5 +85,9 @@
|
|||
"serve-handler": "6.1.3",
|
||||
"source-map-explorer": "2.5.2",
|
||||
"webpack-cli": "3.3.12"
|
||||
},
|
||||
"resolutions": {
|
||||
"//": "See https://github.com/facebook/create-react-app/issues/11773",
|
||||
"react-error-overlay": "6.0.9"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {useContext} from 'react';
|
|||
import AppContext from '../../AppContext';
|
||||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
|
||||
import calculateDiscount from '../../utils/discount';
|
||||
import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getProductBenefits} from '../../utils/helpers';
|
||||
import {isCookiesDisabled, formatNumber, hasOnlyFreePlan, hasMultipleProductsFeature, getProductBenefits, getFreeTierDescription, getFreeTierTitle, getFreeProductBenefits, getProductFromPrice} from '../../utils/helpers';
|
||||
import ProductsSection, {ChangeProductSection} from './ProductsSection';
|
||||
|
||||
export const PlanSectionStyles = `
|
||||
|
@ -68,7 +68,7 @@ export const PlanSectionStyles = `
|
|||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.has-multiple-products .gh-portal-plan-section::before {
|
||||
.gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) .gh-portal-plan-section::before {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
@ -371,7 +371,7 @@ export const PlanSectionStyles = `
|
|||
border: none;
|
||||
}
|
||||
|
||||
.gh-portal-plans-container.has-multiple-products {
|
||||
.gh-portal-plans-container.has-multiple-products:not(.empty-selected-benefits) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
@ -422,12 +422,22 @@ export const PlanSectionStyles = `
|
|||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
.gh-portal-singleproduct-benefits.onlyfree {
|
||||
border-top: 1px solid var(--grey11) !important;
|
||||
border-radius: 5px;
|
||||
margin-top: 30px !important;
|
||||
}
|
||||
|
||||
.gh-portal-singleproduct-benefits .gh-portal-product-benefit {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.gh-portal-singleproduct-benefits .gh-portal-product-benefit:last-of-type {
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.gh-portal-singleproduct-benefits.onlyfree .gh-portal-product-benefit:last-of-type {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.gh-portal-singleproduct-benefits:not(.no-benefits) .gh-portal-product-description {
|
||||
|
@ -489,21 +499,28 @@ function PlanOptions({plans, selectedPlan, onPlanSelect, changePlan}) {
|
|||
const {site} = useContext(AppContext);
|
||||
addDiscountToPlans(plans);
|
||||
|
||||
return plans.map(({name, currency_symbol: currencySymbol, amount, description, interval, id}) => {
|
||||
return plans.map(({name, type, currency_symbol: currencySymbol, amount, description, interval, id}) => {
|
||||
const price = amount / 100;
|
||||
const isChecked = selectedPlan === id;
|
||||
const planDetails = {};
|
||||
let displayName = name;
|
||||
switch (name) {
|
||||
case 'Free':
|
||||
displayName = name;
|
||||
if (type === 'free') {
|
||||
displayName = getFreeTierTitle({site});
|
||||
planDetails.feature = 'Free preview';
|
||||
break;
|
||||
default:
|
||||
} else {
|
||||
displayName = interval === 'month' ? 'Monthly' : 'Yearly';
|
||||
planDetails.feature = description || 'Full access';
|
||||
break;
|
||||
}
|
||||
// switch (name) {
|
||||
// case 'Free':
|
||||
// displayName = getFreeTierTitle({site});
|
||||
// planDetails.feature = 'Free preview';
|
||||
// break;
|
||||
// default:
|
||||
// displayName = interval === 'month' ? 'Monthly' : 'Yearly';
|
||||
// planDetails.feature = description || 'Full access';
|
||||
// break;
|
||||
// }
|
||||
|
||||
let planClass = isChecked ? 'gh-portal-plan-section checked' : 'gh-portal-plan-section';
|
||||
const planNameClass = planDetails.feature ? 'gh-portal-plan-name' : 'gh-portal-plan-name no-description';
|
||||
|
@ -560,13 +577,10 @@ function PlanBenefits({product, plans, selectedPlan}) {
|
|||
return _plan.id === selectedPlan;
|
||||
});
|
||||
let planBenefits = [];
|
||||
let planDescription = product.description;
|
||||
if (!product.description) {
|
||||
planDescription = `Full access to ` + site.title;
|
||||
}
|
||||
let planDescription = product?.description || '';
|
||||
if (selectedPlan === 'free') {
|
||||
planBenefits = [];
|
||||
planDescription = `Free preview of ` + site.title;
|
||||
planBenefits = getFreeProductBenefits({site});
|
||||
planDescription = getFreeTierDescription({site});
|
||||
} else if (plan?.interval === 'month') {
|
||||
planBenefits = productBenefits.monthly;
|
||||
} else if (plan?.interval === 'year') {
|
||||
|
@ -579,12 +593,18 @@ function PlanBenefits({product, plans, selectedPlan}) {
|
|||
);
|
||||
});
|
||||
|
||||
let benefitsClass = (selectedPlan === 'free') ? `no-benefits` : ``;
|
||||
benefitsClass = benefits.length === 0 ? `no-benefits` : ``;
|
||||
if (!planDescription && benefits.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let benefitsClass = benefits.length === 0 ? `no-benefits` : '';
|
||||
if (!product || hasOnlyFreePlan({plans})) {
|
||||
benefitsClass += ' onlyfree';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'gh-portal-singleproduct-benefits gh-portal-product-benefits ' + benefitsClass}>
|
||||
<div className='gh-portal-product-description'> {planDescription} </div>
|
||||
{planDescription ? <div className='gh-portal-product-description'> {planDescription} </div> : ''}
|
||||
{benefits}
|
||||
</div>
|
||||
);
|
||||
|
@ -599,7 +619,14 @@ function PlanLabel({showLabel}) {
|
|||
);
|
||||
}
|
||||
|
||||
function getPlanClassNames({changePlan, cookiesDisabled, plans = [], showVertical = false, site}) {
|
||||
function productHasDescriptionOrBenefits({product}) {
|
||||
if (product?.description || product?.benefits?.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getPlanClassNames({changePlan, cookiesDisabled, plans = [], selectedPlan, showVertical = false, site}) {
|
||||
let className = 'gh-portal-plans-container';
|
||||
if (changePlan) {
|
||||
className += ' hide-checkbox';
|
||||
|
@ -612,6 +639,11 @@ function getPlanClassNames({changePlan, cookiesDisabled, plans = [], showVertica
|
|||
}
|
||||
if (hasMultipleProductsFeature({site})) {
|
||||
className += ' has-multiple-products';
|
||||
const selectedProduct = getProductFromPrice({site, priceId: selectedPlan});
|
||||
|
||||
if (!productHasDescriptionOrBenefits({product: selectedProduct})) {
|
||||
className += ' empty-selected-benefits';
|
||||
}
|
||||
|
||||
const filteredPlans = plans.filter(d => d.id !== 'free');
|
||||
const monthlyPlan = plans.find((d) => {
|
||||
|
@ -668,19 +700,23 @@ export function MultipleProductsPlansSection({products, selectedPlan, onPlanSele
|
|||
|
||||
export function SingleProductPlansSection({product, plans, selectedPlan, onPlanSelect, changePlan = false}) {
|
||||
const {site} = useContext(AppContext);
|
||||
if (!product || hasOnlyFreePlan({plans})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cookiesDisabled = isCookiesDisabled();
|
||||
/**Don't allow plans selection if cookies are disabled */
|
||||
if (cookiesDisabled) {
|
||||
onPlanSelect = () => {};
|
||||
}
|
||||
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, site});
|
||||
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site});
|
||||
|
||||
if (!product || hasOnlyFreePlan({plans})) {
|
||||
return (
|
||||
<section>
|
||||
<PlanBenefits product={product} plans={plans} selectedPlan={selectedPlan} />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="gh-portal-plans mt8">
|
||||
<section>
|
||||
<div className={className}>
|
||||
<PlanOptions plans={plans} onPlanSelect={onPlanSelect} selectedPlan={selectedPlan} changePlan={changePlan} />
|
||||
</div>
|
||||
|
@ -758,7 +794,7 @@ function PlansSection({plans, showLabel = true, selectedPlan, onPlanSelect, chan
|
|||
if (cookiesDisabled) {
|
||||
onPlanSelect = () => {};
|
||||
}
|
||||
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, site});
|
||||
const className = getPlanClassNames({cookiesDisabled, changePlan, plans, selectedPlan, site});
|
||||
return (
|
||||
<section className="gh-portal-plans">
|
||||
<PlanLabel showLabel={showLabel} />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import Switch from '../common/Switch';
|
||||
import {ReactComponent as CheckmarkIcon} from '../../images/icons/checkmark.svg';
|
||||
import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getMemberActivePrice, getProductFromPrice} from '../../utils/helpers';
|
||||
import {getSiteProducts, getCurrencySymbol, getPriceString, getStripeAmount, isCookiesDisabled, getMemberActivePrice, getProductFromPrice, getFreeTierTitle, getFreeTierDescription, getFreeProduct} from '../../utils/helpers';
|
||||
import AppContext from '../../AppContext';
|
||||
import ActionButton from './ActionButton';
|
||||
import calculateDiscount from '../../utils/discount';
|
||||
|
@ -204,7 +204,7 @@ export const ProductsSectionStyles = ({site}) => {
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.gh-portal-product-card:not(.free) .gh-portal-product-description {
|
||||
.gh-portal-product-card .gh-portal-product-description {
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
@ -658,7 +658,8 @@ function FreeProductCard() {
|
|||
const {selectedProduct, selectedInterval, setSelectedProduct} = useContext(ProductsContext);
|
||||
|
||||
const cardClass = selectedProduct === 'free' ? 'gh-portal-product-card free checked' : 'gh-portal-product-card free';
|
||||
|
||||
const product = getFreeProduct({site});
|
||||
const freeProductDescription = getFreeTierDescription({site});
|
||||
// Product cards are duplicated because their design is too different for mobile devices to handle it purely in CSS
|
||||
return (
|
||||
<>
|
||||
|
@ -671,8 +672,9 @@ function FreeProductCard() {
|
|||
<Checkbox name='x' id='x' isChecked={selectedProduct === 'free'} onProductSelect={() => {
|
||||
setSelectedProduct('free');
|
||||
}} />
|
||||
<h4 className="gh-portal-product-name">Free</h4>
|
||||
<div className="gh-portal-product-description">Free preview of {(site.title)}</div>
|
||||
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
|
||||
{freeProductDescription ? <div className="gh-portal-product-description">{freeProductDescription}</div> : ''}
|
||||
<ProductBenefitsContainer product={product} />
|
||||
</div>
|
||||
<div className="gh-portal-product-card-pricecontainer">
|
||||
<div className="gh-portal-product-price">
|
||||
|
@ -691,13 +693,14 @@ function FreeProductCard() {
|
|||
<Checkbox name='x' id='x' isChecked={selectedProduct === 'free'} onProductSelect={() => {
|
||||
setSelectedProduct('free');
|
||||
}} />
|
||||
<h4 className="gh-portal-product-name">Free</h4>
|
||||
<h4 className="gh-portal-product-name">{getFreeTierTitle({site})}</h4>
|
||||
<div className="gh-portal-product-price">
|
||||
<span className="currency-sign">$</span>
|
||||
<span className="amount">0</span>
|
||||
<span className="billing-period">/{selectedInterval}</span>
|
||||
</div>
|
||||
<div className="gh-portal-product-description">Free preview of {(site.title)}</div>
|
||||
{freeProductDescription ? <div className="gh-portal-product-description">{freeProductDescription}</div> : ''}
|
||||
<ProductBenefitsContainer product={product} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import PlansSection, {SingleProductPlansSection} from '../common/PlansSection';
|
|||
import ProductsSection from '../common/ProductsSection';
|
||||
import InputForm from '../common/InputForm';
|
||||
import {ValidateInputForm} from '../../utils/form';
|
||||
import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite, getAvailableProducts, hasMultipleProductsFeature} from '../../utils/helpers';
|
||||
import {getSiteProducts, getSitePrices, hasMultipleProducts, hasOnlyFreePlan, isInviteOnlySite, getAvailableProducts, hasMultipleProductsFeature, freeHasBenefitsOrDescription} from '../../utils/helpers';
|
||||
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||
|
||||
const React = require('react');
|
||||
|
@ -87,18 +87,6 @@ export const SignupPageStyles = `
|
|||
height: unset;
|
||||
}
|
||||
|
||||
/* Needed to cover small horizontal line glitch by the scroll shadow */
|
||||
footer.gh-portal-signup-footer::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -2px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: #fff;
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.gh-portal-content.signup,
|
||||
.gh-portal-content.signin {
|
||||
max-height: unset !important;
|
||||
|
@ -231,7 +219,7 @@ export const SignupPageStyles = `
|
|||
|
||||
@media (min-width: 480px) and (max-width: 820px) {
|
||||
.gh-portal-powered.outside {
|
||||
left: 50%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
|
@ -563,7 +551,7 @@ class SignupPage extends React.Component {
|
|||
|
||||
if (plansData.length <= 1 || isInviteOnlySite({site})) {
|
||||
if ((plansData.length === 1 && plansData[0].type === 'free') || isInviteOnlySite({site, pageQuery})) {
|
||||
sectionClass = 'noplan';
|
||||
sectionClass = freeHasBenefitsOrDescription({site}) ? 'singleplan' : 'noplan';
|
||||
if (fields.length === 1) {
|
||||
sectionClass = 'single-field';
|
||||
}
|
||||
|
|
384
ghost/portal/src/utils/fixtures-generator.js
Normal file
384
ghost/portal/src/utils/fixtures-generator.js
Normal file
|
@ -0,0 +1,384 @@
|
|||
export const sites = {
|
||||
singleProduct: getSiteData({
|
||||
products: getProductsData({numOfProducts: 1})
|
||||
})
|
||||
};
|
||||
|
||||
export function objectId() {
|
||||
const timestamp = (new Date().getTime() / 1000 | 0).toString(16);
|
||||
return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () {
|
||||
return (Math.random() * 16 | 0).toString(16);
|
||||
}).toLowerCase();
|
||||
}
|
||||
|
||||
export function getSiteData({
|
||||
title = 'The Blueprint',
|
||||
description = 'Thoughts, stories and ideas.',
|
||||
logo = 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
icon = 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
url = 'https://portal.localhost',
|
||||
plans = {
|
||||
monthly: 5000,
|
||||
yearly: 150000,
|
||||
currency: 'USD'
|
||||
},
|
||||
products = getProductsData({numOfProducts: 1}),
|
||||
portalProducts = products.map(p => p.id),
|
||||
accentColor: accent_color = '#45C32E',
|
||||
portalPlans: portal_plans = ['free', 'monthly', 'yearly'],
|
||||
allowSelfSignup: allow_self_signup = true,
|
||||
membersSignupAccess: members_signup_access = 'all',
|
||||
freePriceName: free_price_name = 'Free',
|
||||
freePriceDescription: free_price_description = 'Free preview',
|
||||
isStripeConfigured: is_stripe_configured = true,
|
||||
portalButton: portal_button = true,
|
||||
portalName: portal_name = true,
|
||||
portalButtonIcon: portal_button_icon = 'icon-1',
|
||||
portalButtonSignupText: portal_button_signup_text = 'Subscribe now',
|
||||
portalButtonStyle: portal_button_style = 'icon-and-text',
|
||||
membersSupportAddress: members_support_address = 'support@example.com'
|
||||
} = {}) {
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
logo,
|
||||
icon,
|
||||
accent_color,
|
||||
url,
|
||||
plans,
|
||||
products,
|
||||
portal_products: portalProducts,
|
||||
allow_self_signup,
|
||||
members_signup_access,
|
||||
free_price_name,
|
||||
free_price_description,
|
||||
is_stripe_configured,
|
||||
portal_button,
|
||||
portal_name,
|
||||
portal_plans,
|
||||
portal_button_icon,
|
||||
portal_button_signup_text,
|
||||
portal_button_style,
|
||||
members_support_address
|
||||
};
|
||||
}
|
||||
|
||||
export function getOfferData({
|
||||
name = 'Black Friday',
|
||||
code = 'black-friday',
|
||||
displayTitle = 'Black Friday',
|
||||
displayDescription = 'Special deal',
|
||||
type = 'percent',
|
||||
cadence = 'month',
|
||||
amount = 50,
|
||||
duration = 'repeating',
|
||||
durationInMonths = null,
|
||||
currencyRestriction = false,
|
||||
currency = null,
|
||||
status = 'active',
|
||||
tierId = '',
|
||||
tierName = 'Basic'
|
||||
} = {}) {
|
||||
return {
|
||||
id: `offer_${objectId()}`,
|
||||
name,
|
||||
code,
|
||||
display_title: displayTitle,
|
||||
display_description: displayDescription,
|
||||
type,
|
||||
cadence,
|
||||
amount,
|
||||
duration,
|
||||
duration_in_months: durationInMonths,
|
||||
currency_restriction: currencyRestriction,
|
||||
currency,
|
||||
status,
|
||||
tier: {
|
||||
id: `${tierId}`,
|
||||
name: tierName
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getMemberData({
|
||||
name = 'Jamie Larson',
|
||||
email = 'jamie@example.com',
|
||||
firstname = 'Jamie',
|
||||
subscriptions = [],
|
||||
paid = false,
|
||||
avatarImage: avatar_image = '',
|
||||
subscribed = true
|
||||
} = {}) {
|
||||
return {
|
||||
uuid: `member_${objectId()}`,
|
||||
email,
|
||||
name,
|
||||
firstname,
|
||||
paid,
|
||||
subscribed,
|
||||
avatar_image,
|
||||
subscriptions
|
||||
};
|
||||
}
|
||||
|
||||
export function getProductsData({numOfProducts = 3} = {}) {
|
||||
const products = [
|
||||
getProductData({
|
||||
name: 'Bronze',
|
||||
description: 'Access to all members articles',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 700
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 7000
|
||||
}),
|
||||
numOfBenefits: 2
|
||||
}),
|
||||
getProductData({
|
||||
name: 'Silver',
|
||||
description: 'Access to all members articles and weekly podcast',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 1200
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 12000
|
||||
}),
|
||||
numOfBenefits: 3
|
||||
}),
|
||||
getProductData({
|
||||
name: 'Friends of the Blueprint',
|
||||
description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 18000
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 17000
|
||||
}),
|
||||
numOfBenefits: 4
|
||||
})
|
||||
];
|
||||
const paidProducts = products.slice(0, numOfProducts);
|
||||
const freeProduct = getFreeProduct({});
|
||||
return [
|
||||
...paidProducts,
|
||||
freeProduct
|
||||
];
|
||||
}
|
||||
|
||||
export function getProductData({
|
||||
type = 'paid',
|
||||
name = 'Basic',
|
||||
description = '',
|
||||
id = `product_${objectId()}`,
|
||||
monthlyPrice = getPriceData(),
|
||||
yearlyPrice = getPriceData({interval: 'year'}),
|
||||
numOfBenefits = 2
|
||||
}) {
|
||||
return {
|
||||
id: id,
|
||||
name: name,
|
||||
description,
|
||||
monthlyPrice: type === 'free' ? null : monthlyPrice,
|
||||
yearlyPrice: type === 'free' ? null : yearlyPrice,
|
||||
type: type,
|
||||
benefits: getBenefits({numOfBenefits})
|
||||
};
|
||||
}
|
||||
|
||||
export function getFreeProduct({
|
||||
name = 'Free tier',
|
||||
description = 'Free tier description',
|
||||
id = `product_${objectId()}`,
|
||||
numOfBenefits = 2
|
||||
}) {
|
||||
return {
|
||||
id,
|
||||
name: name,
|
||||
type: 'free',
|
||||
description,
|
||||
benefits: getBenefits({numOfBenefits})
|
||||
};
|
||||
}
|
||||
|
||||
export function getBenefits({numOfBenefits}) {
|
||||
const beenfits = [
|
||||
getBenefitData({name: 'Limited early adopter pricing'}),
|
||||
getBenefitData({name: 'Latest gear reviews'}),
|
||||
getBenefitData({name: 'Weekly email newsletter'}),
|
||||
getBenefitData({name: 'Listen to my podcast'})
|
||||
];
|
||||
return beenfits.slice(0, numOfBenefits);
|
||||
}
|
||||
|
||||
export function getBenefitData({
|
||||
id = `benefit_${objectId()}`,
|
||||
name = 'Benefit'
|
||||
}) {
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
export function getPriceData({
|
||||
interval = 'month',
|
||||
amount = (interval === 'month' ? 500 : 5000),
|
||||
nickname = interval === 'month' ? 'Monthly' : 'Yearly',
|
||||
description = null,
|
||||
currency = 'usd',
|
||||
active = true,
|
||||
id = `price_${objectId()}`
|
||||
} = {}) {
|
||||
return {
|
||||
id: id,
|
||||
active,
|
||||
nickname,
|
||||
currency,
|
||||
amount,
|
||||
interval,
|
||||
description,
|
||||
stripe_price_id: `price_${objectId()}`,
|
||||
stripe_product_id: `prod_${objectId()}`,
|
||||
type: 'recurring'
|
||||
};
|
||||
}
|
||||
|
||||
export function getSubscriptionData({
|
||||
id = `sub_${objectId()}`,
|
||||
status = 'active',
|
||||
currency = 'USD',
|
||||
interval = 'year',
|
||||
amount = (interval === 'month' ? 500 : 5000),
|
||||
nickname = (interval === 'month' ? 'Monthly' : 'Yearly'),
|
||||
cardLast4 = '4242',
|
||||
priceId: price_id = `price_${objectId()}`,
|
||||
startDate: start_date = '2021-10-05T03:18:30.000Z',
|
||||
currentPeriodEnd: current_period_end = '2022-10-05T03:18:30.000Z',
|
||||
cancelAtPeriodEnd: cancel_at_period_end = false
|
||||
} = {}) {
|
||||
return {
|
||||
id,
|
||||
customer: {
|
||||
id: `cus_${objectId()}`,
|
||||
name: 'Jamie',
|
||||
email: 'jamie@example.com'
|
||||
},
|
||||
plan: {
|
||||
id: `price_${objectId()}`,
|
||||
nickname,
|
||||
amount,
|
||||
interval,
|
||||
currency
|
||||
},
|
||||
offer,
|
||||
status,
|
||||
start_date,
|
||||
default_payment_card_last4: cardLast4,
|
||||
cancel_at_period_end,
|
||||
cancellation_reason: null,
|
||||
current_period_end,
|
||||
price: {
|
||||
id: `stripe_price_${objectId()}`,
|
||||
price_id,
|
||||
nickname,
|
||||
amount,
|
||||
interval,
|
||||
type: 'recurring',
|
||||
currency,
|
||||
product: {
|
||||
id: `stripe_prod_${objectId()}`,
|
||||
product_id: `prod_${objectId()}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getTestSite() {
|
||||
const products = getProductsData({numOfProducts: 1});
|
||||
const portalProducts = products.map(p => p.id);
|
||||
const portalPlans = ['free', 'monthly', 'yearly'];
|
||||
return getSiteData({
|
||||
products,
|
||||
portalPlans,
|
||||
portalProducts
|
||||
});
|
||||
}
|
||||
|
||||
export const testSite = getTestSite();
|
||||
|
||||
export const site = getSiteData({
|
||||
products: [getProductData({numOfBenefits: 2, type: 'free'})]
|
||||
});
|
||||
|
||||
export const offer = getOfferData({
|
||||
tierId: site.products[0]?.id
|
||||
});
|
||||
|
||||
export const member = {
|
||||
free: getMemberData(),
|
||||
paid: getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData()
|
||||
]
|
||||
}),
|
||||
complimentary: getMemberData({
|
||||
paid: true,
|
||||
subscriptions: []
|
||||
}),
|
||||
complimentaryWithSubscription: getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData({
|
||||
amount: 0
|
||||
})
|
||||
]
|
||||
}),
|
||||
preview: getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData({
|
||||
amount: 1500,
|
||||
startDate: '2019-05-01T11:42:40.000Z',
|
||||
currentPeriodEnd: '2021-06-05T11:42:40.000Z'
|
||||
})
|
||||
]
|
||||
})
|
||||
};
|
||||
export function generateAccountPlanFixture() {
|
||||
const products = getProductsData({numOfProducts: 3});
|
||||
return {
|
||||
site: getSiteData({
|
||||
portalProducts: [products[1]]
|
||||
}),
|
||||
member: member.paid
|
||||
};
|
||||
}
|
||||
|
||||
export function basic() {
|
||||
const products = getProductsData();
|
||||
const siteData = getSiteData({
|
||||
products
|
||||
});
|
||||
const defaultMemberPrice = products?.[0].monthlyPrice;
|
||||
const memberData = getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData({
|
||||
priceId: defaultMemberPrice.id,
|
||||
amount: defaultMemberPrice.amount,
|
||||
currency: defaultMemberPrice.currency
|
||||
})
|
||||
]
|
||||
});
|
||||
return {
|
||||
site: siteData,
|
||||
member: memberData
|
||||
};
|
||||
}
|
|
@ -1,290 +1,123 @@
|
|||
export const sites = {
|
||||
singleProduct: getSiteData({
|
||||
products: getProductsData({numOfProducts: 1})
|
||||
})
|
||||
};
|
||||
|
||||
function objectId() {
|
||||
const timestamp = (new Date().getTime() / 1000 | 0).toString(16);
|
||||
return timestamp + 'xxxxxxxxxxxxxxxx'.replace(/[x]/g, function () {
|
||||
return (Math.random() * 16 | 0).toString(16);
|
||||
}).toLowerCase();
|
||||
}
|
||||
|
||||
export function getSiteData({
|
||||
products = getProductsData({numOfProducts: 1}),
|
||||
portalProducts = products.map(p => p.id),
|
||||
portalPlans: portal_plans = ['free', 'monthly', 'yearly']
|
||||
} = {}) {
|
||||
return {
|
||||
title: 'The Blueprint',
|
||||
description: 'Thoughts, stories and ideas.',
|
||||
logo: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
icon: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
accent_color: '#45C32E',
|
||||
url: 'https://portal.localhost',
|
||||
plans: {
|
||||
monthly: 5000,
|
||||
yearly: 150000,
|
||||
currency: 'USD'
|
||||
},
|
||||
products,
|
||||
portal_products: portalProducts,
|
||||
allow_self_signup: true,
|
||||
members_signup_access: 'all',
|
||||
free_price_name: 'Free',
|
||||
free_price_description: 'Free preview',
|
||||
is_stripe_configured: true,
|
||||
portal_button: true,
|
||||
portal_name: true,
|
||||
portal_plans,
|
||||
portal_button_icon: 'icon-1',
|
||||
portal_button_signup_text: 'Subscribe now',
|
||||
portal_button_style: 'icon-and-text',
|
||||
members_support_address: 'support@example.com'
|
||||
};
|
||||
}
|
||||
|
||||
function getOfferData({
|
||||
name = 'Black Friday',
|
||||
code = 'black-friday',
|
||||
displayTitle = 'Black Friday',
|
||||
displayDescription = 'Special deal',
|
||||
type = 'percent',
|
||||
cadence = 'month',
|
||||
amount = 50,
|
||||
duration = 'repeating',
|
||||
durationInMonths = null,
|
||||
currencyRestriction = false,
|
||||
currency = null,
|
||||
status = 'active',
|
||||
tierId = ''
|
||||
} = {}) {
|
||||
return {
|
||||
id: `offer_${objectId()}`,
|
||||
name,
|
||||
code,
|
||||
display_title: displayTitle,
|
||||
display_description: displayDescription,
|
||||
type,
|
||||
cadence,
|
||||
amount,
|
||||
duration,
|
||||
duration_in_months: durationInMonths,
|
||||
currency_restriction: currencyRestriction,
|
||||
currency,
|
||||
status,
|
||||
tier: {
|
||||
id: `${tierId}`,
|
||||
name: 'Basic'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getMemberData({
|
||||
name = 'Jamie Larson',
|
||||
email = 'jamie@example.com',
|
||||
firstname = 'Jamie',
|
||||
subscriptions = [],
|
||||
paid = false,
|
||||
avatarImage: avatar_image = '',
|
||||
subscribed = true
|
||||
} = {}) {
|
||||
return {
|
||||
uuid: `member_${objectId()}`,
|
||||
email,
|
||||
name,
|
||||
firstname,
|
||||
paid,
|
||||
subscribed,
|
||||
avatar_image,
|
||||
subscriptions
|
||||
};
|
||||
}
|
||||
|
||||
export function getProductsData({numOfProducts = 3} = {}) {
|
||||
const products = [
|
||||
getProductData({
|
||||
name: 'Bronze',
|
||||
description: 'Access to all members articles',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 700
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 7000
|
||||
}),
|
||||
numOfBenefits: 2
|
||||
}),
|
||||
getProductData({
|
||||
name: 'Silver',
|
||||
description: 'Access to all members articles and weekly podcast',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 1200
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 12000
|
||||
}),
|
||||
numOfBenefits: 3
|
||||
}),
|
||||
getProductData({
|
||||
name: 'Friends of the Blueprint',
|
||||
description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 18000
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 17000
|
||||
}),
|
||||
numOfBenefits: 4
|
||||
})
|
||||
];
|
||||
return products.slice(0, numOfProducts);
|
||||
}
|
||||
|
||||
function getProductData({
|
||||
name = 'Basic',
|
||||
description = '',
|
||||
id = `product_${objectId()}`,
|
||||
monthlyPrice = getPriceData(),
|
||||
yearlyPrice = getPriceData({interval: 'year'}),
|
||||
numOfBenefits = 2
|
||||
}) {
|
||||
return {
|
||||
id: id,
|
||||
name: name,
|
||||
description,
|
||||
monthlyPrice,
|
||||
yearlyPrice,
|
||||
benefits: getBenefits({numOfBenefits})
|
||||
};
|
||||
}
|
||||
|
||||
function getBenefits({numOfBenefits}) {
|
||||
const beenfits = [
|
||||
getBenefitData({name: 'Limited early adopter pricing'}),
|
||||
getBenefitData({name: 'Latest gear reviews'}),
|
||||
getBenefitData({name: 'Weekly email newsletter'}),
|
||||
getBenefitData({name: 'Listen to my podcast'})
|
||||
];
|
||||
return beenfits.slice(0, numOfBenefits);
|
||||
}
|
||||
|
||||
function getBenefitData({
|
||||
id = `benefit_${objectId()}`,
|
||||
name = 'Benefit'
|
||||
}) {
|
||||
return {
|
||||
id,
|
||||
name
|
||||
};
|
||||
}
|
||||
|
||||
function getPriceData({
|
||||
interval = 'month',
|
||||
amount = (interval === 'month' ? 500 : 5000),
|
||||
nickname = interval === 'month' ? 'Monthly' : 'Yearly',
|
||||
description = null,
|
||||
currency = 'usd',
|
||||
active = true,
|
||||
id = `price_${objectId()}`
|
||||
}) {
|
||||
return {
|
||||
id: id,
|
||||
active,
|
||||
nickname,
|
||||
currency,
|
||||
amount,
|
||||
interval,
|
||||
description,
|
||||
stripe_price_id: `price_${objectId()}`,
|
||||
stripe_product_id: `prod_${objectId()}`,
|
||||
type: 'recurring'
|
||||
};
|
||||
}
|
||||
|
||||
function getSubscriptionData({
|
||||
id = `sub_${objectId()}`,
|
||||
status = 'active',
|
||||
currency = 'USD',
|
||||
interval = 'year',
|
||||
amount = (interval === 'month' ? 500 : 5000),
|
||||
nickname = (interval === 'month' ? 'Monthly' : 'Yearly'),
|
||||
cardLast4 = '4242',
|
||||
priceId: price_id = `price_${objectId()}`,
|
||||
startDate: start_date = '2021-10-05T03:18:30.000Z',
|
||||
currentPeriodEnd: current_period_end = '2022-10-05T03:18:30.000Z',
|
||||
cancelAtPeriodEnd: cancel_at_period_end = false
|
||||
} = {}) {
|
||||
return {
|
||||
id,
|
||||
customer: {
|
||||
id: `cus_${objectId()}`,
|
||||
name: 'Jamie',
|
||||
email: 'jamie@example.com'
|
||||
},
|
||||
plan: {
|
||||
id: `price_${objectId()}`,
|
||||
nickname,
|
||||
amount,
|
||||
interval,
|
||||
currency
|
||||
},
|
||||
offer,
|
||||
status,
|
||||
start_date,
|
||||
default_payment_card_last4: cardLast4,
|
||||
cancel_at_period_end,
|
||||
cancellation_reason: null,
|
||||
current_period_end,
|
||||
price: {
|
||||
id: `stripe_price_${objectId()}`,
|
||||
price_id,
|
||||
nickname,
|
||||
amount,
|
||||
interval,
|
||||
type: 'recurring',
|
||||
currency,
|
||||
product: {
|
||||
id: `stripe_prod_${objectId()}`,
|
||||
product_id: `prod_${objectId()}`
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getTestSite() {
|
||||
const products = getProductsData({numOfProducts: 1});
|
||||
const portalProducts = products.map(p => p.id);
|
||||
const portalPlans = ['free', 'monthly', 'yearly'];
|
||||
return getSiteData({
|
||||
products,
|
||||
portalPlans,
|
||||
portalProducts
|
||||
});
|
||||
}
|
||||
/* eslint-disable no-unused-vars*/
|
||||
import {getFreeProduct, getMemberData, getOfferData, getPriceData, getProductData, getSiteData, getSubscriptionData, getTestSite} from './fixtures-generator';
|
||||
|
||||
export const testSite = getTestSite();
|
||||
|
||||
const products = [
|
||||
getFreeProduct({
|
||||
name: 'Free',
|
||||
// description: 'Free tier description which is actually a pretty long description',
|
||||
description: '',
|
||||
numOfBenefits: 0
|
||||
})
|
||||
,
|
||||
getProductData({
|
||||
name: 'Bronze',
|
||||
// description: 'Access to all members articles',
|
||||
description: '',
|
||||
monthlyPrice: getPriceData({
|
||||
interval: 'month',
|
||||
amount: 700
|
||||
}),
|
||||
yearlyPrice: getPriceData({
|
||||
interval: 'year',
|
||||
amount: 7000
|
||||
}),
|
||||
numOfBenefits: 2
|
||||
})
|
||||
// ,
|
||||
// getProductData({
|
||||
// name: 'Silver',
|
||||
// description: 'Access to all members articles and weekly podcast',
|
||||
// monthlyPrice: getPriceData({
|
||||
// interval: 'month',
|
||||
// amount: 1200
|
||||
// }),
|
||||
// yearlyPrice: getPriceData({
|
||||
// interval: 'year',
|
||||
// amount: 12000
|
||||
// }),
|
||||
// numOfBenefits: 3
|
||||
// })
|
||||
// ,
|
||||
// getProductData({
|
||||
// name: 'Friends of the Blueprint',
|
||||
// description: 'Get access to everything and lock in early adopter pricing for life + listen to my podcast',
|
||||
// monthlyPrice: getPriceData({
|
||||
// interval: 'month',
|
||||
// amount: 18000
|
||||
// }),
|
||||
// yearlyPrice: getPriceData({
|
||||
// interval: 'year',
|
||||
// amount: 17000
|
||||
// }),
|
||||
// numOfBenefits: 4
|
||||
// })
|
||||
];
|
||||
|
||||
export const site = getSiteData({
|
||||
products: getProductsData({numOfProducts: 1})
|
||||
title: 'The Blueprint',
|
||||
description: 'Thoughts, stories and ideas.',
|
||||
logo: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
icon: 'https://static.ghost.org/v4.0.0/images/ghost-orb-1.png',
|
||||
accentColor: '#45C32E',
|
||||
url: 'https://portal.localhost',
|
||||
plans: {
|
||||
monthly: 5000,
|
||||
yearly: 150000,
|
||||
currency: 'USD'
|
||||
},
|
||||
|
||||
// Simulate pre-multiple-tiers state:
|
||||
// products: [products.find(d => d.type === 'paid')],
|
||||
// portalProducts: null,
|
||||
|
||||
// Simulate multiple-tiers state:
|
||||
products,
|
||||
portalProducts: products.map(p => p.id),
|
||||
|
||||
//
|
||||
allowSelfSignup: true,
|
||||
membersSignupAccess: 'all',
|
||||
freePriceName: 'Free',
|
||||
freePriceDescription: 'Free preview',
|
||||
isStripeConfigured: true,
|
||||
portalButton: true,
|
||||
portalName: true,
|
||||
portalPlans: ['free', 'monthly', 'yearly'],
|
||||
portalButtonIcon: 'icon-1',
|
||||
portalButtonSignupText: 'Subscribe now',
|
||||
portalButtonStyle: 'icon-and-text',
|
||||
membersSupportAddress: 'support@example.com'
|
||||
});
|
||||
|
||||
export const offer = getOfferData({
|
||||
tierId: site.products[0].id
|
||||
tierId: site.products[0]?.id
|
||||
});
|
||||
|
||||
export const member = {
|
||||
free: getMemberData(),
|
||||
free: getMemberData({
|
||||
name: 'Jamie Larson',
|
||||
email: 'jamie@example.com',
|
||||
firstname: 'Jamie',
|
||||
subscriptions: [],
|
||||
paid: false,
|
||||
avatarImage: '',
|
||||
subscribed: true
|
||||
}),
|
||||
paid: getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData()
|
||||
getSubscriptionData({
|
||||
status: 'active',
|
||||
currency: 'USD',
|
||||
interval: 'year',
|
||||
amount: 5000,
|
||||
cardLast4: '4242',
|
||||
startDate: '2021-10-05T03:18:30.000Z',
|
||||
currentPeriodEnd: '2022-10-05T03:18:30.000Z',
|
||||
cancelAtPeriodEnd: false
|
||||
})
|
||||
]
|
||||
}),
|
||||
complimentary: getMemberData({
|
||||
|
@ -310,34 +143,4 @@ export const member = {
|
|||
]
|
||||
})
|
||||
};
|
||||
export function generateAccountPlanFixture() {
|
||||
const products = getProductsData({numOfProducts: 3});
|
||||
return {
|
||||
site: getSiteData({
|
||||
portalProducts: [products[1]]
|
||||
}),
|
||||
member: member.paid
|
||||
};
|
||||
}
|
||||
|
||||
export function basic() {
|
||||
const products = getProductsData();
|
||||
const siteData = getSiteData({
|
||||
products
|
||||
});
|
||||
const defaultMemberPrice = products?.[0].monthlyPrice;
|
||||
const memberData = getMemberData({
|
||||
paid: true,
|
||||
subscriptions: [
|
||||
getSubscriptionData({
|
||||
priceId: defaultMemberPrice.id,
|
||||
amount: defaultMemberPrice.amount,
|
||||
currency: defaultMemberPrice.currency
|
||||
})
|
||||
]
|
||||
});
|
||||
return {
|
||||
site: siteData,
|
||||
member: memberData
|
||||
};
|
||||
}
|
||||
/* eslint-enable no-unused-vars*/
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
// import calculateDiscount from './discount';
|
||||
|
||||
export function removePortalLinkFromUrl() {
|
||||
const [path] = window.location.hash.substr(1).split('?');
|
||||
const linkRegex = /^\/portal\/?(?:\/(\w+(?:\/\w+)*))?\/?$/;
|
||||
|
@ -215,6 +213,11 @@ export function getAvailableProducts({site}) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getFreeProduct({site}) {
|
||||
const {products = []} = site || {};
|
||||
return products.find(product => product.type === 'free');
|
||||
}
|
||||
|
||||
export function getAllProductsForSite({site}) {
|
||||
const {products = [], portal_plans: portalPlans = []} = site || {};
|
||||
|
||||
|
@ -263,10 +266,27 @@ export function getSiteProducts({site}) {
|
|||
return products;
|
||||
}
|
||||
|
||||
export function getFreeBenefits() {
|
||||
return [{
|
||||
name: 'Access to free articles'
|
||||
}];
|
||||
export function getFreeProductBenefits({site}) {
|
||||
const freeProduct = getFreeProduct({site});
|
||||
return freeProduct?.benefits || [];
|
||||
}
|
||||
|
||||
export function getFreeTierTitle({site}) {
|
||||
return 'Free';
|
||||
}
|
||||
|
||||
export function getFreeTierDescription({site}) {
|
||||
const freeProduct = getFreeProduct({site});
|
||||
return freeProduct?.description;
|
||||
}
|
||||
|
||||
export function freeHasBenefitsOrDescription({site}) {
|
||||
const freeProduct = getFreeProduct({site});
|
||||
|
||||
if (freeProduct?.description || freeProduct?.benefits?.length) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getProductBenefits({product, site = null}) {
|
||||
|
@ -274,14 +294,6 @@ export function getProductBenefits({product, site = null}) {
|
|||
const productBenefits = product?.benefits || [];
|
||||
const monthlyBenefits = productBenefits;
|
||||
const yearlyBenefits = productBenefits;
|
||||
// const availablePrices = getAvailablePrices({site, products: [product]});
|
||||
// const yearlyDiscount = calculateDiscount(product.monthlyPrice.amount, product.yearlyPrice.amount);
|
||||
// if (yearlyDiscount > 0 && availablePrices.length > 1) {
|
||||
// yearlyBenefits.push({
|
||||
// name: `${yearlyDiscount}% annual discount`,
|
||||
// className: `gh-portal-strong`
|
||||
// });
|
||||
// }
|
||||
return {
|
||||
monthly: monthlyBenefits,
|
||||
yearly: yearlyBenefits
|
||||
|
@ -319,6 +331,9 @@ export function hasFreeProductPrice({site}) {
|
|||
}
|
||||
|
||||
export function getProductFromPrice({site, priceId}) {
|
||||
if (priceId === 'free') {
|
||||
return getFreeProduct({site});
|
||||
}
|
||||
const products = getAllProductsForSite({site});
|
||||
return products.find((product) => {
|
||||
return (product?.monthlyPrice?.id === priceId) || (product?.yearlyPrice?.id === priceId);
|
||||
|
@ -396,7 +411,7 @@ export function getSitePrices({site = {}, pageQuery = ''} = {}) {
|
|||
type: 'free',
|
||||
price: 0,
|
||||
amount: 0,
|
||||
name: 'Free',
|
||||
name: getFreeTierTitle({site}),
|
||||
...freePriceCurrencyDetail
|
||||
|
||||
});
|
||||
|
|
|
@ -9463,10 +9463,10 @@ react-dom@17.0.2:
|
|||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-error-overlay@^6.0.9:
|
||||
version "6.0.10"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.10.tgz#0fe26db4fa85d9dbb8624729580e90e7159a59a6"
|
||||
integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==
|
||||
react-error-overlay@6.0.9, react-error-overlay@^6.0.9:
|
||||
version "6.0.9"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a"
|
||||
integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
|
|
Loading…
Add table
Reference in a new issue