🐛 Fixed free tier showing in the tiers-only paywall in posts (#19807)

refs INC-36
fixes https://github.com/TryGhost/Ghost/issues/19796

- The tiers-only paywall was incorrectly rendering "Free". Example:
"This post is for subscribers of the Free, Silver and Gold tiers only"
- Steps to reproduce the issue:
    1. Create a post with public visibility, publish it
    2. Then swap the visibility to specific tiers. The default selects all
paid tiers. Leave it like that
    3. Update the post. The paywall show Free, even though it should be
showing only the paid tiers
- This fix filters out the "free" tier when visibility is set to tiers,
before updating a Post or a Page. The fix includes bulk updates from the
list of Posts and Pages (right-click on a Post/Page > Change Access).
This commit is contained in:
Sag 2024-03-06 21:30:00 +01:00
parent 3090f8ec95
commit 69466ecab9
6 changed files with 481 additions and 2 deletions

View File

@ -62,7 +62,7 @@ module.exports = async (model, frame, options = {}) => {
jsonModel.tiers = tiersData || [];
}
if (jsonModel.visibility === 'paid' && jsonModel.tiers) {
if (['tiers', 'paid'].includes(jsonModel.visibility) && jsonModel.tiers) {
jsonModel.tiers = tiersData ? tiersData.filter(t => t.type === 'paid') : [];
}

View File

@ -1004,6 +1004,12 @@ Post = ghostBookshelf.Model.extend({
this.set('tiers', this.get('tiers').map(t => ({
id: t.id
})));
// Don't associate the free tier with the post
const freeTier = await ghostBookshelf.model('Product').findOne({type: 'free'}, {require: false, transacting: options.transacting ?? undefined});
if (freeTier) {
this.set('tiers', this.get('tiers').filter(t => t.id !== freeTier.id));
}
}
if (labs.isSet('collectionsCard') && this.get('type') === 'post' && (newStatus === 'published' || olderStatus === 'published')) {

View File

@ -799,7 +799,7 @@ Object {
"primary_tag": Any<Object>,
"published_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"show_title_and_feature_image": Any<Boolean>,
"slug": "test-page-2",
"slug": "test-page-3",
"status": "published",
"tags": Any<Array>,
"tiers": Array [
@ -1157,6 +1157,192 @@ Hopefully you don't find it a bore.",
}
`;
exports[`Pages API Update Access Visibility is set to tiers Does not allow to attach the free tier 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": null,
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"og_description": null,
"og_image": null,
"og_title": null,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"show_title_and_feature_image": Any<Boolean>,
"slug": "test-page",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "Test Page",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "tiers",
},
],
}
`;
exports[`Pages API Update Access Visibility is set to tiers Does not allow to attach the free tier 1: [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": "3504",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-cache-invalidate": Any<String>,
"x-powered-by": "Express",
}
`;
exports[`Pages API Update Access Visibility is set to tiers Does not allow to attach the free tier 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": "3492",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-cache-invalidate": Any<String>,
"x-powered-by": "Express",
}
`;
exports[`Pages API Update Access Visibility is set to tiers Saves only paid tiers 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": null,
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"og_description": null,
"og_image": null,
"og_title": null,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"show_title_and_feature_image": Any<Boolean>,
"slug": "test-page-2",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "Test Page",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "tiers",
},
],
}
`;
exports[`Pages API Update Access Visibility is set to tiers Saves only paid tiers 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": "3494",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-cache-invalidate": Any<String>,
"x-powered-by": "Express",
}
`;
exports[`Pages API Update Can modify show_title_and_feature_image property 1: [body] 1`] = `
Object {
"pages": Array [

View File

@ -1889,6 +1889,181 @@ Object {
}
`;
exports[`Posts API Update Access Visibility is set to tiers Does not allow to attach the free tier 1: [body] 1`] = `
Object {
"pages": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"negative_feedback": 0,
"paid_conversions": 0,
"positive_feedback": 0,
"signups": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"excerpt": null,
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"og_description": null,
"og_image": null,
"og_title": null,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"show_title_and_feature_image": true,
"slug": "test-page",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "Test Page",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "tiers",
},
],
}
`;
exports[`Posts API Update Access Visibility is set to tiers Does not allow to attach the free tier 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": "3492",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-cache-invalidate": Any<String>,
"x-powered-by": "Express",
}
`;
exports[`Posts API Update Access Visibility is set to tiers Saves only paid tiers 1: [body] 1`] = `
Object {
"posts": Array [
Object {
"authors": Any<Array>,
"canonical_url": null,
"codeinjection_foot": null,
"codeinjection_head": null,
"comment_id": Any<String>,
"count": Object {
"clicks": 0,
"negative_feedback": 0,
"positive_feedback": 0,
},
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"custom_excerpt": null,
"custom_template": null,
"email": null,
"email_only": false,
"email_segment": "all",
"email_subject": null,
"excerpt": null,
"feature_image": null,
"feature_image_alt": null,
"feature_image_caption": null,
"featured": false,
"frontmatter": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"lexical": "{\\"root\\":{\\"children\\":[{\\"children\\":[],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"paragraph\\",\\"version\\":1}],\\"direction\\":null,\\"format\\":\\"\\",\\"indent\\":0,\\"type\\":\\"root\\",\\"version\\":1}}",
"meta_description": null,
"meta_title": null,
"mobiledoc": null,
"newsletter": null,
"og_description": null,
"og_image": null,
"og_title": null,
"primary_author": Any<Object>,
"primary_tag": Any<Object>,
"published_at": null,
"slug": "test-page",
"status": "draft",
"tags": Any<Array>,
"tiers": Array [
Object {
"active": true,
"created_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"currency": "usd",
"description": null,
"id": StringMatching /\\[a-f0-9\\]\\{24\\}/,
"monthly_price": 500,
"monthly_price_id": null,
"name": "Default Product",
"slug": "default-product",
"trial_days": 0,
"type": "paid",
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"visibility": "public",
"welcome_page_url": null,
"yearly_price": 5000,
"yearly_price_id": null,
},
],
"title": "Test Page",
"twitter_description": null,
"twitter_image": null,
"twitter_title": null,
"updated_at": StringMatching /\\\\d\\{4\\}-\\\\d\\{2\\}-\\\\d\\{2\\}T\\\\d\\{2\\}:\\\\d\\{2\\}:\\\\d\\{2\\}\\\\\\.000Z/,
"url": Any<String>,
"uuid": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"visibility": "tiers",
},
],
}
`;
exports[`Posts API Update Access Visibility is set to tiers Saves only paid tiers 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": "3527",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-cache-invalidate": Any<String>,
"x-powered-by": "Express",
}
`;
exports[`Posts API Update Can add and remove collections 1: [body] 1`] = `
Object {
"posts": Array [

View File

@ -285,6 +285,62 @@ describe('Pages API', function () {
})
.expectStatus(200);
});
describe('Access', function () {
describe('Visibility is set to tiers', function () {
it('Saves only paid tiers', async function () {
const page = {
title: 'Test Page',
status: 'draft'
};
// @ts-ignore
const products = await models.Product.findAll();
const freeTier = products.models[0];
const paidTier = products.models[1];
const {body: pageBody} = await agent
.post('/pages/', {
headers: {
'content-type': 'application/json'
}
})
.body({pages: [page]})
.expectStatus(201);
const [pageResponse] = pageBody.pages;
await agent
.put(`/pages/${pageResponse.id}`)
.body({
pages: [{
id: pageResponse.id,
updated_at: pageResponse.updated_at,
visibility: 'tiers',
tiers: [
{id: freeTier.id},
{id: paidTier.id}
]
}]
})
.expectStatus(200)
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
'x-cache-invalidate': anyString
})
.matchBodySnapshot({
pages: [Object.assign({}, matchPageShallowIncludes, {
published_at: null,
tiers: [
{type: paidTier.get('type'), ...tierSnapshot}
]
})]
});
});
});
});
});
describe('Copy', function () {

View File

@ -671,6 +671,62 @@ describe('Posts API', function () {
should.exist(emptyPageCount);
emptyPageCount.should.equal(totalPageCount, 'post-update empty page count');
});
describe('Access', function () {
describe('Visibility is set to tiers', function () {
it('Saves only paid tiers', async function () {
const post = {
title: 'Test Page',
status: 'draft'
};
// @ts-ignore
const products = await models.Product.findAll();
const freeTier = products.models[0];
const paidTier = products.models[1];
const {body: pageBody} = await agent
.post('/posts/', {
headers: {
'content-type': 'application/json'
}
})
.body({posts: [post]})
.expectStatus(201);
const [pageResponse] = pageBody.posts;
await agent
.put(`/posts/${pageResponse.id}`)
.body({
posts: [{
id: pageResponse.id,
updated_at: pageResponse.updated_at,
visibility: 'tiers',
tiers: [
{id: freeTier.id},
{id: paidTier.id}
]
}]
})
.expectStatus(200)
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag,
'x-cache-invalidate': anyString
})
.matchBodySnapshot({
posts: [Object.assign({}, matchPostShallowIncludes, {
published_at: null,
tiers: [
{type: paidTier.get('type'), ...tierSnapshot}
]
})]
});
});
});
});
});
describe('Delete', function () {