Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Added option to disable free signups #21862

Merged
merged 32 commits into from
Dec 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
3e68e0a
Added paid option to members_signup_access setting
sagzy Dec 11, 2024
1c6dc16
Fixed database tests
sagzy Dec 11, 2024
c0e5f8f
Added the new paid option to subscription access settings
sagzy Dec 11, 2024
a82e01f
Updated admin-x-settings acceptance tests
sagzy Dec 12, 2024
0808ec6
Hid the free tier in Portal settings, if site is set to paid-members …
sagzy Dec 16, 2024
90ae443
Refactored sendMagicLink code for readability
sagzy Dec 16, 2024
7f82828
Returned a specific error message on free signup against a paid-membe…
sagzy Dec 16, 2024
f3407ea
Added failing tests in Portal for paid-members only sites
sagzy Dec 16, 2024
b96a22f
Updated signup options in Portal settings to be available for paid-me…
sagzy Dec 17, 2024
733088b
Added to the Settings cache, to avoid duplicating the logic in Portal
sagzy Dec 17, 2024
e924504
Refactored isInviteOnly helper function, to only do what the name sug…
sagzy Dec 17, 2024
c92ee6f
Added paid-members only error message to the error translator helper
sagzy Dec 17, 2024
c299a83
Updated isSignupAllowed / isSigninAllowed helper functions in Portal
sagzy Dec 17, 2024
c7e3630
Fallback magic link endpoint to subscribe by default
sagzy Dec 17, 2024
a67eb02
Don't show the invite-only banner on paid-members only sites
sagzy Dec 17, 2024
562d7eb
Fixed unwanted full-size layout on /signup/free page
sagzy Dec 17, 2024
7dfdd23
Blocked /signup/free portal page, for paid-members only sites
sagzy Dec 19, 2024
0cc6745
Fixed linting
sagzy Dec 19, 2024
3a634d4
Updated settings content api snapshot to include the new key
sagzy Dec 19, 2024
afba0ef
Added new translated string
sagzy Dec 19, 2024
f3e9704
Added Portal tests
sagzy Dec 19, 2024
adbf5e7
Updated test snapshots to include the new members_signup_access key
sagzy Dec 19, 2024
32e5f5a
Missing translation
sagzy Dec 19, 2024
32ccf94
Added tests for /portal/signup links
sagzy Dec 19, 2024
13a28c1
Merge branch 'main' into add-disable-free-signups-option
sagzy Dec 19, 2024
a7aa91a
Improved naming
sagzy Dec 19, 2024
443491c
Removed leftover .only
sagzy Dec 19, 2024
bcabca1
Refactored ìsSignupAllowed for consistency
sagzy Dec 19, 2024
17de626
Update browser test
sagzy Dec 19, 2024
fbd9820
Refactored signupPage flow and made paid-members only message more co…
sagzy Dec 19, 2024
3e529a6
Updated Portal tests
sagzy Dec 19, 2024
860b823
Fixed linting
sagzy Dec 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/admin-x-design-system/src/global/form/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ export interface CheckboxProps {
key?: string;
checked?: boolean;
separator?: boolean;
testId?: string;
}

const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disabled, error, hint, checked, separator}) => {
const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disabled, error, hint, checked, separator, testId}) => {
const id = useId();

const handleCheckedChange = (isChecked: boolean | 'indeterminate') => {
Expand All @@ -29,7 +30,7 @@ const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disab
<div className={`flex flex-col gap-1 ${separator && 'pb-2'}`}>
{title && <Heading grey={true} level={6}>{title}</Heading>}
<label className={`flex cursor-pointer items-start ${title && '-mb-1 mt-1'}`} htmlFor={id}>
<CheckboxPrimitive.Root className="mt-0.5 flex h-4 w-4 cursor-pointer appearance-none items-center justify-center rounded-[3px] border border-solid border-grey-500 bg-white outline-none data-[state=checked]:border-black data-[state=indeterminate]:border-black data-[state=checked]:bg-black data-[state=indeterminate]:bg-black" defaultChecked={checked} disabled={disabled} id={id} value={value} onCheckedChange={handleCheckedChange}>
<CheckboxPrimitive.Root className="mt-0.5 flex h-4 w-4 cursor-pointer appearance-none items-center justify-center rounded-[3px] border border-solid border-grey-500 bg-white outline-none data-[state=checked]:border-black data-[state=indeterminate]:border-black data-[state=checked]:bg-black data-[state=indeterminate]:bg-black" data-testid={testId} defaultChecked={checked} disabled={disabled} id={id} value={value} onCheckedChange={handleCheckedChange}>
<CheckboxPrimitive.Indicator>
<svg fill="none" height="11" viewBox="0 0 10 11" width="10">
<path d="M1 5.88889L4.6 9L9 1" stroke="white" strokeLinecap="round" strokeWidth="2"/>
Expand Down
13 changes: 13 additions & 0 deletions apps/admin-x-framework/src/test/acceptance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,19 @@ export async function chooseOptionInSelect(select: Locator, optionText: string |
await select.page().locator('[data-testid="select-option"]', {hasText: optionText}).click();
}

export async function getOptionsFromSelect(select: Locator): Promise<string[]> {
// Open the select dropdown
await select.click();

const options = await select.page().locator('[data-testid="select-option"]');
const optionTexts = await options.allTextContents();

// Close the select dropdown
await select.press('Escape');

return optionTexts;
}

export async function testUrlValidation(input: Locator, textToEnter: string, expectedResult: string, expectedError?: string) {
await input.fill(textToEnter);
await input.blur();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ const MEMBERS_SIGNUP_ACCESS_OPTIONS = [
label: 'Anyone can sign up',
hint: 'All visitors will be able to subscribe and sign in'
},
{
value: 'paid',
label: 'Paid-members only',
hint: 'A paid Stripe subscription is required to sign up'
},
{
value: 'invite',
label: 'Only people I invite',
hint: 'People can sign in from your site but won\'t be able to sign up'
label: 'Invite-only',
hint: 'People can sign in but won\'t be able to sign up'
},
{
value: 'none',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,21 @@ const SignupOptions: React.FC<{
}
};

// This is a bit unclear in current admin, maybe we should add a message if the settings are disabled?
const isDisabled = membersSignupAccess !== 'all';

const isSignupAllowed = membersSignupAccess === 'all' || membersSignupAccess === 'paid';
const isFreeSignupAllowed = membersSignupAccess === 'all';
const isStripeEnabled = checkStripeEnabled(localSettings, config!);

let tiersCheckboxes: CheckboxProps[] = [];

if (localTiers) {
localTiers.forEach((tier) => {
if (tier.type === 'free') {
if (tier.type === 'free' && isFreeSignupAllowed) {
tiersCheckboxes.push({
checked: (portalPlans.includes('free')),
disabled: isDisabled,
disabled: !isSignupAllowed,
label: tier.name,
value: 'free',
testId: 'free-tier-checkbox',
onChange: (checked) => {
if (portalPlans.includes('free') && !checked) {
portalPlans.splice(portalPlans.indexOf('free'), 1);
Expand Down Expand Up @@ -119,7 +119,7 @@ const SignupOptions: React.FC<{
<Toggle
checked={Boolean(portalName)}
direction='rtl'
disabled={isDisabled}
disabled={!isSignupAllowed}
label='Display name in signup form'
onChange={e => updateSetting('portal_name', e.target.checked)}
/>
Expand All @@ -135,7 +135,7 @@ const SignupOptions: React.FC<{
checkboxes={[
{
checked: portalPlans.includes('monthly'),
disabled: isDisabled,
disabled: !isSignupAllowed,
label: 'Monthly',
value: 'monthly',
onChange: () => {
Expand All @@ -144,7 +144,7 @@ const SignupOptions: React.FC<{
},
{
checked: portalPlans.includes('yearly'),
disabled: isDisabled,
disabled: !isSignupAllowed,
label: 'Yearly',
value: 'yearly',
onChange: () => {
Expand Down Expand Up @@ -179,7 +179,7 @@ const SignupOptions: React.FC<{

{portalSignupTermsHtml?.toString() && <Toggle
checked={Boolean(portalSignupCheckboxRequired)}
disabled={isDisabled}
disabled={!isSignupAllowed}
label='Require agreement'
labelStyle='heading'
onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)}
Expand Down
4 changes: 1 addition & 3 deletions apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ export const getPortalPreviewUrl = ({settings, config, tiers, siteData, selected
const portalBase = '/?v=modal-portal-settings#/portal/preview';

const portalPlans: string[] = JSON.parse(getSettingValue<string>(settings, 'portal_plans') || '');
const membersSignupAccess = getSettingValue<string>(settings, 'members_signup_access');
const allowSelfSignup = membersSignupAccess === 'all' && (!checkStripeEnabled(settings, config) || portalPlans.includes('free'));

const settingsParam = new URLSearchParams();
settingsParam.append('button', getSettingValue(settings, 'portal_button') ? 'true' : 'false');
Expand All @@ -36,7 +34,7 @@ export const getPortalPreviewUrl = ({settings, config, tiers, siteData, selected
settingsParam.append('buttonIcon', encodeURIComponent(getSettingValue(settings, 'portal_button_icon') || 'icon-1'));
settingsParam.append('signupButtonText', encodeURIComponent(getSettingValue(settings, 'portal_button_signup_text') || ''));
settingsParam.append('membersSignupAccess', getSettingValue(settings, 'members_signup_access') || 'all');
settingsParam.append('allowSelfSignup', allowSelfSignup ? 'true' : 'false');
settingsParam.append('allowSelfSignup', getSettingValue<string>(settings, 'allow_self_signup') || 'false');
settingsParam.append('signupTermsHtml', getSettingValue(settings, 'portal_signup_terms_html') || '');
settingsParam.append('signupCheckboxRequired', getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false');
settingsParam.append('portalProducts', portalTiers.join(',')); // assuming that it might be more than 1
Expand Down
22 changes: 17 additions & 5 deletions apps/admin-x-settings/test/acceptance/membership/access.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {chooseOptionInSelect, mockApi, responseFixtures, updatedSettingsResponse} from '@tryghost/admin-x-framework/test/acceptance';
import {chooseOptionInSelect, getOptionsFromSelect, mockApi, responseFixtures, updatedSettingsResponse} from '@tryghost/admin-x-framework/test/acceptance';
import {expect, test} from '@playwright/test';
import {globalDataRequests} from '../../utils/acceptance';

Expand All @@ -17,17 +17,29 @@ test.describe('Access settings', async () => {

const section = page.getByTestId('access');

// Check current selected values
await expect(section.getByText('Anyone can sign up')).toHaveCount(1);
await expect(section.getByText('Public')).toHaveCount(1);
await expect(section.getByText('Nobody')).toHaveCount(1);

await chooseOptionInSelect(section.getByTestId('subscription-access-select'), 'Only people I invite');
await chooseOptionInSelect(section.getByTestId('default-post-access-select'), /^Members only$/);
await chooseOptionInSelect(section.getByTestId('commenting-select'), 'All members');
const subscriptionAccessSelect = section.getByTestId('subscription-access-select');
const defaultPostAccessSelect = section.getByTestId('default-post-access-select');
const commentingSelect = section.getByTestId('commenting-select');

// Check available options
await expect(getOptionsFromSelect(subscriptionAccessSelect)).resolves.toEqual(['Anyone can sign up', 'Paid-members only', 'Invite-only', 'Nobody']);
await expect(getOptionsFromSelect(defaultPostAccessSelect)).resolves.toEqual(['Public', 'Members only', 'Paid-members only', 'Specific tiers']);
await expect(getOptionsFromSelect(commentingSelect)).resolves.toEqual(['All members', 'Paid-members only', 'Nobody']);

// Edit access settings to new values
await chooseOptionInSelect(subscriptionAccessSelect, 'Invite-only');
await chooseOptionInSelect(defaultPostAccessSelect, /^Members only$/);
await chooseOptionInSelect(commentingSelect, 'All members');

await section.getByRole('button', {name: 'Save'}).click();

await expect(section.getByText('Only people I invite')).toHaveCount(1);
// Check that the new values are now displayed
await expect(section.getByText('Invite-only')).toHaveCount(1);
await expect(section.getByText('Members only')).toHaveCount(1);
await expect(section.getByText('All members')).toHaveCount(1);

Expand Down
44 changes: 40 additions & 4 deletions apps/admin-x-settings/test/acceptance/membership/portal.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {expect, test} from '@playwright/test';
import {globalDataRequests} from '../../utils/acceptance';
import {mockApi, mockSitePreview, responseFixtures} from '@tryghost/admin-x-framework/test/acceptance';
import {mockApi, mockSitePreview, responseFixtures, updatedSettingsResponse} from '@tryghost/admin-x-framework/test/acceptance';

test.describe('Portal Settings', async () => {
test('Loads Portal Preview Modal', async ({page}) => {
Expand All @@ -22,7 +22,7 @@ test.describe('Portal Settings', async () => {
await page.waitForSelector('[data-testid="portal-modal"]');
});

test('can toggle portal signup options', async ({page}) => {
test('can set portal signup options', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
tiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers},
Expand All @@ -44,8 +44,16 @@ test.describe('Portal Settings', async () => {

const modal = await page.getByTestId('portal-modal');

await modal.getByRole('switch').click();
await modal.getByRole('checkbox').click();
const displayNameToggle = await modal.getByLabel('Display name in signup form');
expect(displayNameToggle).toBeVisible();
expect(displayNameToggle).toBeChecked();

const freeTierCheckbox = await modal.getByTestId('free-tier-checkbox');
expect(freeTierCheckbox).toBeVisible();
expect(freeTierCheckbox).toBeChecked();

await freeTierCheckbox.click();
await displayNameToggle.click();
await modal.getByRole('button', {name: 'Save'}).click();

expect(lastApiRequests.editTiers?.body).toMatchObject({
Expand All @@ -56,6 +64,34 @@ test.describe('Portal Settings', async () => {
});
});

test('free tier is hidden from portal signup options if the site is set to paid-members only', async ({page}) => {
await mockApi({page, requests: {
...globalDataRequests,
// Set site to paid-members only
browseSettings: {...globalDataRequests.browseSettings, response: updatedSettingsResponse([
{key: 'members_signup_access', value: 'paid'}
])},
// Free tier is available in the tiers list
browseTiers: {method: 'GET', path: '/tiers/', response: responseFixtures.tiers}
}});

await mockSitePreview({
page,
url: 'http://localhost:2368/?v=modal-portal-settings#/portal/preview?button=true&name=true&isFree=true&isMonthly=true&isYearly=true&page=signup&buttonIcon=icon-1&signupButtonText=Subscribe&membersSignupAccess=all&allowSelfSignup=true&signupTermsHtml=&signupCheckboxRequired=false&portalProducts=6511005e14c14a231e49af15&portalPrices=free%252Cmonthly%252Cyearly&accentColor=%2523FF1A75&buttonStyle=icon-and-text&disableBackground=false',
response: '<html><head><style></style></head><body><div>PortalPreview</div></body></html>'
});

await page.goto('/');
const section = await page.getByTestId('portal');
await section.getByRole('button', {name: 'Customize'}).click();
await page.waitForSelector('[data-testid="portal-modal"]');
const modal = await page.getByTestId('portal-modal');

// In Portal settings, the free tier is hidden because the site is set to paid-members only, even if available in the tiers list
const freeTierCheckbox = await modal.getByTestId('free-tier-checkbox');
expect(freeTierCheckbox).not.toBeVisible();
});

test('can toggle portal Look & Feel options', async ({page}) => {
const {lastApiRequests} = await mockApi({page, requests: {
...globalDataRequests,
Expand Down
4 changes: 2 additions & 2 deletions apps/portal/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {transformPortalAnchorToRelative} from './utils/transform-portal-anchor-t
import {getActivePage, isAccountPage, isOfferPage} from './pages';
import ActionHandler from './actions';
import './App.css';
import {hasRecommendations, allowCompMemberUpgrade, createPopupNotification, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnlySite, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {hasRecommendations, allowCompMemberUpgrade, createPopupNotification, hasAvailablePrices, getCurrencySymbol, getFirstpromoterId, getPriceIdFromPageQuery, getProductCadenceFromPrice, getProductFromId, getQueryPrice, getSiteDomain, isActiveOffer, isComplimentaryMember, isInviteOnly, isPaidMember, isRecentMember, isSentryEventAllowed, removePortalLinkFromUrl} from './utils/helpers';
import {handleDataAttributes} from './data-attributes';

import i18nLib from '@tryghost/i18n';
Expand Down Expand Up @@ -927,7 +927,7 @@ export default class App extends React.Component {
getContextPage({site, page, member}) {
/**Set default page based on logged-in status */
if (!page || page === 'default') {
const loggedOutPage = isInviteOnlySite({site}) ? 'signin' : 'signup';
const loggedOutPage = isInviteOnly({site}) || !hasAvailablePrices({site}) ? 'signin' : 'signup';
page = member ? 'accountHome' : loggedOutPage;
}

Expand Down
6 changes: 3 additions & 3 deletions apps/portal/src/components/PopupModal.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {getFrameStyles} from './Frame.styles';
import Pages, {getActivePage} from '../pages';
import PopupNotification from './common/PopupNotification';
import PoweredBy from './common/PoweredBy';
import {getSiteProducts, isInviteOnlySite, isCookiesDisabled, hasFreeProductPrice} from '../utils/helpers';
import {getSiteProducts, hasAvailablePrices, isInviteOnly, isCookiesDisabled, hasFreeProductPrice} from '../utils/helpers';

const StylesWrapper = () => {
return {
Expand Down Expand Up @@ -142,7 +142,7 @@ class PopupContent extends React.Component {

render() {
const {page, pageQuery, site, customSiteUrl} = this.context;
const products = getSiteProducts({site});
const products = getSiteProducts({site, pageQuery});
const noOfProducts = products.length;

getActivePage({page});
Expand Down Expand Up @@ -177,7 +177,7 @@ class PopupContent extends React.Component {
break;
}

if (noOfProducts > 1 && !isInviteOnlySite({site, pageQuery})) {
if (noOfProducts > 1 && !isInviteOnly({site}) && hasAvailablePrices({site, pageQuery})) {
if (page === 'signup') {
pageClass += ' full-size';
popupSize = 'full';
Expand Down
4 changes: 2 additions & 2 deletions apps/portal/src/components/TriggerButton.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ButtonIcon5} from '../images/icons/button-icon-5.svg';
import TriggerButtonStyle from './TriggerButton.styles';
import {isInviteOnlySite, isSigninAllowed} from '../utils/helpers';
import {hasAvailablePrices, isInviteOnly, isSigninAllowed} from '../utils/helpers';
import {hasMode} from '../utils/check-mode';

const ICON_MAPPING = {
Expand Down Expand Up @@ -176,7 +176,7 @@ class TriggerButtonContent extends React.Component {
}

if (isSigninAllowed({site})) {
const page = isInviteOnlySite({site}) ? 'signin' : 'signup';
const page = isInviteOnly({site}) || !hasAvailablePrices({site}) ? 'signin' : 'signup';
this.context.onAction('openPopup', {page});
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import AppContext from '../../../../AppContext';
import ActionButton from '../../../common/ActionButton';
import {isSignupAllowed} from '../../../../utils/helpers';
import {isSignupAllowed, hasAvailablePrices} from '../../../../utils/helpers';
import {useContext} from 'react';

const SubscribeButton = () => {
const {site, action, brandColor, onAction, t} = useContext(AppContext);

if (!isSignupAllowed({site})) {
if (!isSignupAllowed({site}) || !hasAvailablePrices({site})) {
return null;
}
const isRunning = ['checkoutPlan:running'].includes(action);
Expand Down
Loading
Loading