Added portal default plan setting (#19238)

fixes PROD-61

This adds a new default plan setting. It defaults to yearly, which is
the current default selected interval in Portal.

Behind the new portal improvements feature flag, the default plan can be
changed. It will also change automatically if the available intervals
are changed.

This PR also wires up passing the new setting to the Portal preview.
This commit is contained in:
Simon Backx 2023-12-06 11:39:58 +01:00 committed by GitHub
parent a38aef7522
commit 3f6ea04c43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 164 additions and 28 deletions

View File

@ -176,6 +176,10 @@
"key": "portal_plans",
"value": "[\"monthly\",\"yearly\",\"free\"]"
},
{
"key": "portal_default_plan",
"value": "yearly"
},
{
"key": "portal_products",
"value": "[]"

View File

@ -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 && <Select disabled={(portalPlans.includes('yearly') && portalPlans.includes('monthly')) ? false : true} options={selectOptions} selectedOption={selectOptions.find(option => option.value === (portalPlans.includes('yearly') ? 'default-yearly' : 'default-monthly'))} title='Price shown by default' onSelect={(value) => {
alert(value);
}} />}
{hasPortalImprovements &&
<Select
disabled={(portalPlans.includes('yearly') && portalPlans.includes('monthly')) ? false : true}
options={defaultPlanOptions}
selectedOption={defaultPlanOptions.find(option => option.value === portalDefaultPlan)}
title='Price shown by default'
onSelect={(option) => {
updateSetting('portal_default_plan', option?.value ?? 'yearly');
}}
/>
}
</>
)}

View File

@ -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<string>(settings, 'portal_default_plan');
if (portalDefaultPlan) {
settingsParam.append('portalDefaultPlan', portalDefaultPlan);
}
if (portalPlans && portalPlans.length) {
settingsParam.append('portalPrices', encodeURIComponent(portalPlans.join(',')));

View File

@ -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;
}

View File

@ -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});

View File

@ -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),

View File

@ -38,6 +38,7 @@ const EDITABLE_SETTINGS = [
'portal_name',
'portal_button',
'portal_plans',
'portal_default_plan',
'portal_button_style',
'firstpromoter',
'firstpromoter_id',

View File

@ -0,0 +1,8 @@
const {addSetting} = require('../../utils');
module.exports = addSetting({
key: 'portal_default_plan',
value: 'yearly',
type: 'string',
group: 'portal'
});

View File

@ -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"

View File

@ -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',

View File

@ -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": "[]",

View File

@ -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;

View File

@ -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",

View File

@ -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",

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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,

View File

@ -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"

View File

@ -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"