ghost 4.34.0 → 4.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/core/built/assets/ghost-dark-cf32b34face01b83c9f3f30d650ba9a8.css +1 -0
  3. package/core/built/assets/ghost.min-73194305644461704a99cd2eb522637a.css +1 -0
  4. package/core/built/assets/{ghost.min-d329a469b9fa31acdde16e8e3a22cacc.js → ghost.min-f3a45466144e8273938175e32f788438.js} +1521 -1308
  5. package/core/built/assets/icons/get-started-members.svg +6 -0
  6. package/core/built/assets/icons/get-started-migrations.svg +6 -0
  7. package/core/built/assets/icons/get-started.svg +3 -0
  8. package/core/built/assets/{vendor.min-e915c5ce0e8f2aedde80180d0cc09fd6.js → vendor.min-b36a93f4fb64127af926ceefa488b00e.js} +1305 -1261
  9. package/core/frontend/helpers/prev_post.js +1 -1
  10. package/core/frontend/helpers/products.js +2 -6
  11. package/core/frontend/helpers/tpl/content-cta.hbs +1 -1
  12. package/core/frontend/services/routing/controllers/email-post.js +1 -1
  13. package/core/frontend/services/routing/controllers/preview.js +1 -1
  14. package/core/frontend/services/routing/controllers/static.js +1 -1
  15. package/core/frontend/services/routing/helpers/entry-lookup.js +1 -1
  16. package/core/frontend/services/routing/helpers/fetch-data.js +1 -1
  17. package/core/frontend/views/unsubscribe.hbs +4 -2
  18. package/core/server/api/canary/email-post.js +1 -1
  19. package/core/server/api/canary/pages-public.js +1 -1
  20. package/core/server/api/canary/pages.js +1 -1
  21. package/core/server/api/canary/posts-public.js +1 -1
  22. package/core/server/api/canary/posts.js +1 -1
  23. package/core/server/api/canary/utils/serializers/input/pages.js +1 -9
  24. package/core/server/api/canary/utils/serializers/input/posts.js +1 -9
  25. package/core/server/api/canary/utils/serializers/output/email-posts.js +2 -2
  26. package/core/server/api/canary/utils/serializers/output/pages.js +9 -5
  27. package/core/server/api/canary/utils/serializers/output/posts.js +9 -5
  28. package/core/server/api/canary/utils/serializers/output/preview.js +3 -2
  29. package/core/server/api/canary/utils/serializers/output/products.js +2 -0
  30. package/core/server/api/canary/utils/serializers/output/utils/clean.js +0 -9
  31. package/core/server/api/canary/utils/serializers/output/utils/mapper.js +18 -3
  32. package/core/server/api/canary/utils/validators/input/pages.js +1 -1
  33. package/core/server/api/canary/utils/validators/input/posts.js +1 -1
  34. package/core/server/data/exporter/table-lists.js +1 -0
  35. package/core/server/data/migrations/versions/4.35/2022-01-20-05-55-add-post-products-table.js +8 -0
  36. package/core/server/data/migrations/versions/4.35/2022-01-30-15-17-set-welcome-page-url-from-settings.js +45 -0
  37. package/core/server/data/migrations/versions/4.35/2022-02-01-11-48-update-email-recipient-filter-column-type.js +18 -0
  38. package/core/server/data/migrations/versions/4.35/2022-02-01-12-03-update-recipient-filter-column-type.js +18 -0
  39. package/core/server/data/migrations/versions/4.35/2022-02-02-10-38-add-default-content-visibility-tiers-setting.js +8 -0
  40. package/core/server/data/migrations/versions/4.35/2022-02-02-13-10-transform-specific-tiers-default-content-visibility.js +147 -0
  41. package/core/server/data/migrations/versions/4.35/2022-02-04-04-34-populate-empty-portal-products.js +60 -0
  42. package/core/server/data/schema/default-settings.json +4 -0
  43. package/core/server/data/schema/schema.js +10 -4
  44. package/core/server/models/post.js +29 -5
  45. package/core/server/services/mega/template.js +1 -1
  46. package/core/server/services/members/content-gating.js +6 -1
  47. package/core/server/services/members/middleware.js +20 -3
  48. package/core/server/services/members/service.js +34 -8
  49. package/core/server/services/posts/posts-service.js +25 -1
  50. package/core/server/web/admin/views/default-prod.html +4 -4
  51. package/core/server/web/admin/views/default.html +4 -4
  52. package/core/shared/labs.js +3 -2
  53. package/package.json +33 -33
  54. package/yarn.lock +811 -799
  55. package/core/built/assets/ghost-dark-b8ad0651901bb6c34b7bb14aceed5550.css +0 -1
  56. package/core/built/assets/ghost.min-12d943f054c2f01efca103c8f9ca00e0.css +0 -1
@@ -0,0 +1,147 @@
1
+ const logging = require('@tryghost/logging');
2
+
3
+ const {createTransactionalMigration} = require('../../utils');
4
+
5
+ module.exports = createTransactionalMigration(
6
+ async function up(knex) {
7
+ logging.info('Checking default_content_visibility for specific tiers');
8
+
9
+ const settings = await knex('settings')
10
+ .select()
11
+ .whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
12
+ const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
13
+ const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
14
+ if (!contentVisibilitySetting) {
15
+ logging.warn('No default_content_visibility setting found.');
16
+ return;
17
+ }
18
+
19
+ if (!visibilityTiersSetting) {
20
+ logging.warn('No default_content_visibility_tiers setting found.');
21
+ return;
22
+ }
23
+
24
+ const contentVisibility = contentVisibilitySetting.value;
25
+
26
+ if (['public', 'members', 'paid'].includes(contentVisibility)) {
27
+ logging.info(`Ignoring default_content_visibility change as already set to ${contentVisibility}.`);
28
+ return;
29
+ }
30
+ // Transform visibility to tiers when stored as nql string
31
+ const isValidProductNqlFilter = /^(?:product:[\w-]+,?)+$/.test(contentVisibility);
32
+ const now = knex.raw('CURRENT_TIMESTAMP');
33
+ // Reset visibility value to paid if invalid string/filter
34
+ if (!isValidProductNqlFilter) {
35
+ logging.warn(`Found invalid default_content_visibility value - ${contentVisibility}, resetting to paid`);
36
+ await knex('settings')
37
+ .where({
38
+ key: 'default_content_visibility'
39
+ })
40
+ .update({
41
+ value: 'paid',
42
+ updated_at: now
43
+ });
44
+
45
+ logging.info(`Resetting default_content_visibility_tiers to []`);
46
+ await knex('settings')
47
+ .where({
48
+ key: 'default_content_visibility_tiers'
49
+ })
50
+ .update({
51
+ value: JSON.stringify([]),
52
+ updated_at: now
53
+ });
54
+ return;
55
+ }
56
+
57
+ // fetch product slugs from nql filter
58
+ const productSlugs = contentVisibility.split(',').map((segment) => {
59
+ return segment.replace('product:', '');
60
+ });
61
+
62
+ // get product ids for slugs
63
+ const products = await knex('products')
64
+ .select('id')
65
+ .whereIn('slug', productSlugs);
66
+ const productList = products.map((product) => {
67
+ return product.id;
68
+ });
69
+
70
+ logging.info(`Updating default_content_visibility to tiers`);
71
+ await knex('settings')
72
+ .where({
73
+ key: 'default_content_visibility'
74
+ })
75
+ .update({
76
+ value: 'tiers',
77
+ updated_at: now
78
+ });
79
+
80
+ logging.info(`Updating default_content_visibility_tiers to ${productList}`);
81
+ await knex('settings')
82
+ .where({
83
+ key: 'default_content_visibility_tiers'
84
+ })
85
+ .update({
86
+ value: JSON.stringify(productList),
87
+ updated_at: now
88
+ });
89
+ },
90
+ async function down(knex) {
91
+ logging.info('Reverting default_content_visibility for specific tiers');
92
+
93
+ const settings = await knex('settings')
94
+ .select()
95
+ .whereIn('key', ['default_content_visibility', 'default_content_visibility_tiers']);
96
+ const contentVisibilitySetting = settings.find(d => d.key === 'default_content_visibility');
97
+ const visibilityTiersSetting = settings.find(d => d.key === 'default_content_visibility_tiers');
98
+
99
+ const visibilityValue = contentVisibilitySetting && contentVisibilitySetting.value;
100
+ const visibilityTiersValue = visibilityTiersSetting && visibilityTiersSetting.value;
101
+
102
+ if (visibilityValue !== 'tiers') {
103
+ logging.info(`Ignoring default_content_visibility as is set to ${visibilityValue}.`);
104
+ return;
105
+ }
106
+
107
+ if (!visibilityTiersValue) {
108
+ logging.warn(`Ignoring, found empty default_content_visibility_tiers value`);
109
+ return;
110
+ }
111
+
112
+ try {
113
+ const parsedTiersValue = JSON.parse(visibilityTiersValue);
114
+ const products = await knex('products')
115
+ .select('slug')
116
+ .whereIn('id', parsedTiersValue);
117
+ const productSlugs = products.map((product) => {
118
+ return `product:${product.slug}`;
119
+ }).join(',');
120
+ const now = knex.raw('CURRENT_TIMESTAMP');
121
+
122
+ logging.info(`Setting default_content_visibility to ${productSlugs}`);
123
+ await knex('settings')
124
+ .where({
125
+ key: 'default_content_visibility'
126
+ })
127
+ .update({
128
+ value: productSlugs,
129
+ updated_at: now
130
+ });
131
+
132
+ logging.info(`Setting default_content_visibility_tiers to []`);
133
+ await knex('settings')
134
+ .where({
135
+ key: 'default_content_visibility_tiers'
136
+ })
137
+ .update({
138
+ value: JSON.stringify([]),
139
+ updated_at: now
140
+ });
141
+ } catch (e) {
142
+ logging.warn(`Invalid default_content_visibility_tiers value - ${visibilityTiersValue}`);
143
+ logging.warn(e);
144
+ return;
145
+ }
146
+ }
147
+ );
@@ -0,0 +1,60 @@
1
+ const {createTransactionalMigration} = require('../../utils');
2
+ const logging = require('@tryghost/logging');
3
+
4
+ module.exports = createTransactionalMigration(
5
+ async function up(knex) {
6
+ const products = await knex
7
+ .select('id')
8
+ .where({
9
+ type: 'paid'
10
+ })
11
+ .from('products');
12
+
13
+ if (products.length === 0) {
14
+ logging.warn(`Skipping updating portal_products, no product exists`);
15
+ return;
16
+ }
17
+
18
+ if (products.length > 1) {
19
+ logging.warn(`Skipping updating portal_products, tiers beta is enabled`);
20
+ return;
21
+ }
22
+
23
+ const portalProductsSetting = await knex('settings')
24
+ .where('key', 'portal_products')
25
+ .select('value')
26
+ .first();
27
+
28
+ if (!portalProductsSetting) {
29
+ logging.warn(`Missing portal_products setting`);
30
+ return;
31
+ }
32
+ try {
33
+ const currPortalProductsValue = JSON.parse(portalProductsSetting.value);
34
+
35
+ if (currPortalProductsValue.length > 0) {
36
+ logging.warn(`Ignoring - portal_products setting is not empty, - ${currPortalProductsValue}`);
37
+ return;
38
+ }
39
+
40
+ const defaultProduct = products[0];
41
+ const portalProductsValue = [defaultProduct.id];
42
+
43
+ logging.info(`Setting portal_products setting to have product - ${defaultProduct.id}`);
44
+
45
+ const now = knex.raw('CURRENT_TIMESTAMP');
46
+
47
+ await knex('settings')
48
+ .where('key', 'portal_products')
49
+ .update({
50
+ value: JSON.stringify(portalProductsValue),
51
+ updated_at: now
52
+ });
53
+ } catch (e) {
54
+ logging.warn(`Ignoring, unable to parse portal_products setting value - ${portalProductsSetting.value}`);
55
+ logging.warn(e);
56
+ }
57
+ },
58
+ // no-op - we don't want to return to invalid state
59
+ async function down() {}
60
+ );
@@ -235,6 +235,10 @@
235
235
  "defaultValue": "public",
236
236
  "type": "string"
237
237
  },
238
+ "default_content_visibility_tiers": {
239
+ "defaultValue": "[]",
240
+ "type": "array"
241
+ },
238
242
  "members_signup_access": {
239
243
  "defaultValue": "all",
240
244
  "validations": {
@@ -30,8 +30,8 @@ module.exports = {
30
30
  defaultTo: 'public'
31
31
  },
32
32
  email_recipient_filter: {
33
- type: 'string',
34
- maxlength: 50,
33
+ type: 'text',
34
+ maxlength: 1000000000,
35
35
  nullable: false,
36
36
  defaultTo: 'none'
37
37
  },
@@ -424,6 +424,12 @@ module.exports = {
424
424
  product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
425
425
  sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
426
426
  },
427
+ posts_products: {
428
+ id: {type: 'string', maxlength: 24, nullable: false, primary: true},
429
+ post_id: {type: 'string', maxlength: 24, nullable: false, references: 'posts.id', cascadeDelete: true},
430
+ product_id: {type: 'string', maxlength: 24, nullable: false, references: 'products.id', cascadeDelete: true},
431
+ sort_order: {type: 'integer', nullable: false, unsigned: true, defaultTo: 0}
432
+ },
427
433
  members_payment_events: {
428
434
  id: {type: 'string', maxlength: 24, nullable: false, primary: true},
429
435
  member_id: {type: 'string', maxlength: 24, nullable: false, references: 'members.id', cascadeDelete: true},
@@ -592,8 +598,8 @@ module.exports = {
592
598
  validations: {isIn: [['pending', 'submitting', 'submitted', 'failed']]}
593
599
  },
594
600
  recipient_filter: {
595
- type: 'string',
596
- maxlength: 50,
601
+ type: 'text',
602
+ maxlength: 1000000000,
597
603
  nullable: false,
598
604
  defaultTo: 'status:-free'
599
605
  },
@@ -54,9 +54,20 @@ Post = ghostBookshelf.Model.extend({
54
54
  */
55
55
  defaults: function defaults() {
56
56
  let visibility = 'public';
57
-
58
- if (settingsCache.get('default_content_visibility')) {
59
- visibility = settingsCache.get('default_content_visibility');
57
+ let tiers = [];
58
+ const defaultContentVisibility = settingsCache.get('default_content_visibility');
59
+ if (defaultContentVisibility) {
60
+ if (defaultContentVisibility === 'tiers') {
61
+ const tiersData = settingsCache.get('default_content_visibility_tiers') || [];
62
+ visibility = 'tiers',
63
+ tiers = tiersData.map((tierId) => {
64
+ return {
65
+ id: tierId
66
+ };
67
+ });
68
+ } else if (defaultContentVisibility !== 'tiers') {
69
+ visibility = settingsCache.get('default_content_visibility');
70
+ }
60
71
  }
61
72
 
62
73
  return {
@@ -64,16 +75,18 @@ Post = ghostBookshelf.Model.extend({
64
75
  status: 'draft',
65
76
  featured: false,
66
77
  type: 'post',
78
+ tiers,
67
79
  visibility: visibility,
68
80
  email_recipient_filter: 'none'
69
81
  };
70
82
  },
71
83
 
72
- relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta'],
84
+ relationships: ['tags', 'authors', 'mobiledoc_revisions', 'posts_meta', 'tiers'],
73
85
 
74
86
  // NOTE: look up object, not super nice, but was easy to implement
75
87
  relationshipBelongsTo: {
76
88
  tags: 'tags',
89
+ tiers: 'products',
77
90
  authors: 'users',
78
91
  posts_meta: 'posts_meta'
79
92
  },
@@ -89,6 +102,17 @@ Post = ghostBookshelf.Model.extend({
89
102
  }
90
103
  },
91
104
 
105
+ tiers() {
106
+ return this.belongsToMany('Product', 'posts_products', 'post_id', 'product_id')
107
+ .withPivot('sort_order')
108
+ .query('orderBy', 'sort_order', 'ASC')
109
+ .query((qb) => {
110
+ // avoids bookshelf adding a `DISTINCT` to the query
111
+ // we know the result set will already be unique and DISTINCT hurts query performance
112
+ qb.columns('products.*');
113
+ });
114
+ },
115
+
92
116
  parse() {
93
117
  const attrs = ghostBookshelf.Model.prototype.parse.apply(this, arguments);
94
118
 
@@ -171,7 +195,7 @@ Post = ghostBookshelf.Model.extend({
171
195
 
172
196
  // transform visibility NQL queries to special-case values where necessary
173
197
  // ensures checks against special-case values such as `{{#has visibility="paid"}}` continue working
174
- if (attrs.visibility && !['public', 'members', 'paid'].includes(attrs.visibility)) {
198
+ if (attrs.visibility && !['public', 'members', 'paid', 'tiers'].includes(attrs.visibility)) {
175
199
  if (attrs.visibility === 'status:-free') {
176
200
  attrs.visibility = 'paid';
177
201
  } else {
@@ -1151,7 +1151,7 @@ ${ templateSettings.showBadge ? `
1151
1151
  <table role="presentation" border="0" cellpadding="0" cellspacing="0">
1152
1152
  ${ templateSettings.showHeaderIcon && site.iconUrl ? `
1153
1153
  <tr>
1154
- <td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" border="0"></a></td>
1154
+ <td class="site-icon"><a href="${site.url}"><img src="${site.iconUrl}" alt="${site.title}" border="0"></a></td>
1155
1155
  </tr>
1156
1156
  ` : ``}
1157
1157
  ${ templateSettings.showHeaderTitle ? `
@@ -40,7 +40,12 @@ function checkPostAccess(post, member) {
40
40
  return PERMIT_ACCESS;
41
41
  }
42
42
 
43
- const visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
43
+ let visibility = post.visibility === 'paid' ? 'status:-free' : post.visibility;
44
+ if (visibility === 'tiers') {
45
+ visibility = post.tiers.map((product) => {
46
+ return `product:${product.slug}`;
47
+ }).join(',');
48
+ }
44
49
 
45
50
  if (visibility && member.status && nql(visibility, {expansions: MEMBER_NQL_EXPANSIONS}).queryJSON(member)) {
46
51
  return PERMIT_ACCESS;
@@ -1,6 +1,7 @@
1
1
  const _ = require('lodash');
2
2
  const logging = require('@tryghost/logging');
3
3
  const membersService = require('./service');
4
+ const models = require('../../models');
4
5
  const offersService = require('../offers/service');
5
6
  const urlUtils = require('../../../shared/url-utils');
6
7
  const ghostVersion = require('@tryghost/version');
@@ -199,10 +200,26 @@ const createSessionFromMagicLink = async function (req, res, next) {
199
200
 
200
201
  if (action === 'signup') {
201
202
  let customRedirect = '';
202
- if (subscriptions.find(sub => ['active', 'trialing'].includes(sub.status))) {
203
- customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
203
+ const mostRecentActiveSubscription = subscriptions
204
+ .sort((a, b) => {
205
+ const aStartDate = new Date(a.start_date);
206
+ const bStartDate = new Date(b.start_date);
207
+ return bStartDate.valueOf() - aStartDate.valueOf();
208
+ })
209
+ .find(sub => ['active', 'trialing'].includes(sub.status));
210
+ if (mostRecentActiveSubscription) {
211
+ if (labsService.isSet('multipleProducts')) {
212
+ customRedirect = mostRecentActiveSubscription.tier.welcome_page_url;
213
+ } else {
214
+ customRedirect = settingsCache.get('members_paid_signup_redirect') || '';
215
+ }
204
216
  } else {
205
- customRedirect = settingsCache.get('members_free_signup_redirect') || '';
217
+ if (labsService.isSet('multipleProducts')) {
218
+ const freeTier = await models.Product.findOne({type: 'free'});
219
+ customRedirect = freeTier && freeTier.get('welcome_page_url') || '';
220
+ } else {
221
+ customRedirect = settingsCache.get('members_free_signup_redirect') || '';
222
+ }
206
223
  }
207
224
 
208
225
  if (customRedirect && customRedirect !== '/') {
@@ -16,13 +16,12 @@ const models = require('../../models');
16
16
  const {GhostMailer} = require('../mail');
17
17
  const jobsService = require('../jobs');
18
18
  const VerificationTrigger = require('@tryghost/verification-trigger');
19
+ const events = require('../../lib/common/events');
19
20
 
20
21
  const messages = {
21
22
  noLiveKeysInDevelopment: 'Cannot use live stripe keys in development. Please restart in production mode.',
22
23
  sslRequiredForStripe: 'Cannot run Ghost without SSL when Stripe is connected. Please update your url config to use "https://".',
23
- remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.',
24
- emailVerificationNeeded: `We're hard at work processing your import. To make sure you get great deliverability on a list of that size, we'll need to enable some extra features for your account. A member of our team will be in touch with you by email to review your account make sure everything is configured correctly so you're ready to go.`,
25
- emailVerificationEmailMessage: `Email verification needed for site: {siteUrl}, just imported: {importedNumber} members.`
24
+ remoteWebhooksInDevelopment: 'Cannot use remote webhooks in development. See https://ghost.org/docs/webhooks/#stripe-webhooks for developing with Stripe.'
26
25
  };
27
26
 
28
27
  const ghostMailer = new GhostMailer();
@@ -60,10 +59,11 @@ const processImport = async (options) => {
60
59
  delete result.meta.originalImportSize;
61
60
 
62
61
  const importThreshold = await verificationTrigger.getImportThreshold();
63
- if (importThreshold > importSize) {
62
+ if (importSize > importThreshold) {
64
63
  await verificationTrigger.startVerificationProcess({
65
64
  amountImported: importSize,
66
- throwOnTrigger: true
65
+ throwOnTrigger: true,
66
+ source: 'import'
67
67
  });
68
68
  }
69
69
 
@@ -76,6 +76,32 @@ module.exports = {
76
76
  const createMembersApiInstance = require('./api');
77
77
  const env = config.get('env');
78
78
 
79
+ events.on('settings.edited', async function (settingModel) {
80
+ if (labsService.isSet('multipleProducts')) {
81
+ return;
82
+ }
83
+
84
+ const key = settingModel.get('key');
85
+ const value = settingModel.get('value');
86
+
87
+ if (key === 'members_free_signup_redirect') {
88
+ try {
89
+ await models.Product.forge().query().update('welcome_page_url', value).where('type', 'free');
90
+ } catch (err) {
91
+ logging.error(err);
92
+ }
93
+ return;
94
+ }
95
+ if (key === 'members_paid_signup_redirect') {
96
+ try {
97
+ await models.Product.forge().query().update('welcome_page_url', value).where('type', 'paid');
98
+ } catch (err) {
99
+ logging.error(err);
100
+ }
101
+ return;
102
+ }
103
+ });
104
+
79
105
  // @TODO Move to stripe service
80
106
  if (env !== 'production') {
81
107
  if (stripeService.api.configured && stripeService.api.mode === 'live') {
@@ -108,11 +134,11 @@ module.exports = {
108
134
  const fromAddress = config.get('user_email');
109
135
 
110
136
  if (escalationAddress) {
111
- this._ghostMailer.send({
137
+ ghostMailer.send({
112
138
  subject,
113
139
  html: tpl(message, {
114
- amountImported,
115
- siteUrl: this._urlUtils.getSiteUrl()
140
+ importedNumber: amountImported,
141
+ siteUrl: urlUtils.getSiteUrl()
116
142
  }),
117
143
  forceTextContent: true,
118
144
  from: fromAddress,
@@ -1,8 +1,10 @@
1
+ const nql = require('@nexes/nql');
1
2
  const {BadRequestError} = require('@tryghost/errors');
2
3
  const tpl = require('@tryghost/tpl');
3
4
 
4
5
  const messages = {
5
- invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.'
6
+ invalidEmailRecipientFilter: 'Invalid filter in email_recipient_filter param.',
7
+ invalidVisibilityFilter: 'Invalid visibility filter.'
6
8
  };
7
9
 
8
10
  class PostsService {
@@ -78,6 +80,28 @@ class PostsService {
78
80
  return model;
79
81
  }
80
82
 
83
+ async getProductsFromVisibilityFilter(visibilityFilter) {
84
+ try {
85
+ const allProducts = await this.models.Product.findAll();
86
+ const visibilityFilterJson = nql(visibilityFilter).toJSON();
87
+ const productsData = (visibilityFilterJson.product ? [visibilityFilterJson] : visibilityFilterJson.$or) || [];
88
+ const tiers = productsData
89
+ .map((data) => {
90
+ return allProducts.find((p) => {
91
+ return p.get('slug') === data.product;
92
+ });
93
+ }).filter(p => !!p).map((d) => {
94
+ return d.toJSON();
95
+ });
96
+ return tiers;
97
+ } catch (err) {
98
+ return Promise.reject(new BadRequestError({
99
+ message: tpl(messages.invalidVisibilityFilter),
100
+ context: err.message
101
+ }));
102
+ }
103
+ }
104
+
81
105
  /**
82
106
  * Calculates if the email should be tried to be sent out
83
107
  * @private
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.34%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.35%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-12d943f054c2f01efca103c8f9ca00e0.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-73194305644461704a99cd2eb522637a.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-e915c5ce0e8f2aedde80180d0cc09fd6.js"></script>
60
- <script src="assets/ghost.min-d329a469b9fa31acdde16e8e3a22cacc.js"></script>
59
+ <script src="assets/vendor.min-b36a93f4fb64127af926ceefa488b00e.js"></script>
60
+ <script src="assets/ghost.min-f3a45466144e8273938175e32f788438.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -8,7 +8,7 @@
8
8
  <title>Ghost Admin</title>
9
9
 
10
10
 
11
- <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.34%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
11
+ <meta name="ghost-admin/config/environment" content="%7B%22modulePrefix%22%3A%22ghost-admin%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22%2F%22%2C%22locationType%22%3A%22trailing-hash%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%2C%22Array%22%3Atrue%2C%22String%22%3Atrue%2C%22Function%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_JQUERY_INTEGRATION%22%3Atrue%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22version%22%3A%224.35%22%2C%22name%22%3A%22ghost-admin%22%7D%2C%22ember-simple-auth%22%3A%7B%7D%2C%22moment%22%3A%7B%22includeTimezone%22%3A%22all%22%7D%2C%22%40sentry%2Fember%22%3A%7B%22disablePerformance%22%3Atrue%2C%22sentry%22%3A%7B%7D%7D%2C%22ember-cli-mirage%22%3A%7B%22usingProxy%22%3Afalse%2C%22useDefaultPassthroughs%22%3Atrue%7D%2C%22exportApplicationGlobal%22%3Afalse%2C%22ember-load%22%3A%7B%22loadingIndicatorClass%22%3A%22ember-load-indicator%22%7D%7D" />
12
12
 
13
13
  <meta name="HandheldFriendly" content="True" />
14
14
  <meta name="MobileOptimized" content="320" />
@@ -38,7 +38,7 @@
38
38
 
39
39
 
40
40
  <link rel="stylesheet" href="assets/vendor.min-2c8ad32b7960bb605ebc20097fee5ebd.css">
41
- <link rel="stylesheet" href="assets/ghost.min-12d943f054c2f01efca103c8f9ca00e0.css" title="light">
41
+ <link rel="stylesheet" href="assets/ghost.min-73194305644461704a99cd2eb522637a.css" title="light">
42
42
 
43
43
 
44
44
 
@@ -56,8 +56,8 @@
56
56
  <div id="ember-basic-dropdown-wormhole"></div>
57
57
 
58
58
 
59
- <script src="assets/vendor.min-e915c5ce0e8f2aedde80180d0cc09fd6.js"></script>
60
- <script src="assets/ghost.min-d329a469b9fa31acdde16e8e3a22cacc.js"></script>
59
+ <script src="assets/vendor.min-b36a93f4fb64127af926ceefa488b00e.js"></script>
60
+ <script src="assets/ghost.min-f3a45466144e8273938175e32f788438.js"></script>
61
61
 
62
62
  </body>
63
63
  </html>
@@ -27,11 +27,12 @@ const BETA_FEATURES = [
27
27
  const ALPHA_FEATURES = [
28
28
  'oauthLogin',
29
29
  'membersActivity',
30
- 'cardSettingsPanel',
31
30
  'urlCache',
32
31
  'beforeAfterCard',
33
32
  'tweetGridCard',
34
- 'membersActivityFeed'
33
+ 'membersActivityFeed',
34
+ 'improvedOnboarding',
35
+ 'tierWelcomePages'
35
36
  ];
36
37
 
37
38
  module.exports.GA_KEYS = [...GA_FEATURES];