mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-01 02:41:39 -05:00
Fixed copy in Portal when signup is not available (#21965)
ref https://linear.app/ghost/issue/ENG-1235 - we currently have three different messages when signup is not available (this site is invite-only, this site only accepts paid memebers, membership unavailable); the first two offer a link to sign in, whereas the third one does not as all membership features are disabled - this PR fixes the logic to render the correct message, given the reason why signup is not available - also removes the usage of `allowSelfSignup` in Portal, as 1) the naming is poor and 2) `allowSelfSignup` is computed based on the existing `membersSignupAccess` and is therefore redundant
This commit is contained in:
parent
b6fe724b57
commit
1fd2175a44
10 changed files with 135 additions and 50 deletions
|
@ -397,8 +397,6 @@ export default class App extends React.Component {
|
|||
currency = currencyValue;
|
||||
} else if (key === 'disableBackground') {
|
||||
data.site.disableBackground = JSON.parse(value);
|
||||
} else if (key === 'allowSelfSignup') {
|
||||
data.site.allow_self_signup = JSON.parse(value);
|
||||
} else if (key === 'membersSignupAccess' && value) {
|
||||
data.site.members_signup_access = value;
|
||||
} else if (key === 'portalDefaultPlan' && value) {
|
||||
|
|
|
@ -999,7 +999,7 @@ const MobileStyles = `
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.preview .gh-portal-invite-only-notification + .gh-portal-signup-message {
|
||||
.preview .gh-portal-invite-only-notification + .gh-portal-signup-message, .preview .gh-portal-paid-members-only-notification + .gh-portal-signup-message {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ import CloseButton from '../common/CloseButton';
|
|||
import AppContext from '../../AppContext';
|
||||
import InputForm from '../common/InputForm';
|
||||
import {ValidateInputForm} from '../../utils/form';
|
||||
import {isSigninAllowed} from '../../utils/helpers';
|
||||
import {hasAvailablePrices, isSigninAllowed, isSignupAllowed} from '../../utils/helpers';
|
||||
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||
|
||||
export default class SigninPage extends React.Component {
|
||||
|
@ -131,6 +131,7 @@ export default class SigninPage extends React.Component {
|
|||
|
||||
renderForm() {
|
||||
const {site, t} = this.context;
|
||||
const isSignupAvailable = isSignupAllowed({site}) && hasAvailablePrices({site});
|
||||
|
||||
if (!isSigninAllowed({site})) {
|
||||
return (
|
||||
|
@ -158,7 +159,7 @@ export default class SigninPage extends React.Component {
|
|||
</div>
|
||||
<footer className='gh-portal-signin-footer'>
|
||||
{this.renderSubmitButton()}
|
||||
{this.renderSignupMessage()}
|
||||
{isSignupAvailable && this.renderSignupMessage()}
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@ import NewsletterSelectionPage from './NewsletterSelectionPage';
|
|||
import ProductsSection from '../common/ProductsSection';
|
||||
import InputForm from '../common/InputForm';
|
||||
import {ValidateInputForm} from '../../utils/form';
|
||||
import {getSiteProducts, getSitePrices, hasAvailablePrices, hasOnlyFreePlan, isInviteOnly, isFreeSignupAllowed, isPaidMembersOnly, freeHasBenefitsOrDescription, hasMultipleNewsletters, hasFreeTrialTier, isSignupAllowed} from '../../utils/helpers';
|
||||
import {getSiteProducts, getSitePrices, hasAvailablePrices, hasOnlyFreePlan, isInviteOnly, isFreeSignupAllowed, isPaidMembersOnly, freeHasBenefitsOrDescription, hasMultipleNewsletters, hasFreeTrialTier, isSignupAllowed, isSigninAllowed} from '../../utils/helpers';
|
||||
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||
import {interceptAnchorClicks} from '../../utils/links';
|
||||
|
||||
|
@ -177,7 +177,7 @@ footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message {
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.gh-portal-invite-only-notification, .gh-portal-members-disabled-notification {
|
||||
.gh-portal-invite-only-notification, .gh-portal-members-disabled-notification, .gh-portal-paid-members-only-notification {
|
||||
margin: 8px 32px 24px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
|
@ -194,7 +194,7 @@ footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message {
|
|||
padding-bottom: 32px;
|
||||
}
|
||||
|
||||
.gh-portal-invite-only-notification + .gh-portal-signup-message {
|
||||
.gh-portal-invite-only-notification + .gh-portal-signup-message, .gh-portal-paid-members-only-notification + .gh-portal-signup-message {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
|
@ -670,6 +670,7 @@ class SignupPage extends React.Component {
|
|||
<div>{t('Already a member?')}</div>
|
||||
<button
|
||||
data-test-button='signin-switch'
|
||||
data-testid='signin-switch'
|
||||
className='gh-portal-btn gh-portal-btn-link'
|
||||
style={{color: brandColor}}
|
||||
onClick={() => onAction('switchPage', {page: 'signin'})}
|
||||
|
@ -698,24 +699,23 @@ class SignupPage extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
if (!hasAvailablePrices({site, pageQuery})) {
|
||||
if (isPaidMembersOnly({site})) {
|
||||
return this.renderPaidMembersOnlyMessage();
|
||||
}
|
||||
|
||||
if (isInviteOnly({site})) {
|
||||
return this.renderInviteOnlyMessage();
|
||||
}
|
||||
|
||||
return this.renderMembersDisabledMessage();
|
||||
// Invite-only site: block signups, offer to sign in
|
||||
if (isInviteOnly({site})) {
|
||||
return this.renderInviteOnlyMessage();
|
||||
}
|
||||
|
||||
if (pageQuery === 'free' && !isFreeSignupAllowed({site})) {
|
||||
// Paid-members-only site: block free signups, offer to sign in
|
||||
if (isPaidMembersOnly({site}) && pageQuery === 'free') {
|
||||
return this.renderPaidMembersOnlyMessage();
|
||||
}
|
||||
|
||||
if (!isSignupAllowed({site})) {
|
||||
return this.renderMembersDisabledMessage();
|
||||
// Signup is not allowed or no prices are available: block signup with the relevant message, offer signin when available
|
||||
if (!isSignupAllowed({site}) || !hasAvailablePrices({site, pageQuery})) {
|
||||
if (!isSigninAllowed({site})) {
|
||||
return this.renderMembersDisabledMessage();
|
||||
}
|
||||
|
||||
return this.renderInviteOnlyMessage();
|
||||
}
|
||||
|
||||
const showOnlyFree = pageQuery === 'free' && isFreeSignupAllowed({site});
|
||||
|
@ -773,8 +773,8 @@ class SignupPage extends React.Component {
|
|||
<section>
|
||||
<div className='gh-portal-section'>
|
||||
<p
|
||||
className='gh-portal-invite-only-notification'
|
||||
data-testid="invite-only-notification-text"
|
||||
className='gh-portal-paid-members-only-notification'
|
||||
data-testid="paid-members-only-notification-text"
|
||||
>
|
||||
{t('This site only accepts paid members.')}
|
||||
</p>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import SignupPage from './SignupPage';
|
||||
import {getFreeProduct, getProductData, getSiteData} from '../../utils/fixtures-generator';
|
||||
import {render, fireEvent, getByTestId} from '../../utils/test-utils';
|
||||
import {render, fireEvent, getByTestId, queryByTestId} from '../../utils/test-utils';
|
||||
|
||||
const setup = (overrides) => {
|
||||
const {mockOnActionFn, ...utils} = render(
|
||||
|
@ -115,4 +115,101 @@ describe('SignupPage', () => {
|
|||
expect(message).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when site is invite-only', () => {
|
||||
test('blocks signups but offers to sign in', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'invite'
|
||||
})
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'invite-only-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = getByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when site is paid-members only', () => {
|
||||
test('blocks the #/portal/signup/free page, but offers to sign in', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'paid'
|
||||
}),
|
||||
pageQuery: 'free'
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'paid-members-only-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = getByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('blocks signups when only the free plan is available, but offers to sign in', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'paid',
|
||||
products: [getFreeProduct({})]
|
||||
})
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'invite-only-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = getByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('blocks signups when no plans are available, but offers to sign in', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'paid',
|
||||
products: []
|
||||
})
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'invite-only-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = getByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when site has memberships disabled', () => {
|
||||
test('blocks signups and signins', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'none'
|
||||
})
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'members-disabled-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = queryByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when site is anyone-can-signup, but has no available prices', () => {
|
||||
test('blocks signups, but allows signins', () => {
|
||||
setup({
|
||||
site: getSiteData({
|
||||
membersSignupAccess: 'all',
|
||||
products: [],
|
||||
portalPlans: []
|
||||
})
|
||||
});
|
||||
|
||||
const message = getByTestId(document.body, 'invite-only-notification-text');
|
||||
expect(message).toBeInTheDocument();
|
||||
|
||||
const signinLink = getByTestId(document.body, 'signin-switch');
|
||||
expect(signinLink).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ export const TipsAndDonationsSuccessStyle = `
|
|||
height: 48px;
|
||||
}
|
||||
|
||||
.gh-portal-tips-and-donations .gh-tips-and-donations-icon-success svg {
|
||||
.gh-portal-tips-and-donations .gh-tips-and-donations-icon-success svg {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
@ -64,6 +64,7 @@ const SupportSuccess = () => {
|
|||
<div>{t('Already a member?')}</div>
|
||||
<button
|
||||
data-test-button='signin-switch'
|
||||
data-testid='signin-switch'
|
||||
className='gh-portal-btn gh-portal-btn-link'
|
||||
style={{color: brandColor}}
|
||||
onClick={() => onAction('switchPage', {page: 'signin'})}
|
||||
|
|
|
@ -837,7 +837,7 @@ describe('Signup', () => {
|
|||
popupFrame, emailInput,
|
||||
freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, fullAccessTitle
|
||||
} = await setup({
|
||||
site: {...FixtureSite.singleTier.onlyFreePlan, allow_self_signup: false, members_signup_access: 'paid'}
|
||||
site: {...FixtureSite.singleTier.onlyFreePlan, members_signup_access: 'paid'}
|
||||
});
|
||||
|
||||
expect(popupFrame).toBeInTheDocument();
|
||||
|
@ -863,7 +863,7 @@ describe('Signup', () => {
|
|||
popupFrame, emailInput, nameInput,
|
||||
freePlanTitle, monthlyPlanTitle, yearlyPlanTitle, chooseBtns
|
||||
} = await setup({
|
||||
site: {...FixtureSite.multipleTiers.basic, allow_self_signup: false, members_signup_access: 'paid'}
|
||||
site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'}
|
||||
});
|
||||
|
||||
expect(popupFrame).toBeInTheDocument();
|
||||
|
|
|
@ -139,19 +139,19 @@ describe('Portal Data links:', () => {
|
|||
|
||||
describe('on a paid-members only site', () => {
|
||||
describe('with only a free plan', () => {
|
||||
test('renders paid-members only message and does not allow signups', async () => {
|
||||
test('renders invite-only message and does not allow signups', async () => {
|
||||
window.location.hash = '#/portal/signup';
|
||||
let {
|
||||
popupFrame
|
||||
} = await setup({
|
||||
site: {...FixtureSite.singleTier.onlyFreePlan, allow_self_signup: false, members_signup_access: 'paid'},
|
||||
site: {...FixtureSite.singleTier.onlyFreePlan, members_signup_access: 'paid'},
|
||||
member: null
|
||||
});
|
||||
|
||||
expect(popupFrame).toBeInTheDocument();
|
||||
|
||||
const paidMembersOnlyMessage = within(popupFrame.contentDocument).queryByText(/This site only accepts paid members/i);
|
||||
expect(paidMembersOnlyMessage).toBeInTheDocument();
|
||||
const inviteOnlyMessage = within(popupFrame.contentDocument).queryByText(/This site is invite-only/i);
|
||||
expect(inviteOnlyMessage).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -164,7 +164,7 @@ describe('Portal Data links:', () => {
|
|||
popupFrame
|
||||
|
||||
} = await setup({
|
||||
site: {...FixtureSite.multipleTiers.basic, allow_self_signup: false, members_signup_access: 'paid'},
|
||||
site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'},
|
||||
member: null
|
||||
});
|
||||
|
||||
|
@ -215,7 +215,7 @@ describe('Portal Data links:', () => {
|
|||
let {
|
||||
popupFrame
|
||||
} = await setup({
|
||||
site: {...FixtureSite.multipleTiers.basic, allow_self_signup: false, members_signup_access: 'paid'},
|
||||
site: {...FixtureSite.multipleTiers.basic, members_signup_access: 'paid'},
|
||||
member: null
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ export function getSiteData({
|
|||
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',
|
||||
|
@ -56,7 +55,6 @@ export function getSiteData({
|
|||
plans,
|
||||
products,
|
||||
portal_products: portalProducts,
|
||||
allow_self_signup,
|
||||
members_signup_access,
|
||||
free_price_name,
|
||||
free_price_description,
|
||||
|
|
|
@ -313,10 +313,6 @@ export function transformApiSiteData({site}) {
|
|||
|
||||
site.is_stripe_configured = !!site.paid_members_enabled;
|
||||
|
||||
if (site.allow_self_signup === undefined) {
|
||||
site.allow_self_signup = site.members_signup_access === 'all';
|
||||
}
|
||||
|
||||
// Map tier visibility to old settings
|
||||
if (site.products?.[0]?.visibility) {
|
||||
// Map paid tier visibility to portal products
|
||||
|
@ -500,11 +496,9 @@ export function getPricesFromProducts({site = null, products = null}) {
|
|||
}
|
||||
|
||||
export function hasFreeProductPrice({site}) {
|
||||
const {
|
||||
allow_self_signup: allowSelfSignup,
|
||||
portal_plans: portalPlans
|
||||
} = site || {};
|
||||
return allowSelfSignup && portalPlans.includes('free');
|
||||
const {portal_plans: portalPlans} = site || {};
|
||||
|
||||
return isFreeSignupAllowed({site}) && portalPlans.includes('free');
|
||||
}
|
||||
|
||||
export function getSiteNewsletters({site}) {
|
||||
|
@ -639,14 +633,9 @@ export function getFreePriceCurrency({site}) {
|
|||
}
|
||||
|
||||
export function getSitePrices({site = {}, pageQuery = ''} = {}) {
|
||||
const {
|
||||
allow_self_signup: allowSelfSignup,
|
||||
portal_plans: portalPlans
|
||||
} = site || {};
|
||||
|
||||
const plansData = [];
|
||||
|
||||
if (allowSelfSignup && portalPlans.includes('free')) {
|
||||
if (hasFreeProductPrice({site})) {
|
||||
const freePriceCurrencyDetail = getFreePriceCurrency({site});
|
||||
plansData.push({
|
||||
id: 'free',
|
||||
|
@ -666,6 +655,7 @@ export function getSitePrices({site = {}, pageQuery = ''} = {}) {
|
|||
plansData.push(price);
|
||||
});
|
||||
}
|
||||
|
||||
return plansData;
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue