diff --git a/apps/admin-x-framework/src/test/responses/settings.json b/apps/admin-x-framework/src/test/responses/settings.json index ea3e443bc5..758c3dc546 100644 --- a/apps/admin-x-framework/src/test/responses/settings.json +++ b/apps/admin-x-framework/src/test/responses/settings.json @@ -176,6 +176,10 @@ "key": "portal_plans", "value": "[\"monthly\",\"yearly\",\"free\"]" }, + { + "key": "portal_default_plan", + "value": "yearly" + }, { "key": "portal_products", "value": "[]" diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx index 2f7614467e..171ac7c558 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/SignupOptions.tsx @@ -14,9 +14,9 @@ const SignupOptions: React.FC<{ setError: (key: string, error: string | undefined) => void }> = ({localSettings, updateSetting, localTiers, updateTier, errors, setError}) => { const {config} = useGlobalData(); - - const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson] = getSettingValues( - localSettings, ['members_signup_access', 'portal_name', 'portal_signup_terms_html', 'portal_signup_checkbox_required', 'portal_plans'] + const hasPortalImprovements = useFeatureFlag('portalImprovements'); + const [membersSignupAccess, portalName, portalSignupTermsHtml, portalSignupCheckboxRequired, portalPlansJson, portalDefaultPlan] = getSettingValues( + localSettings, ['members_signup_access', 'portal_name', 'portal_signup_terms_html', 'portal_signup_checkbox_required', 'portal_plans', 'portal_default_plan'] ); const portalPlans = JSON.parse(portalPlansJson?.toString() || '[]') as string[]; @@ -50,6 +50,20 @@ const SignupOptions: React.FC<{ } updateSetting('portal_plans', JSON.stringify(portalPlans)); + + // Check default plan is included + if (hasPortalImprovements) { + if (portalDefaultPlan === 'yearly') { + if (!portalPlans.includes('yearly') && portalPlans.includes('monthly')) { + updateSetting('portal_default_plan', 'monthly'); + } + } else if (portalDefaultPlan === 'monthly') { + if (!portalPlans.includes('monthly')) { + // If both yearly and monthly are missing from plans, still set it to yearly + updateSetting('portal_default_plan', 'yearly'); + } + } + } }; // This is a bit unclear in current admin, maybe we should add a message if the settings are disabled? @@ -86,13 +100,10 @@ const SignupOptions: React.FC<{ } const paidActiveTiersResult = getPaidActiveTiers(localTiers) || []; - const hasPortalImprovements = useFeatureFlag('portalImprovements'); - - // TODO: Hook up with actual values and then delete this - const selectOptions: SelectOption[] = [ - {value: 'default-yearly', label: 'Yearly'}, - {value: 'default-monthly', label: 'Monthly'} + const defaultPlanOptions: SelectOption[] = [ + {value: 'yearly', label: 'Yearly'}, + {value: 'monthly', label: 'Monthly'} ]; if (paidActiveTiersResult.length > 0 && isStripeEnabled) { @@ -145,9 +156,17 @@ const SignupOptions: React.FC<{ ]} title='Prices available at signup' /> - {hasPortalImprovements && option.value === portalDefaultPlan)} + title='Price shown by default' + onSelect={(option) => { + updateSetting('portal_default_plan', option?.value ?? 'yearly'); + }} + /> + } )} diff --git a/apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts b/apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts index 7be4576717..87d61431bf 100644 --- a/apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts +++ b/apps/admin-x-settings/src/utils/getPortalPreviewUrl.ts @@ -39,7 +39,12 @@ export const getPortalPreviewUrl = ({settings, config, tiers, siteData, selected settingsParam.append('allowSelfSignup', allowSelfSignup ? 'true' : 'false'); settingsParam.append('signupTermsHtml', getSettingValue(settings, 'portal_signup_terms_html') || ''); settingsParam.append('signupCheckboxRequired', getSettingValue(settings, 'portal_signup_checkbox_required') ? 'true' : 'false'); - settingsParam.append('portalProducts', encodeURIComponent(portalTiers.join(','))); // assuming that it might be more than 1 + settingsParam.append('portalProducts', portalTiers.join(',')); // assuming that it might be more than 1 + + const portalDefaultPlan = getSettingValue(settings, 'portal_default_plan'); + if (portalDefaultPlan) { + settingsParam.append('portalDefaultPlan', portalDefaultPlan); + } if (portalPlans && portalPlans.length) { settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(','))); diff --git a/apps/portal/src/App.js b/apps/portal/src/App.js index 8d8557ce70..66b920872d 100644 --- a/apps/portal/src/App.js +++ b/apps/portal/src/App.js @@ -311,7 +311,10 @@ export default class App extends React.Component { // Handle the query params key/value pairs for (let pair of qsParams.entries()) { const key = pair[0]; + + // Note: this needs to be cleaned up, there is no reason why we need to double encode/decode const value = decodeURIComponent(pair[1]); + if (key === 'button') { data.site.portal_button = JSON.parse(value); } else if (key === 'name') { @@ -357,6 +360,8 @@ export default class App extends React.Component { data.site.allow_self_signup = JSON.parse(value); } else if (key === 'membersSignupAccess' && value) { data.site.members_signup_access = value; + } else if (key === 'portalDefaultPlan' && value) { + data.site.portal_default_plan = value; } } data.site.portal_plans = allowedPlans; @@ -389,6 +394,7 @@ export default class App extends React.Component { } ]; } + return data; } diff --git a/apps/portal/src/components/common/ProductsSection.js b/apps/portal/src/components/common/ProductsSection.js index 11c72827fb..7af7e8a636 100644 --- a/apps/portal/src/components/common/ProductsSection.js +++ b/apps/portal/src/components/common/ProductsSection.js @@ -894,7 +894,7 @@ function getSelectedPrice({products, selectedProduct, selectedInterval}) { return selectedPrice; } -function getActiveInterval({portalPlans, selectedInterval = 'year'}) { +function getActiveInterval({portalPlans, portalDefaultPlan, selectedInterval}) { if (selectedInterval === 'month' && portalPlans.includes('monthly')) { return 'month'; } @@ -903,26 +903,32 @@ function getActiveInterval({portalPlans, selectedInterval = 'year'}) { return 'year'; } - if (portalPlans.includes('monthly')) { - return 'month'; + if (portalDefaultPlan) { + if (portalDefaultPlan === 'monthly' && portalPlans.includes('monthly')) { + return 'month'; + } } if (portalPlans.includes('yearly')) { return 'year'; } + + if (portalPlans.includes('monthly')) { + return 'month'; + } } function ProductsSection({onPlanSelect, products, type = null, handleChooseSignup, errors}) { const {site, member, t} = useContext(AppContext); - const {portal_plans: portalPlans} = site; - const defaultInterval = getActiveInterval({portalPlans}); - + const {portal_plans: portalPlans, portal_default_plan: portalDefaultPlan} = site; const defaultProductId = products.length > 0 ? products[0].id : 'free'; - const [selectedInterval, setSelectedInterval] = useState(defaultInterval); + + // Note: by default we set it to null, so that it changes reactively in the preview version of Portal + const [selectedInterval, setSelectedInterval] = useState(null); const [selectedProduct, setSelectedProduct] = useState(defaultProductId); const selectedPrice = getSelectedPrice({products, selectedInterval, selectedProduct}); - const activeInterval = getActiveInterval({portalPlans, selectedInterval}); + const activeInterval = getActiveInterval({portalPlans, portalDefaultPlan, selectedInterval}); const isComplimentary = isComplimentaryMember({member}); diff --git a/ghost/admin/mirage/fixtures/settings.js b/ghost/admin/mirage/fixtures/settings.js index ae2994231c..0f8aa086b8 100644 --- a/ghost/admin/mirage/fixtures/settings.js +++ b/ghost/admin/mirage/fixtures/settings.js @@ -77,6 +77,7 @@ export default [ setting('portal', 'portal_name', true), setting('portal', 'portal_button', true), setting('portal', 'portal_plans', JSON.stringify(['free'])), + setting('portal', 'portal_default_plan', 'yearly'), setting('portal', 'portal_products', JSON.stringify([])), setting('portal', 'portal_button_style', 'icon-and-text'), setting('portal', 'portal_button_icon', null), diff --git a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js index c1de8ce2eb..2de0321a0f 100644 --- a/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js +++ b/ghost/core/core/server/api/endpoints/utils/serializers/input/settings.js @@ -38,6 +38,7 @@ const EDITABLE_SETTINGS = [ 'portal_name', 'portal_button', 'portal_plans', + 'portal_default_plan', 'portal_button_style', 'firstpromoter', 'firstpromoter_id', diff --git a/ghost/core/core/server/data/migrations/versions/5.76/2023-12-05-11-00-add-portal-default-plan-setting.js b/ghost/core/core/server/data/migrations/versions/5.76/2023-12-05-11-00-add-portal-default-plan-setting.js new file mode 100644 index 0000000000..ed76650180 --- /dev/null +++ b/ghost/core/core/server/data/migrations/versions/5.76/2023-12-05-11-00-add-portal-default-plan-setting.js @@ -0,0 +1,8 @@ +const {addSetting} = require('../../utils'); + +module.exports = addSetting({ + key: 'portal_default_plan', + value: 'yearly', + type: 'string', + group: 'portal' +}); diff --git a/ghost/core/core/server/data/schema/default-settings/default-settings.json b/ghost/core/core/server/data/schema/default-settings/default-settings.json index b321087fab..a652ec1544 100644 --- a/ghost/core/core/server/data/schema/default-settings/default-settings.json +++ b/ghost/core/core/server/data/schema/default-settings/default-settings.json @@ -330,6 +330,14 @@ "defaultValue": "[\"free\"]", "type": "array" }, + "portal_default_plan": { + "defaultValue": "yearly", + "validations": { + "isEmpty": false, + "isIn": [["yearly", "monthly"]] + }, + "type": "string" + }, "portal_products": { "defaultValue": "[]", "type": "array" diff --git a/ghost/core/core/shared/settings-cache/public.js b/ghost/core/core/shared/settings-cache/public.js index d06d00db30..fde8a8667e 100644 --- a/ghost/core/core/shared/settings-cache/public.js +++ b/ghost/core/core/shared/settings-cache/public.js @@ -38,6 +38,7 @@ module.exports = { portal_signup_terms_html: 'portal_signup_terms_html', portal_signup_checkbox_required: 'portal_signup_checkbox_required', portal_plans: 'portal_plans', + portal_default_plan: 'portal_default_plan', portal_name: 'portal_name', portal_button: 'portal_button', comments_enabled: 'comments_enabled', diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap index ee03512ffa..83cbd6d906 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/settings.test.js.snap @@ -180,6 +180,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -602,6 +606,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -967,6 +975,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -1143,7 +1155,7 @@ exports[`Settings API Edit Can edit a setting 2: [headers] 1`] = ` Object { "access-control-allow-origin": "http://127.0.0.1:2369", "cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0", - "content-length": "4487", + "content-length": "4534", "content-type": "application/json; charset=utf-8", "content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/, "etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/, @@ -1333,6 +1345,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -1698,6 +1714,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -2526,6 +2546,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -2892,6 +2916,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -3258,6 +3286,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -3628,6 +3660,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -3993,6 +4029,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -4363,6 +4403,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -5097,6 +5141,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -5463,6 +5511,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", @@ -5893,6 +5945,10 @@ Object { "key": "portal_plans", "value": "[\\"free\\"]", }, + Object { + "key": "portal_default_plan", + "value": "yearly", + }, Object { "key": "portal_products", "value": "[]", diff --git a/ghost/core/test/e2e-api/admin/settings.test.js b/ghost/core/test/e2e-api/admin/settings.test.js index f28ba4be6c..21df9cde1c 100644 --- a/ghost/core/test/e2e-api/admin/settings.test.js +++ b/ghost/core/test/e2e-api/admin/settings.test.js @@ -9,15 +9,17 @@ const models = require('../../../core/server/models'); const {mockLabsDisabled, mockLabsEnabled} = require('../../utils/e2e-framework-mock-manager'); const {anyErrorId} = matchers; -const CURRENT_SETTINGS_COUNT = 86; +const CURRENT_SETTINGS_COUNT = 87; const settingsMatcher = {}; const publicHashSettingMatcher = { + key: 'public_hash', value: stringMatching(/[a-z0-9]{30}/) }; const labsSettingMatcher = { + key: 'labs', value: stringMatching(/\{[^\s]+\}/) }; @@ -30,10 +32,10 @@ const matchSettingsArray = (length) => { settingsArray[26] = publicHashSettingMatcher; } - if (length > 60) { + if (length > 61) { // Added a setting that is alphabetically before 'labs'? then you need to increment this counter. // Item at index x is the lab settings, which changes as we add and remove features - settingsArray[60] = labsSettingMatcher; + settingsArray[61] = labsSettingMatcher; } return settingsArray; diff --git a/ghost/core/test/e2e-api/content/__snapshots__/settings.test.js.snap b/ghost/core/test/e2e-api/content/__snapshots__/settings.test.js.snap index e3cfe4c8e2..a103edbdab 100644 --- a/ghost/core/test/e2e-api/content/__snapshots__/settings.test.js.snap +++ b/ghost/core/test/e2e-api/content/__snapshots__/settings.test.js.snap @@ -54,6 +54,7 @@ Object { "portal_button_icon": null, "portal_button_signup_text": "Subscribe", "portal_button_style": "icon-and-text", + "portal_default_plan": "yearly", "portal_name": true, "portal_plans": Array [ "free", diff --git a/ghost/core/test/e2e-api/shared/__snapshots__/version.test.js.snap b/ghost/core/test/e2e-api/shared/__snapshots__/version.test.js.snap index 57a434fd89..2e9ea09beb 100644 --- a/ghost/core/test/e2e-api/shared/__snapshots__/version.test.js.snap +++ b/ghost/core/test/e2e-api/shared/__snapshots__/version.test.js.snap @@ -1406,6 +1406,7 @@ Object { "portal_button_icon": null, "portal_button_signup_text": "Subscribe", "portal_button_style": "icon-and-text", + "portal_default_plan": "yearly", "portal_name": true, "portal_plans": Array [ "free", @@ -1507,6 +1508,7 @@ Object { "portal_button_icon": null, "portal_button_signup_text": "Subscribe", "portal_button_style": "icon-and-text", + "portal_default_plan": "yearly", "portal_name": true, "portal_plans": Array [ "free", diff --git a/ghost/core/test/regression/models/model_settings.test.js b/ghost/core/test/regression/models/model_settings.test.js index 737f3d5d31..e69b1d9891 100644 --- a/ghost/core/test/regression/models/model_settings.test.js +++ b/ghost/core/test/regression/models/model_settings.test.js @@ -5,7 +5,7 @@ const db = require('../../../core/server/data/db'); // Stuff we are testing const models = require('../../../core/server/models'); -const SETTINGS_LENGTH = 93; +const SETTINGS_LENGTH = 94; describe('Settings Model', function () { before(models.init); diff --git a/ghost/core/test/unit/server/data/exporter/index.test.js b/ghost/core/test/unit/server/data/exporter/index.test.js index f90001c994..080940e567 100644 --- a/ghost/core/test/unit/server/data/exporter/index.test.js +++ b/ghost/core/test/unit/server/data/exporter/index.test.js @@ -236,7 +236,7 @@ describe('Exporter', function () { // NOTE: if default settings changed either modify the settings keys blocklist or increase allowedKeysLength // This is a reminder to think about the importer/exporter scenarios ;) - const allowedKeysLength = 85; + const allowedKeysLength = 86; totalKeysLength.should.eql(SETTING_KEYS_BLOCKLIST.length + allowedKeysLength); }); }); diff --git a/ghost/core/test/unit/server/data/schema/integrity.test.js b/ghost/core/test/unit/server/data/schema/integrity.test.js index a778347255..fea8db9b1e 100644 --- a/ghost/core/test/unit/server/data/schema/integrity.test.js +++ b/ghost/core/test/unit/server/data/schema/integrity.test.js @@ -37,7 +37,7 @@ describe('DB version integrity', function () { // Only these variables should need updating const currentSchemaHash = '34a9fa4dc1223ef6c45f8ed991d25de5'; const currentFixturesHash = '4db87173699ad9c9d8a67ccab96dfd2d'; - const currentSettingsHash = '3128d4ec667a50049486b0c21f04be07'; + const currentSettingsHash = '5c957ceb48c4878767d7d3db484c592d'; const currentRoutesHash = '3d180d52c663d173a6be791ef411ed01'; // If this test is failing, then it is likely a change has been made that requires a DB version bump, diff --git a/ghost/core/test/utils/fixtures/default-settings-browser.json b/ghost/core/test/utils/fixtures/default-settings-browser.json index 592fdcd166..c4cf81ae26 100644 --- a/ghost/core/test/utils/fixtures/default-settings-browser.json +++ b/ghost/core/test/utils/fixtures/default-settings-browser.json @@ -326,6 +326,14 @@ "defaultValue": "[\"free\"]", "type": "array" }, + "portal_default_plan": { + "defaultValue": "yearly", + "validations": { + "isEmpty": false, + "isIn": [["yearly", "monthly"]] + }, + "type": "string" + }, "portal_products": { "defaultValue": "[]", "type": "array" diff --git a/ghost/core/test/utils/fixtures/default-settings.json b/ghost/core/test/utils/fixtures/default-settings.json index c039fbe451..8183d55ff5 100644 --- a/ghost/core/test/utils/fixtures/default-settings.json +++ b/ghost/core/test/utils/fixtures/default-settings.json @@ -338,6 +338,14 @@ "defaultValue": "[\"free\"]", "type": "array" }, + "portal_default_plan": { + "defaultValue": "yearly", + "validations": { + "isEmpty": false, + "isIn": [["yearly", "monthly"]] + }, + "type": "string" + }, "portal_products": { "defaultValue": "[]", "type": "array"