mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
Added logic to load Portal when Tips & Donations are enabled (#17659)
closes https://github.com/TryGhost/Product/issues/3661 - until now, Portal was not loaded if members were disabled. With the introduction of Tips & Donations, signed-off readers can also make payments, using the Portal link /#/portal/support. - now, Portal is loaded when Tips & Donations are enabled, even if Memberships are disabled - depending on the member signup access, the top bar / trigger button Portal buttons are hidden as before (signup/subscribe hidden if access is set to none, subscribe hidden if access is set to invite-only) - for any other signup / signin Portal links (e.g., added by the theme, or added via a Post/Page), a new popup informs the reader when Memberships are disabled: "Memberships unavailable, contact the site owner for access".
This commit is contained in:
parent
8273671425
commit
4ace11a441
12 changed files with 303 additions and 72 deletions
|
@ -9,7 +9,7 @@ import {ReactComponent as ButtonIcon3} from '../images/icons/button-icon-3.svg';
|
||||||
import {ReactComponent as ButtonIcon4} from '../images/icons/button-icon-4.svg';
|
import {ReactComponent as ButtonIcon4} from '../images/icons/button-icon-4.svg';
|
||||||
import {ReactComponent as ButtonIcon5} from '../images/icons/button-icon-5.svg';
|
import {ReactComponent as ButtonIcon5} from '../images/icons/button-icon-5.svg';
|
||||||
import TriggerButtonStyle from './TriggerButton.styles';
|
import TriggerButtonStyle from './TriggerButton.styles';
|
||||||
import {isInviteOnlySite} from '../utils/helpers';
|
import {isInviteOnlySite, isSigninAllowed} from '../utils/helpers';
|
||||||
import {hasMode} from '../utils/check-mode';
|
import {hasMode} from '../utils/check-mode';
|
||||||
|
|
||||||
const ICON_MAPPING = {
|
const ICON_MAPPING = {
|
||||||
|
@ -164,12 +164,21 @@ class TriggerButtonContent extends React.Component {
|
||||||
|
|
||||||
onToggle() {
|
onToggle() {
|
||||||
const {showPopup, member, site} = this.context;
|
const {showPopup, member, site} = this.context;
|
||||||
|
|
||||||
if (showPopup) {
|
if (showPopup) {
|
||||||
this.context.onAction('closePopup');
|
this.context.onAction('closePopup');
|
||||||
} else {
|
return;
|
||||||
const loggedOutPage = isInviteOnlySite({site}) ? 'signin' : 'signup';
|
}
|
||||||
const page = member ? 'accountHome' : loggedOutPage;
|
|
||||||
|
if (member) {
|
||||||
|
this.context.onAction('openPopup', {page: 'accountHome'});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSigninAllowed({site})) {
|
||||||
|
const page = isInviteOnlySite({site}) ? 'signin' : 'signup';
|
||||||
this.context.onAction('openPopup', {page});
|
this.context.onAction('openPopup', {page});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,10 +249,11 @@ export default class TriggerButton extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {portal_button: portalButton} = this.context.site;
|
const site = this.context.site;
|
||||||
|
const {portal_button: portalButton} = site;
|
||||||
const {showPopup} = this.context;
|
const {showPopup} = this.context;
|
||||||
|
|
||||||
if (!portalButton || hasMode(['offerPreview'])) {
|
if (!portalButton || !isSigninAllowed({site}) || hasMode(['offerPreview'])) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,18 @@ import {getSupportAddress} from '../../../utils/helpers';
|
||||||
|
|
||||||
import AccountFooter from './components/AccountFooter';
|
import AccountFooter from './components/AccountFooter';
|
||||||
import AccountMain from './components/AccountMain';
|
import AccountMain from './components/AccountMain';
|
||||||
|
import {isSigninAllowed} from '../../../utils/helpers';
|
||||||
|
|
||||||
export default class AccountHomePage extends React.Component {
|
export default class AccountHomePage extends React.Component {
|
||||||
static contextType = AppContext;
|
static contextType = AppContext;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const {member} = this.context;
|
const {member, site} = this.context;
|
||||||
|
|
||||||
|
if (!isSigninAllowed({site})) {
|
||||||
|
this.context.onAction('signout');
|
||||||
|
}
|
||||||
|
|
||||||
if (!member) {
|
if (!member) {
|
||||||
this.context.onAction('switchPage', {
|
this.context.onAction('switchPage', {
|
||||||
page: 'signin',
|
page: 'signin',
|
||||||
|
@ -31,6 +37,9 @@ export default class AccountHomePage extends React.Component {
|
||||||
if (!member) {
|
if (!member) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (!isSigninAllowed({site})) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className='gh-portal-account-wrapper'>
|
<div className='gh-portal-account-wrapper'>
|
||||||
<AccountMain />
|
<AccountMain />
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import AppContext from '../../../../AppContext';
|
import AppContext from '../../../../AppContext';
|
||||||
import ActionButton from '../../../common/ActionButton';
|
import ActionButton from '../../../common/ActionButton';
|
||||||
import {hasOnlyFreePlan} from '../../../../utils/helpers';
|
import {isSignupAllowed} from '../../../../utils/helpers';
|
||||||
import {useContext} from 'react';
|
import {useContext} from 'react';
|
||||||
|
|
||||||
const SubscribeButton = () => {
|
const SubscribeButton = () => {
|
||||||
const {site, action, brandColor, onAction, t} = useContext(AppContext);
|
const {site, action, brandColor, onAction, t} = useContext(AppContext);
|
||||||
const {is_stripe_configured: isStripeConfigured} = site;
|
|
||||||
|
|
||||||
if (!isStripeConfigured || hasOnlyFreePlan({site})) {
|
if (!isSignupAllowed({site})) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const isRunning = ['checkoutPlan:running'].includes(action);
|
const isRunning = ['checkoutPlan:running'].includes(action);
|
||||||
|
|
|
@ -5,6 +5,8 @@ import CloseButton from '../common/CloseButton';
|
||||||
import AppContext from '../../AppContext';
|
import AppContext from '../../AppContext';
|
||||||
import InputForm from '../common/InputForm';
|
import InputForm from '../common/InputForm';
|
||||||
import {ValidateInputForm} from '../../utils/form';
|
import {ValidateInputForm} from '../../utils/form';
|
||||||
|
import {isSigninAllowed} from '../../utils/helpers';
|
||||||
|
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||||
|
|
||||||
export default class SigninPage extends React.Component {
|
export default class SigninPage extends React.Component {
|
||||||
static contextType = AppContext;
|
static contextType = AppContext;
|
||||||
|
@ -116,6 +118,23 @@ export default class SigninPage extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderForm() {
|
renderForm() {
|
||||||
|
const {site, t} = this.context;
|
||||||
|
|
||||||
|
if (!isSigninAllowed({site})) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className='gh-portal-section'>
|
||||||
|
<p
|
||||||
|
className='gh-portal-members-disabled-notification'
|
||||||
|
data-testid="members-disabled-notification-text"
|
||||||
|
>
|
||||||
|
{t('Memberships unavailable, contact the owner for access.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className='gh-portal-section'>
|
<div className='gh-portal-section'>
|
||||||
|
@ -125,32 +144,52 @@ export default class SigninPage extends React.Component {
|
||||||
onKeyDown={(e, field) => this.onKeyDown(e, field)}
|
onKeyDown={(e, field) => this.onKeyDown(e, field)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<footer className='gh-portal-signin-footer'>
|
||||||
|
{this.renderSubmitButton()}
|
||||||
|
{this.renderSignupMessage()}
|
||||||
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSiteLogo() {
|
renderSiteIcon() {
|
||||||
const siteLogo = this.context.site.icon;
|
const iconStyle = {};
|
||||||
|
const {site} = this.context;
|
||||||
|
const siteIcon = site.icon;
|
||||||
|
|
||||||
const logoStyle = {};
|
if (siteIcon) {
|
||||||
|
iconStyle.backgroundImage = `url(${siteIcon})`;
|
||||||
if (siteLogo) {
|
|
||||||
logoStyle.backgroundImage = `url(${siteLogo})`;
|
|
||||||
return (
|
return (
|
||||||
<img className='gh-portal-signup-logo' src={siteLogo} alt={this.context.site.title} />
|
<img className='gh-portal-signup-logo' src={siteIcon} alt={this.context.site.title} />
|
||||||
|
);
|
||||||
|
} else if (!isSigninAllowed({site})) {
|
||||||
|
return (
|
||||||
|
<InvitationIcon className='gh-portal-icon gh-portal-icon-invitation' />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFormHeader() {
|
renderSiteTitle() {
|
||||||
// const siteTitle = this.context.site.title || 'Site Title';
|
const {site, t} = this.context;
|
||||||
const {t} = this.context;
|
const siteTitle = site.title;
|
||||||
|
|
||||||
|
if (!isSigninAllowed({site})) {
|
||||||
|
return (
|
||||||
|
<h1 className='gh-portal-main-title'>{siteTitle}</h1>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<h1 className='gh-portal-main-title'>{t('Sign in')}</h1>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFormHeader() {
|
||||||
return (
|
return (
|
||||||
<header className='gh-portal-signin-header'>
|
<header className='gh-portal-signin-header'>
|
||||||
{this.renderSiteLogo()}
|
{this.renderSiteIcon()}
|
||||||
<h1 className="gh-portal-main-title">{t('Sign in')}</h1>
|
{this.renderSiteTitle()}
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -158,19 +197,12 @@ export default class SigninPage extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <div className='gh-portal-back-sitetitle'>
|
|
||||||
<SiteTitleBackButton />
|
|
||||||
</div> */}
|
|
||||||
<CloseButton />
|
<CloseButton />
|
||||||
<div className='gh-portal-logged-out-form-container'>
|
<div className='gh-portal-logged-out-form-container'>
|
||||||
<div className='gh-portal-content signin'>
|
<div className='gh-portal-content signin'>
|
||||||
{this.renderFormHeader()}
|
{this.renderFormHeader()}
|
||||||
{this.renderForm()}
|
{this.renderForm()}
|
||||||
</div>
|
</div>
|
||||||
<footer className='gh-portal-signin-footer'>
|
|
||||||
{this.renderSubmitButton()}
|
|
||||||
{this.renderSignupMessage()}
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,18 +1,30 @@
|
||||||
import {render, fireEvent} from '../../utils/test-utils';
|
import {render, fireEvent, getByTestId} from '../../utils/test-utils';
|
||||||
import SigninPage from './SigninPage';
|
import SigninPage from './SigninPage';
|
||||||
|
import {getSiteData} from '../../utils/fixtures-generator';
|
||||||
|
|
||||||
const setup = () => {
|
const setup = (overrides) => {
|
||||||
const {mockOnActionFn, ...utils} = render(
|
const {mockOnActionFn, ...utils} = render(
|
||||||
<SigninPage />,
|
<SigninPage />,
|
||||||
{
|
{
|
||||||
overrideContext: {
|
overrideContext: {
|
||||||
member: null
|
member: null,
|
||||||
|
...overrides
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emailInput = utils.getByLabelText(/email/i);
|
|
||||||
const submitButton = utils.queryByRole('button', {name: 'Continue'});
|
let emailInput;
|
||||||
const signupButton = utils.queryByRole('button', {name: 'Sign up'});
|
let submitButton;
|
||||||
|
let signupButton;
|
||||||
|
|
||||||
|
try {
|
||||||
|
emailInput = utils.getByLabelText(/email/i);
|
||||||
|
submitButton = utils.queryByRole('button', {name: 'Continue'});
|
||||||
|
signupButton = utils.queryByRole('button', {name: 'Sign up'});
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emailInput,
|
emailInput,
|
||||||
submitButton,
|
submitButton,
|
||||||
|
@ -47,4 +59,17 @@ describe('SigninPage', () => {
|
||||||
fireEvent.click(signupButton);
|
fireEvent.click(signupButton);
|
||||||
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {page: 'signup'});
|
expect(mockOnActionFn).toHaveBeenCalledWith('switchPage', {page: 'signup'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when members are disabled', () => {
|
||||||
|
test('renders an informative message', () => {
|
||||||
|
setup({
|
||||||
|
site: getSiteData({
|
||||||
|
membersSignupAccess: 'none'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = getByTestId(document.body, 'members-disabled-notification-text');
|
||||||
|
expect(message).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,7 +7,7 @@ import NewsletterSelectionPage from './NewsletterSelectionPage';
|
||||||
import ProductsSection from '../common/ProductsSection';
|
import ProductsSection from '../common/ProductsSection';
|
||||||
import InputForm from '../common/InputForm';
|
import InputForm from '../common/InputForm';
|
||||||
import {ValidateInputForm} from '../../utils/form';
|
import {ValidateInputForm} from '../../utils/form';
|
||||||
import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice, hasMultipleNewsletters, hasFreeTrialTier} from '../../utils/helpers';
|
import {getSiteProducts, getSitePrices, hasOnlyFreePlan, isInviteOnlySite, freeHasBenefitsOrDescription, hasOnlyFreeProduct, getFreeProductBenefits, getFreeTierDescription, hasFreeProductPrice, hasMultipleNewsletters, hasFreeTrialTier, isSignupAllowed} from '../../utils/helpers';
|
||||||
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
import {ReactComponent as InvitationIcon} from '../../images/icons/invitation.svg';
|
||||||
|
|
||||||
export const SignupPageStyles = `
|
export const SignupPageStyles = `
|
||||||
|
@ -171,7 +171,7 @@ footer.gh-portal-signup-footer.invite-only .gh-portal-signup-message {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gh-portal-invite-only-notification {
|
.gh-portal-invite-only-notification, .gh-portal-members-disabled-notification {
|
||||||
margin: 8px 32px 24px;
|
margin: 8px 32px 24px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -670,6 +670,21 @@ class SignupPage extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isSignupAllowed({site})) {
|
||||||
|
return (
|
||||||
|
<section>
|
||||||
|
<div className='gh-portal-section'>
|
||||||
|
<p
|
||||||
|
className='gh-portal-members-disabled-notification'
|
||||||
|
data-testid="members-disabled-notification-text"
|
||||||
|
>
|
||||||
|
{t('Memberships unavailable, contact the owner for access.')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const freeBenefits = getFreeProductBenefits({site});
|
const freeBenefits = getFreeProductBenefits({site});
|
||||||
const freeDescription = getFreeTierDescription({site});
|
const freeDescription = getFreeTierDescription({site});
|
||||||
const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site});
|
const showOnlyFree = pageQuery === 'free' && hasFreeProductPrice({site});
|
||||||
|
@ -716,22 +731,22 @@ class SignupPage extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderSiteLogo() {
|
renderSiteIcon() {
|
||||||
const {site, pageQuery} = this.context;
|
const {site, pageQuery} = this.context;
|
||||||
|
const siteIcon = site.icon;
|
||||||
|
|
||||||
const siteLogo = site.icon;
|
if (siteIcon) {
|
||||||
|
|
||||||
const logoStyle = {};
|
|
||||||
|
|
||||||
if (siteLogo) {
|
|
||||||
logoStyle.backgroundImage = `url(${siteLogo})`;
|
|
||||||
return (
|
return (
|
||||||
<img className='gh-portal-signup-logo' src={siteLogo} alt={site.title} />
|
<img className='gh-portal-signup-logo' src={siteIcon} alt={site.title} />
|
||||||
);
|
);
|
||||||
} else if (isInviteOnlySite({site, pageQuery})) {
|
} else if (isInviteOnlySite({site, pageQuery})) {
|
||||||
return (
|
return (
|
||||||
<InvitationIcon className='gh-portal-icon gh-portal-icon-invitation' />
|
<InvitationIcon className='gh-portal-icon gh-portal-icon-invitation' />
|
||||||
);
|
);
|
||||||
|
} else if (!isSignupAllowed({site})) {
|
||||||
|
return (
|
||||||
|
<InvitationIcon className='gh-portal-icon gh-portal-icon-invitation' />
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -741,7 +756,7 @@ class SignupPage extends React.Component {
|
||||||
const siteTitle = site.title || '';
|
const siteTitle = site.title || '';
|
||||||
return (
|
return (
|
||||||
<header className='gh-portal-signup-header'>
|
<header className='gh-portal-signup-header'>
|
||||||
{this.renderSiteLogo()}
|
{this.renderSiteIcon()}
|
||||||
<h1 className="gh-portal-main-title" data-testid='site-title-text'>{siteTitle}</h1>
|
<h1 className="gh-portal-main-title" data-testid='site-title-text'>{siteTitle}</h1>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
|
@ -794,10 +809,6 @@ class SignupPage extends React.Component {
|
||||||
{this.renderFormHeader()}
|
{this.renderFormHeader()}
|
||||||
{this.renderForm()}
|
{this.renderForm()}
|
||||||
</div>
|
</div>
|
||||||
{/* <footer className={'gh-portal-signup-footer gh-portal-logged-out-form-container ' + footerClass}>
|
|
||||||
{this.renderSubmitButton()}
|
|
||||||
{this.renderLoginMessage()}
|
|
||||||
</footer> */}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import SignupPage from './SignupPage';
|
import SignupPage from './SignupPage';
|
||||||
import {getFreeProduct, getProductData, getSiteData} from '../../utils/fixtures-generator';
|
import {getFreeProduct, getProductData, getSiteData} from '../../utils/fixtures-generator';
|
||||||
import {render, fireEvent} from '../../utils/test-utils';
|
import {render, fireEvent, getByTestId} from '../../utils/test-utils';
|
||||||
|
|
||||||
const setup = (overrides) => {
|
const setup = (overrides) => {
|
||||||
const {mockOnActionFn, ...utils} = render(
|
const {mockOnActionFn, ...utils} = render(
|
||||||
|
@ -12,12 +12,25 @@ const setup = (overrides) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const emailInput = utils.getByLabelText(/email/i);
|
|
||||||
const nameInput = utils.getByLabelText(/name/i);
|
let emailInput;
|
||||||
const submitButton = utils.queryByRole('button', {name: 'Continue'});
|
let nameInput;
|
||||||
const chooseButton = utils.queryAllByRole('button', {name: 'Choose'});
|
let submitButton;
|
||||||
const signinButton = utils.queryByRole('button', {name: 'Sign in'});
|
let chooseButton;
|
||||||
const freeTrialMessage = utils.queryByText(/After a free trial ends/i);
|
let signinButton;
|
||||||
|
let freeTrialMessage;
|
||||||
|
|
||||||
|
try {
|
||||||
|
emailInput = utils.getByLabelText(/email/i);
|
||||||
|
nameInput = utils.getByLabelText(/name/i);
|
||||||
|
submitButton = utils.queryByRole('button', {name: 'Continue'});
|
||||||
|
chooseButton = utils.queryAllByRole('button', {name: 'Choose'});
|
||||||
|
signinButton = utils.queryByRole('button', {name: 'Sign in'});
|
||||||
|
freeTrialMessage = utils.queryByText(/After a free trial ends/i);
|
||||||
|
} catch (err) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nameInput,
|
nameInput,
|
||||||
emailInput,
|
emailInput,
|
||||||
|
@ -89,4 +102,17 @@ describe('SignupPage', () => {
|
||||||
|
|
||||||
expect(freeTrialMessage).not.toBeInTheDocument();
|
expect(freeTrialMessage).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when members are disabled', () => {
|
||||||
|
test('renders an informative message', () => {
|
||||||
|
setup({
|
||||||
|
site: getSiteData({
|
||||||
|
membersSignupAccess: 'none'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const message = getByTestId(document.body, 'members-disabled-notification-text');
|
||||||
|
expect(message).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -238,6 +238,14 @@ export function isInviteOnlySite({site = {}, pageQuery = ''}) {
|
||||||
return prices.length === 0 || (site && site.members_signup_access === 'invite');
|
return prices.length === 0 || (site && site.members_signup_access === 'invite');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isSigninAllowed({site}) {
|
||||||
|
return site?.members_signup_access === 'all' || site?.members_signup_access === 'invite';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSignupAllowed({site}) {
|
||||||
|
return site?.members_signup_access === 'all' && (site?.is_stripe_configured || hasOnlyFreePlan({site}));
|
||||||
|
}
|
||||||
|
|
||||||
export function hasMultipleProducts({site}) {
|
export function hasMultipleProducts({site}) {
|
||||||
const products = getAvailableProducts({site});
|
const products = getAvailableProducts({site});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import {getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData} from './helpers';
|
import {getAllProductsForSite, getAvailableProducts, getCurrencySymbol, getFreeProduct, getMemberName, getMemberSubscription, getPriceFromSubscription, getPriceIdFromPageQuery, getSupportAddress, getUrlHistory, hasMultipleProducts, isActiveOffer, isInviteOnlySite, isPaidMember, isSameCurrency, transformApiTiersData, isSigninAllowed, isSignupAllowed} from './helpers';
|
||||||
import * as Fixtures from './fixtures-generator';
|
import * as Fixtures from './fixtures-generator';
|
||||||
import {site as FixturesSite, member as FixtureMember, offer as FixtureOffer, transformTierFixture as TransformFixtureTiers} from '../utils/test-fixtures';
|
import {site as FixturesSite, member as FixtureMember, offer as FixtureOffer, transformTierFixture as TransformFixtureTiers} from '../utils/test-fixtures';
|
||||||
import {isComplimentaryMember} from '../utils/helpers';
|
import {isComplimentaryMember} from '../utils/helpers';
|
||||||
|
@ -137,8 +137,12 @@ describe('Helpers - ', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('isInviteOnlySite - ', () => {
|
describe('isInviteOnlySite - ', () => {
|
||||||
test('returns true for invite only site', () => {
|
test('returns true for a site without plans', () => {
|
||||||
const value = isInviteOnlySite({site: FixturesSite.singleTier.inviteOnly});
|
const value = isInviteOnlySite({site: FixturesSite.singleTier.withoutPlans});
|
||||||
|
expect(value).toBe(true);
|
||||||
|
});
|
||||||
|
test('returns true for a site with invite-only members', () => {
|
||||||
|
const value = isInviteOnlySite({site: FixturesSite.singleTier.membersInviteOnly});
|
||||||
expect(value).toBe(true);
|
expect(value).toBe(true);
|
||||||
});
|
});
|
||||||
test('returns false for non invite only site', () => {
|
test('returns false for non invite only site', () => {
|
||||||
|
@ -147,6 +151,45 @@ describe('Helpers - ', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isSigninAllowed - ', () => {
|
||||||
|
test('returns true for a site with members enabled', () => {
|
||||||
|
const value = isSigninAllowed({site: FixturesSite.singleTier.basic});
|
||||||
|
expect(value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for a site with invite-only members', () => {
|
||||||
|
const value = isSigninAllowed({site: FixturesSite.singleTier.membersInviteOnly});
|
||||||
|
expect(value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for a site with members disabled', () => {
|
||||||
|
const value = isSigninAllowed({site: FixturesSite.singleTier.membersDisabled});
|
||||||
|
expect(value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isSignupAllowed - ', () => {
|
||||||
|
test('returns true for a site with members enabled, and with Stripe configured', () => {
|
||||||
|
const value = isSignupAllowed({site: FixturesSite.singleTier.basic});
|
||||||
|
expect(value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns true for a site with members enabled, without Stripe configured, but with only free tiers', () => {
|
||||||
|
const value = isSignupAllowed({site: FixturesSite.singleTier.onlyFreePlanWithoutStripe});
|
||||||
|
expect(value).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for a site with invite-only members', () => {
|
||||||
|
const value = isSignupAllowed({site: FixturesSite.singleTier.membersInviteOnly});
|
||||||
|
expect(value).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('returns false for a site with members disabled', () => {
|
||||||
|
const value = isSignupAllowed({site: FixturesSite.singleTier.membersDisabled});
|
||||||
|
expect(value).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('hasMultipleProducts - ', () => {
|
describe('hasMultipleProducts - ', () => {
|
||||||
test('returns true for multiple tier site', () => {
|
test('returns true for multiple tier site', () => {
|
||||||
const value = hasMultipleProducts({site: FixturesSite.multipleTiers.basic});
|
const value = hasMultipleProducts({site: FixturesSite.multipleTiers.basic});
|
||||||
|
|
|
@ -136,7 +136,7 @@ const baseMultiTierSite = getSiteData({
|
||||||
export const site = {
|
export const site = {
|
||||||
singleTier: {
|
singleTier: {
|
||||||
basic: baseSingleTierSite,
|
basic: baseSingleTierSite,
|
||||||
inviteOnly: {
|
withoutPlans: {
|
||||||
...baseSingleTierSite,
|
...baseSingleTierSite,
|
||||||
portal_plans: []
|
portal_plans: []
|
||||||
},
|
},
|
||||||
|
@ -151,6 +151,23 @@ export const site = {
|
||||||
withoutName: {
|
withoutName: {
|
||||||
...baseSingleTierSite,
|
...baseSingleTierSite,
|
||||||
portal_name: false
|
portal_name: false
|
||||||
|
},
|
||||||
|
withoutStripe: {
|
||||||
|
...baseSingleTierSite,
|
||||||
|
is_stripe_configured: false
|
||||||
|
},
|
||||||
|
onlyFreePlanWithoutStripe: {
|
||||||
|
...baseSingleTierSite,
|
||||||
|
portal_plans: ['free'],
|
||||||
|
is_stripe_configured: false
|
||||||
|
},
|
||||||
|
membersInviteOnly: {
|
||||||
|
...baseSingleTierSite,
|
||||||
|
members_signup_access: 'invite'
|
||||||
|
},
|
||||||
|
membersDisabled: {
|
||||||
|
...baseSingleTierSite,
|
||||||
|
members_signup_access: 'none'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
multipleTiers: {
|
multipleTiers: {
|
||||||
|
|
|
@ -45,9 +45,11 @@ function finaliseStructuredData(meta) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMembersHelper(data, frontendKey) {
|
function getMembersHelper(data, frontendKey) {
|
||||||
if (!settingsCache.get('members_enabled')) {
|
// Do not load Portal if both Memberships and Tips & Donations are disabled
|
||||||
|
if (!settingsCache.get('members_enabled') && !settingsCache.get('donations_enabled')) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
const {scriptUrl} = getFrontendAppConfig('portal');
|
const {scriptUrl} = getFrontendAppConfig('portal');
|
||||||
|
|
||||||
const colorString = (_.has(data, 'site._preview') && data.site.accent_color) ? data.site.accent_color : '';
|
const colorString = (_.has(data, 'site._preview') && data.site.accent_color) ? data.site.accent_color : '';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const {expect, test} = require('@playwright/test');
|
const {expect, test} = require('@playwright/test');
|
||||||
const {createPostDraft, createTier} = require('../utils');
|
const {createPostDraft, createTier, disconnectStripe, generateStripeIntegrationToken, setupStripe} = require('../utils');
|
||||||
|
|
||||||
const changeSubscriptionAccess = async (page, access) => {
|
const changeSubscriptionAccess = async (page, access) => {
|
||||||
// Go to settings page
|
// Go to settings page
|
||||||
|
@ -53,9 +53,6 @@ test.describe('Site Settings', () => {
|
||||||
|
|
||||||
// Check free trial message is not shown for invite only
|
// Check free trial message is not shown for invite only
|
||||||
await expect(portalFrame.locator('.gh-portal-free-trial-notification')).not.toBeVisible();
|
await expect(portalFrame.locator('.gh-portal-free-trial-notification')).not.toBeVisible();
|
||||||
|
|
||||||
// Check portal script loaded (just a negative test for the following test to test the test)
|
|
||||||
await checkPortalScriptLoaded(page, true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Disabled subscription access', async ({page}) => {
|
test('Disabled subscription access', async ({page}) => {
|
||||||
|
@ -63,13 +60,9 @@ test.describe('Site Settings', () => {
|
||||||
|
|
||||||
await changeSubscriptionAccess(page, 'none');
|
await changeSubscriptionAccess(page, 'none');
|
||||||
|
|
||||||
// Go to the sigup page
|
// Go to the signup page
|
||||||
await page.goto('/#/portal/signup');
|
await page.goto('/#/portal/signup');
|
||||||
|
|
||||||
// Check Portal not loaded, and thus the signup page is not shown
|
|
||||||
await expect(page.locator('#ghost-portal-root div iframe')).toHaveCount(0);
|
|
||||||
await checkPortalScriptLoaded(page, false);
|
|
||||||
|
|
||||||
// Check publishing flow is different and has membership features disabled
|
// Check publishing flow is different and has membership features disabled
|
||||||
await page.goto('/ghost');
|
await page.goto('/ghost');
|
||||||
await createPostDraft(page, {
|
await createPostDraft(page, {
|
||||||
|
@ -79,9 +72,65 @@ test.describe('Site Settings', () => {
|
||||||
await page.locator('[data-test-button="publish-flow"]').click();
|
await page.locator('[data-test-button="publish-flow"]').click();
|
||||||
await expect(page.locator('[data-test-setting="publish-type"] > button')).toHaveCount(0);
|
await expect(page.locator('[data-test-setting="publish-type"] > button')).toHaveCount(0);
|
||||||
await expect(page.locator('[data-test-setting="email-recipients"]')).toHaveCount(0);
|
await expect(page.locator('[data-test-setting="email-recipients"]')).toHaveCount(0);
|
||||||
// reset back to all
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Portal script', () => {
|
||||||
|
test('Portal loads if Memberships are enabled', async ({page}) => {
|
||||||
|
await page.goto('/ghost');
|
||||||
|
|
||||||
|
// Enable Memberships
|
||||||
|
await changeSubscriptionAccess(page, 'all');
|
||||||
|
|
||||||
|
// Go to the signup page
|
||||||
|
await page.goto('/#/portal/signup');
|
||||||
|
|
||||||
|
// Portal should load
|
||||||
|
await expect(page.locator('#ghost-portal-root div iframe')).toHaveCount(1);
|
||||||
|
await checkPortalScriptLoaded(page, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Portal loads if Tips & Donations are enabled (Stripe connected)', async ({page}) => {
|
||||||
|
await page.goto('/ghost');
|
||||||
|
|
||||||
|
// Disable Memberships
|
||||||
|
await changeSubscriptionAccess(page, 'none');
|
||||||
|
|
||||||
|
// Go to the signup page
|
||||||
|
await page.goto('/#/portal/signup');
|
||||||
|
|
||||||
|
// Portal should load
|
||||||
|
await expect(page.locator('#ghost-portal-root div iframe')).toHaveCount(1);
|
||||||
|
await checkPortalScriptLoaded(page, true);
|
||||||
|
|
||||||
|
// Reset
|
||||||
await page.goto('/ghost');
|
await page.goto('/ghost');
|
||||||
await changeSubscriptionAccess(page, 'all');
|
await changeSubscriptionAccess(page, 'all');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Portal does not load if both Memberships and Tips & Donations are disabled', async ({page}) => {
|
||||||
|
// Disconnect stripe first, which will disable Tips & Donations
|
||||||
|
await page.goto('/ghost');
|
||||||
|
await disconnectStripe(page);
|
||||||
|
|
||||||
|
// Disable Memberships
|
||||||
|
await page.goto('/ghost');
|
||||||
|
await changeSubscriptionAccess(page, 'none');
|
||||||
|
|
||||||
|
// Go to the signup page
|
||||||
|
await page.goto('/#/portal/signup');
|
||||||
|
|
||||||
|
// Portal should not load
|
||||||
|
await expect(page.locator('#ghost-portal-root div iframe')).toHaveCount(0);
|
||||||
|
await checkPortalScriptLoaded(page, false);
|
||||||
|
|
||||||
|
// Reset subscription access & re-connect Stripe
|
||||||
|
await page.goto('/ghost');
|
||||||
|
await changeSubscriptionAccess(page, 'all');
|
||||||
|
|
||||||
|
await page.goto('/ghost');
|
||||||
|
const stripeToken = await generateStripeIntegrationToken();
|
||||||
|
await setupStripe(page, stripeToken);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue