ghost 6.18.2 → 6.19.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.
- package/components/tryghost-i18n-6.19.0.tgz +0 -0
- package/components/tryghost-parse-email-address-6.19.0.tgz +0 -0
- package/core/built/admin/assets/{PolarAngleAxis-DALH8FDm.js → PolarAngleAxis-CGprvq8M.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-D_UsvJRN.js → _baseAssignValue-Bwn07Ln5.js} +1 -1
- package/core/built/admin/assets/{a-large-small-DVyx4GMu.js → a-large-small-p58xibEK.js} +1 -1
- package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
- package/core/built/admin/assets/admin-x-settings/{code-editor-view-DBrulgE8.mjs → code-editor-view-DCKvO1TY.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DgGwb1L-.mjs → index-BxWVtVnD.mjs} +6 -6
- package/core/built/admin/assets/admin-x-settings/{index-Cypgljb3.mjs → index-DyZIONe1.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-CsGHbqSU.mjs → index-HfMJcj2U.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-rdo7i0D3.mjs → modals-CIiqfKY0.mjs} +7593 -7509
- package/core/built/admin/assets/{at-sign-CGNkBrZS.js → at-sign-BZPuAx5m.js} +1 -1
- package/core/built/admin/assets/audience-Cy9gLIa9.js +1 -0
- package/core/built/admin/assets/{avatar-flipboard-jqr9Aapd.js → avatar-flipboard-CLmWb5Xn.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-C2749SVt.js → bluesky-sharing-BHt0oiii.js} +1 -1
- package/core/built/admin/assets/{chart-BAQCVPCH.js → chart--RFLGIL4.js} +1 -1
- package/core/built/admin/assets/{chunk.524.428356d01feabbc7b932.js → chunk.524.5c5c3f3273a86d1d9913.js} +7 -7
- package/core/built/admin/assets/{chunk.582.b8b41ba720f49d724992.js → chunk.582.e6c787cdf7e6de7819e0.js} +9 -9
- package/core/built/admin/assets/{code-editor-view-YNhJ1w71.js → code-editor-view-BW9y0JXN.js} +1 -1
- package/core/built/admin/assets/comments-C_lilDBp.js +1 -0
- package/core/built/admin/assets/content-helpers-BOnQQADZ.js +1 -0
- package/core/built/admin/assets/{copy-CNsHZEFR.js → copy-Bi7VpgPR.js} +1 -1
- package/core/built/admin/assets/{data-list-Dn5MkmyD.js → data-list-Bu3jzs1s.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-Dmm_clMl.js → deleted-feed-item-B-doIIKc.js} +1 -1
- package/core/built/admin/assets/{edit-profile-BLSdtROQ.js → edit-profile-ZBdjFfvL.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-C2Z0dddU.js → empty-indicator-C6BBh99D.js} +1 -1
- package/core/built/admin/assets/{en-DuTkaMAI.js → en-C7eSFQRw.js} +1 -1
- package/core/built/admin/assets/{feed-B4Xp4C2J.js → feed-CAioauwD.js} +1 -1
- package/core/built/admin/assets/{filters-gnOx5Z45.js → filters-DEKF0tIB.js} +1 -1
- package/core/built/admin/assets/{gh-chart-B2aWYkGg.js → gh-chart-D9KI0_sO.js} +1 -1
- package/core/built/admin/assets/{ghost-29f8f3c80e41126ba5e3c52ad5727e8a.js → ghost-6bff5c535ef015b0ed9421ceb6c699a7.js} +220 -211
- package/core/built/admin/assets/{growth-BysawIWe.js → growth-C6XGchvT.js} +1 -1
- package/core/built/admin/assets/{hash-j3sh-UI3.js → hash-DFhnGHId.js} +1 -1
- package/core/built/admin/assets/{inbox-1cUdr8Mm.js → inbox-p-Y5uScT.js} +1 -1
- package/core/built/admin/assets/{index-BVoBTlp_.js → index--VlvGKWi.js} +1 -1
- package/core/built/admin/assets/{index-DGBK5k9H.js → index-3uU4zg_6.js} +1 -1
- package/core/built/admin/assets/index-4ZkTY34B.css +1 -0
- package/core/built/admin/assets/{index-BhY_SbGB.js → index-BWnrLoWI.js} +1 -1
- package/core/built/admin/assets/{index-I9n711UW.js → index-BzU2odQq.js} +1 -1
- package/core/built/admin/assets/{index-mhojl7aD.js → index-CIZ_upe2.js} +1 -1
- package/core/built/admin/assets/{index-WjY3mGep.js → index-CS4XqyC-.js} +2 -2
- package/core/built/admin/assets/{index-BgrwCpgw.js → index-D17PEsUB.js} +1 -1
- package/core/built/admin/assets/{index-B8G0f0hb.js → index-DCTdAEHx.js} +3 -3
- package/core/built/admin/assets/{index-DarAAzVH.js → index-FPf-AERu.js} +1 -1
- package/core/built/admin/assets/index-JmqyGBro.js +1 -0
- package/core/built/admin/assets/{index-DKAlEWM4.js → index-LyfDjTto.js} +1 -1
- package/core/built/admin/assets/index-S62-c8TV.js +13 -0
- package/core/built/admin/assets/{index-D8CXbJm4.js → index-Wx8_AGZX.js} +1 -1
- package/core/built/admin/assets/{index-CkAUXgAi.js → index-jAidr8jI.js} +1 -1
- package/core/built/admin/assets/{index-Dz8eTtOq.js → index-lPLmWtv0.js} +1 -1
- package/core/built/admin/assets/koenig-lexical/index.css +1 -1
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.js +1853 -1863
- package/core/built/admin/assets/koenig-lexical/koenig-lexical.umd.js +94 -94
- package/core/built/admin/assets/{koenig-lexical-coiS9dIC.js → koenig-lexical-CWnkYKQt.js} +77 -77
- package/core/built/admin/assets/{kpi-card-BEFHo2gr.js → kpi-card-B5UaJgOT.js} +1 -1
- package/core/built/admin/assets/{kpis-D-gz-lUk.js → kpis-Cc2RxnvZ.js} +1 -1
- package/core/built/admin/assets/{label-BvlHzzcJ.js → label-CW7Kmp9w.js} +1 -1
- package/core/built/admin/assets/{links-tdJvBrsT.js → links-wWNc9Uat.js} +1 -1
- package/core/built/admin/assets/{list-filter-DPE4Xj2T.js → list-filter-3EF6cVg-.js} +1 -1
- package/core/built/admin/assets/{lucide-react-ClQ3Iy7l.js → lucide-react-DtMIZssr.js} +214 -214
- package/core/built/admin/assets/{main-layout-DY96wK-_.js → main-layout-BFYhCrLn.js} +1 -1
- package/core/built/admin/assets/{message-square-text-Dd1XS5-H.js → message-square-text-BDZtUW-2.js} +1 -1
- package/core/built/admin/assets/{minus-CyEc3BE5.js → minus-CSphShdJ.js} +1 -1
- package/core/built/admin/assets/modals-CDWP04Iy.js +77 -0
- package/core/built/admin/assets/{moderation-B0fdmlig.js → moderation-qcng2_lB.js} +1 -1
- package/core/built/admin/assets/{newsletter-Byg3ARa5.js → newsletter-R-IYcV4M.js} +1 -1
- package/core/built/admin/assets/{newsletters-DbH39vgA.js → newsletters-BB4e24i1.js} +1 -1
- package/core/built/admin/assets/{note-Cwy-rIAF.js → note-B_bpRX66.js} +1 -1
- package/core/built/admin/assets/overview-Nh-IoP-v.js +1 -0
- package/core/built/admin/assets/{pagemenu-CkzsZrHC.js → pagemenu-Cd45frnF.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-BLkup0Xh.js → post-analytics-context-BST4bzps.js} +1 -1
- package/core/built/admin/assets/post-analytics-header-DAZPp6M8.js +1 -0
- package/core/built/admin/assets/{post-analytics-aA-nG9Fq.js → post-analytics-jHZOhQ8L.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-CkvJtrRw.js → post-share-modal-D1JQfuYx.js} +1 -1
- package/core/built/admin/assets/posts/{comments-Bn_vn_sb.mjs → comments-C0ntS6jH.mjs} +678 -692
- package/core/built/admin/assets/posts/{dialog-CKPcMrj7.mjs → dialog-CXEmd-rC.mjs} +3 -3
- package/core/built/admin/assets/posts/{empty-indicator-kkrhP6K_.mjs → empty-indicator-CY1NEQZt.mjs} +2 -2
- package/core/built/admin/assets/posts/{filters-DHsxxy0F.mjs → filters-CWWDQAf0.mjs} +6 -6
- package/core/built/admin/assets/posts/{growth-D7ACh-oR.mjs → growth-DUnL-W24.mjs} +20 -19
- package/core/built/admin/assets/posts/{heading-BrKVbOxA.mjs → heading-DZWin3nG.mjs} +2 -2
- package/core/built/admin/assets/posts/{hooks-BBEMuLiW.mjs → hooks-DQYjPsLg.mjs} +2 -2
- package/core/built/admin/assets/posts/{index-C0rRguZx.mjs → index-BiGQFQkQ.mjs} +9 -9
- package/core/built/admin/assets/posts/{kpis-1o524OIK.mjs → kpis-DNnkyd0U.mjs} +10 -10
- package/core/built/admin/assets/posts/{links-JpeATx1f.mjs → links-D3nSZFlJ.mjs} +4 -4
- package/core/built/admin/assets/posts/{loading-indicator-BHAmSf8j.mjs → loading-indicator-CZQmPRvv.mjs} +3 -3
- package/core/built/admin/assets/posts/{main-layout-BTItAOQE.mjs → main-layout-D5WnaGr4.mjs} +2 -2
- package/core/built/admin/assets/posts/{newsletter-CQdsCEHv.mjs → newsletter-C5eikVBq.mjs} +13 -13
- package/core/built/admin/assets/posts/{overview-sYh4JN1D.mjs → overview-C5mCMPJY.mjs} +113 -112
- package/core/built/admin/assets/posts/{post-analytics-DtLt2SWx.mjs → post-analytics-Cc2NCQis.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-CB1yM9fk.mjs → post-analytics-context-qXWjDzSt.mjs} +44 -41
- package/core/built/admin/assets/posts/{post-analytics-header-BYaWuL9W.mjs → post-analytics-header-QGsYyvhA.mjs} +11 -11
- package/core/built/admin/assets/posts/{post-share-modal-V0HTOBaf.mjs → post-share-modal-B9Q7FJsH.mjs} +4 -4
- package/core/built/admin/assets/posts/posts-C2SKGLoU.mjs +17 -0
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/posts/{search-CLjC37AT.mjs → search-C8k0TFkh.mjs} +2 -2
- package/core/built/admin/assets/posts/{separator-KNoTIaJx.mjs → separator-CJnnsW1a.mjs} +5 -5
- package/core/built/admin/assets/posts/{sheet-DBg_SCDt.mjs → sheet-HDzKgNHc.mjs} +3 -3
- package/core/built/admin/assets/posts/{skeleton-DvsoolWu.mjs → skeleton-BlZGKNzB.mjs} +3 -3
- package/core/built/admin/assets/posts/{source-icon-OmRaTU2G.mjs → source-icon-DjBIjtaY.mjs} +3 -3
- package/core/built/admin/assets/posts/{stats-BC2DzntY.mjs → stats-BRVHejqx.mjs} +4 -4
- package/core/built/admin/assets/posts/{table-CxX9OKAj.mjs → table-D7AEpPWL.mjs} +2 -2
- package/core/built/admin/assets/posts/{tabs-B1jw7cBi.mjs → tabs-RcpmzDgr.mjs} +10 -10
- package/core/built/admin/assets/posts/{tags-BitrLT_j.mjs → tags-8k1xw8R8.mjs} +2 -2
- package/core/built/admin/assets/posts/{tags-BBilTo1a.mjs → tags-DNY2rBlf.mjs} +11 -11
- package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-DaijA1ao.mjs → use-infinite-virtual-scroll-CMiIpcDA.mjs} +3 -3
- package/core/built/admin/assets/posts/{web-CKmyC4Xj.mjs → web-BCl5J6bB.mjs} +15 -15
- package/core/built/admin/assets/{posts-DDkuYoN7.js → posts-DFiJPOft.js} +1 -1
- package/core/built/admin/assets/{repeat-B-SL0yPM.js → repeat-Smml9n0k.js} +1 -1
- package/core/built/admin/assets/{reply-BdCPoUQ9.js → reply-CooY4lmk.js} +1 -1
- package/core/built/admin/assets/{select-C7aNW8QS.js → select-WwMsbBbg.js} +1 -1
- package/core/built/admin/assets/{settings-D-dhma2e.js → settings-CQEhPDQU.js} +4 -4
- package/core/built/admin/assets/{settings-Bzy1GNfD.js → settings-CxWtvcUf.js} +1 -1
- package/core/built/admin/assets/{sort-button-Dv8vjh13.js → sort-button-DFr8vezl.js} +1 -1
- package/core/built/admin/assets/{source-icon-DTz4isLK.js → source-icon-VpYWpwYb.js} +1 -1
- package/core/built/admin/assets/{sprout-BwLQTzMf.js → sprout-DRqlFJSK.js} +1 -1
- package/core/built/admin/assets/{square-YF1YE9ex.js → square-CwHQ9sJ2.js} +1 -1
- package/core/built/admin/assets/stats/audience-Caf7BjaU.mjs +269 -0
- package/core/built/admin/assets/stats/{url-helpers-Drq3xg0l.mjs → content-helpers-MbtAvUJh.mjs} +83 -106
- package/core/built/admin/assets/stats/{index-CcCyLMxL.mjs → index-BGqKbF1D.mjs} +8 -8
- package/core/built/admin/assets/stats/{index-DXU2rE9t.mjs → index-BTjfXOi6.mjs} +30 -30
- package/core/built/admin/assets/stats/{index-K7ASx7EG.mjs → index-ChjIxhHy.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-D5mlMG4l.mjs → index-DMkZbbZs.mjs} +14 -14
- package/core/built/admin/assets/stats/{index-CUuQaROI.mjs → index-wT0OIw_N.mjs} +355 -335
- package/core/built/admin/assets/stats/{sort-button-CELUx6Zp.mjs → sort-button-B8n8fYMi.mjs} +23 -23
- package/core/built/admin/assets/stats/{stats-d_u_in4l.mjs → stats-CGH-0JNs.mjs} +120 -119
- package/core/built/admin/assets/stats/stats.js +1 -1
- package/core/built/admin/assets/stats/{tabs-3wLZsy0v.mjs → tabs-EQCUj3Y9.mjs} +19 -19
- package/core/built/admin/assets/stats/{use-growth-stats-28Sr42va.mjs → use-growth-stats--CIfag21.mjs} +3 -3
- package/core/built/admin/assets/{stats-C5ad0fgQ.js → stats-lnFiOQZx.js} +1 -1
- package/core/built/admin/assets/{stats-view-BvkxPYNX.js → stats-view-BJVawuIv.js} +1 -1
- package/core/built/admin/assets/{step-1-D7_s-D99.js → step-1-Bx8MiQVF.js} +1 -1
- package/core/built/admin/assets/{step-2-B94Yf7FF.js → step-2-CLLXppER.js} +1 -1
- package/core/built/admin/assets/{step-3-DswXYYf4.js → step-3-C5WrQe8u.js} +2 -2
- package/core/built/admin/assets/{table-D30IXfUP.js → table-2waWpIF5.js} +1 -1
- package/core/built/admin/assets/{tabs-CjNdfW0y.js → tabs-CBD0hW8V.js} +1 -1
- package/core/built/admin/assets/{tags-B0ux9_dT.js → tags-Bgw5Hrb5.js} +1 -1
- package/core/built/admin/assets/{tags-PGeGAafJ.js → tags-CKrBptuC.js} +1 -1
- package/core/built/admin/assets/{tiers-BaXK0JoI.js → tiers-CO-pz1FX.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DdY8HF3Y.js → toggle-group-BXAZzvJ9.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Boh22uGD.js → topic-filter-U3qUH2P_.js} +1 -1
- package/core/built/admin/assets/{trash-D7ZWrnDq.js → trash-CJY1Wro_.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-Bg0nE0WG.js → use-growth-stats-BrLZI7da.js} +1 -1
- package/core/built/admin/assets/{use-infinite-virtual-scroll-DoeI5IY-.js → use-infinite-virtual-scroll-CiRn3kpz.js} +2 -2
- package/core/built/admin/assets/{use-simple-pagination-BJzBULR3.js → use-simple-pagination-CskJ0MDP.js} +1 -1
- package/core/built/admin/assets/{user-round-check-BLc3L-ei.js → user-round-check-zOuzV3OS.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-xX4QZik7.js → wallet-cards-ddLuIN-b.js} +1 -1
- package/core/built/admin/assets/{web-DQ2qBymm.js → web-CGbkZWn3.js} +1 -1
- package/core/built/admin/index.html +6 -5
- package/core/frontend/services/theme-engine/i18n/i18n.js +9 -14
- package/core/server/api/endpoints/settings-public.js +3 -1
- package/core/server/api/endpoints/utils/serializers/input/settings.js +6 -1
- package/core/server/data/migrations/versions/6.19/2026-02-10-12-00-00-add-transistor-portal-settings.js +39 -0
- package/core/server/data/schema/default-settings/default-settings.json +28 -0
- package/core/server/data/schema/schema.js +2 -2
- package/core/server/data/seeders/importers/offers-importer.js +2 -2
- package/core/server/data/tinybird/endpoints/api_top_pages_router.pipe +28 -0
- package/core/server/data/tinybird/endpoints/api_top_pages_v3.pipe +8 -4
- package/core/server/data/tinybird/tests/api_top_pages_router.yaml +74 -0
- package/core/server/services/comments/comments-service-emails.js +1 -4
- package/core/server/services/email-service/email-templates/partials/styles.hbs +43 -0
- package/core/server/services/koenig/node-renderers/transistor-renderer.js +23 -17
- package/core/server/services/members/members-api/repositories/member-repository.js +30 -6
- package/core/server/services/members/members-api/services/next-payment-calculator.js +28 -20
- package/core/server/services/members/members-api/services/payments-service.js +1 -1
- package/core/server/services/members/members-api/utils/add-calendar-months.js +49 -0
- package/core/server/services/offers/application/offer-mapper.js +2 -2
- package/core/server/services/offers/domain/models/offer-amount.js +16 -0
- package/core/server/services/offers/domain/models/offer-duration.js +3 -4
- package/core/server/services/offers/domain/models/offer-type.js +5 -3
- package/core/server/services/offers/domain/models/offer.js +8 -1
- package/core/server/services/staff/staff-service-emails.js +2 -0
- package/core/server/services/stripe/stripe-api.js +19 -0
- package/core/shared/config/defaults.json +1 -1
- package/core/shared/labs.js +0 -1
- package/core/shared/settings-cache/cache-manager.js +8 -0
- package/core/shared/settings-cache/public.js +5 -0
- package/package.json +5 -5
- package/yarn.lock +4 -4
- package/components/tryghost-i18n-6.18.2.tgz +0 -0
- package/components/tryghost-parse-email-address-6.18.2.tgz +0 -0
- package/core/built/admin/assets/audience-CJHVR7kD.js +0 -1
- package/core/built/admin/assets/comments-CPd_iCc3.js +0 -1
- package/core/built/admin/assets/index-B2yksBz4.js +0 -13
- package/core/built/admin/assets/index-Cl_EPbQ2.js +0 -1
- package/core/built/admin/assets/index-D_sGUCda.css +0 -1
- package/core/built/admin/assets/modals-C06GcVIm.js +0 -77
- package/core/built/admin/assets/overview-CVMLGrVt.js +0 -1
- package/core/built/admin/assets/post-analytics-header-D9srAPT5.js +0 -1
- package/core/built/admin/assets/posts/posts-DeQT3knv.mjs +0 -21
- package/core/built/admin/assets/stats/audience-BWqU7WWT.mjs +0 -245
- package/core/built/admin/assets/url-helpers-mt6MBIi0.js +0 -1
|
@@ -3,7 +3,6 @@ const logging = require('@tryghost/logging');
|
|
|
3
3
|
const fs = require('fs-extra');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const MessageFormat = require('intl-messageformat');
|
|
6
|
-
const jp = require('jsonpath');
|
|
7
6
|
const isString = require('lodash/isString');
|
|
8
7
|
const isObject = require('lodash/isObject');
|
|
9
8
|
const isEqual = require('lodash/isEqual');
|
|
@@ -128,29 +127,25 @@ class I18n {
|
|
|
128
127
|
}
|
|
129
128
|
|
|
130
129
|
/**
|
|
131
|
-
* Do the lookup within the
|
|
130
|
+
* Do the lookup within the translation strings
|
|
132
131
|
*
|
|
133
132
|
* @param {String} msgPath
|
|
134
133
|
*/
|
|
135
134
|
_getCandidateString(msgPath) {
|
|
136
|
-
// Our default string mode is "dot" for dot-notation, e.g. $.something.like.this used in the backend
|
|
137
|
-
// Both jsonpath's dot-notation and bracket-notation start with '$' E.g.: $.store.book.title or $['store']['book']['title']
|
|
138
|
-
// While bracket-notation allows any Unicode characters in keys (i.e. for themes / fulltext mode) E.g. $['Read more']
|
|
139
|
-
// dot-notation allows only word characters in keys for backend messages (that is \w or [A-Za-z0-9_] in RegExp)
|
|
140
|
-
let jsonPath = `$.${msgPath}`;
|
|
141
135
|
let fallback = null;
|
|
142
136
|
|
|
143
137
|
if (this._stringMode === 'fulltext') {
|
|
144
|
-
jsonPath = jp.stringify(['$', msgPath]);
|
|
145
|
-
// In fulltext mode we can use the passed string as a fallback
|
|
146
138
|
fallback = msgPath;
|
|
139
|
+
} else if (/[^\w.]/.test(msgPath)) {
|
|
140
|
+
// In dot mode, keys must only contain word characters and dots.
|
|
141
|
+
// Reject anything else to match previous behavior.
|
|
142
|
+
this._handleInvalidKeyError(msgPath, new errors.InternalServerError({message: 'Invalid dot-notation path'}));
|
|
147
143
|
}
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
145
|
+
// Use array form [msgPath] for fulltext mode to prevent lodash splitting on dots.
|
|
146
|
+
// Use string form for dot mode so lodash splits 'a.b.c' into nested lookup.
|
|
147
|
+
const lookupPath = this._stringMode === 'fulltext' ? [msgPath] : msgPath;
|
|
148
|
+
return get(this._strings, lookupPath) || fallback;
|
|
154
149
|
}
|
|
155
150
|
|
|
156
151
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const settingsCache = require('../../../shared/settings-cache');
|
|
2
|
+
const labs = require('../../../shared/labs');
|
|
2
3
|
const urlUtils = require('../../../shared/url-utils');
|
|
3
4
|
const ghostVersion = require('@tryghost/version');
|
|
4
5
|
|
|
@@ -17,7 +18,8 @@ const controller = {
|
|
|
17
18
|
return Object.assign({},
|
|
18
19
|
settingsCache.getPublic(), {
|
|
19
20
|
url: urlUtils.urlFor('home', true),
|
|
20
|
-
version: ghostVersion.safe
|
|
21
|
+
version: ghostVersion.safe,
|
|
22
|
+
labs: labs.getAll()
|
|
21
23
|
}
|
|
22
24
|
);
|
|
23
25
|
}
|
|
@@ -80,7 +80,12 @@ const EDITABLE_SETTINGS = [
|
|
|
80
80
|
'explore_ping',
|
|
81
81
|
'explore_ping_growth',
|
|
82
82
|
'indexnow_api_key',
|
|
83
|
-
'transistor'
|
|
83
|
+
'transistor',
|
|
84
|
+
'transistor_portal_enabled',
|
|
85
|
+
'transistor_portal_heading',
|
|
86
|
+
'transistor_portal_description',
|
|
87
|
+
'transistor_portal_button_text',
|
|
88
|
+
'transistor_portal_url_template'
|
|
84
89
|
];
|
|
85
90
|
|
|
86
91
|
module.exports = {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const {combineTransactionalMigrations, addSetting} = require('../../utils');
|
|
2
|
+
|
|
3
|
+
module.exports = combineTransactionalMigrations(
|
|
4
|
+
addSetting({
|
|
5
|
+
key: 'transistor_portal_enabled',
|
|
6
|
+
value: 'true',
|
|
7
|
+
type: 'boolean',
|
|
8
|
+
group: 'transistor',
|
|
9
|
+
flags: 'PUBLIC'
|
|
10
|
+
}),
|
|
11
|
+
addSetting({
|
|
12
|
+
key: 'transistor_portal_heading',
|
|
13
|
+
value: 'Podcasts',
|
|
14
|
+
type: 'string',
|
|
15
|
+
group: 'transistor',
|
|
16
|
+
flags: 'PUBLIC'
|
|
17
|
+
}),
|
|
18
|
+
addSetting({
|
|
19
|
+
key: 'transistor_portal_description',
|
|
20
|
+
value: 'Access your private podcast feed',
|
|
21
|
+
type: 'string',
|
|
22
|
+
group: 'transistor',
|
|
23
|
+
flags: 'PUBLIC'
|
|
24
|
+
}),
|
|
25
|
+
addSetting({
|
|
26
|
+
key: 'transistor_portal_button_text',
|
|
27
|
+
value: 'View',
|
|
28
|
+
type: 'string',
|
|
29
|
+
group: 'transistor',
|
|
30
|
+
flags: 'PUBLIC'
|
|
31
|
+
}),
|
|
32
|
+
addSetting({
|
|
33
|
+
key: 'transistor_portal_url_template',
|
|
34
|
+
value: 'https://partner.transistor.fm/ghost/{memberUuid}',
|
|
35
|
+
type: 'string',
|
|
36
|
+
group: 'transistor',
|
|
37
|
+
flags: 'PUBLIC'
|
|
38
|
+
})
|
|
39
|
+
);
|
|
@@ -486,6 +486,34 @@
|
|
|
486
486
|
"isIn": [["true", "false"]]
|
|
487
487
|
},
|
|
488
488
|
"type": "boolean"
|
|
489
|
+
},
|
|
490
|
+
"transistor_portal_enabled": {
|
|
491
|
+
"defaultValue": "true",
|
|
492
|
+
"validations": {
|
|
493
|
+
"isIn": [["true", "false"]]
|
|
494
|
+
},
|
|
495
|
+
"type": "boolean",
|
|
496
|
+
"flags": "PUBLIC"
|
|
497
|
+
},
|
|
498
|
+
"transistor_portal_heading": {
|
|
499
|
+
"defaultValue": "Podcasts",
|
|
500
|
+
"type": "string",
|
|
501
|
+
"flags": "PUBLIC"
|
|
502
|
+
},
|
|
503
|
+
"transistor_portal_description": {
|
|
504
|
+
"defaultValue": "Access your private podcast feed",
|
|
505
|
+
"type": "string",
|
|
506
|
+
"flags": "PUBLIC"
|
|
507
|
+
},
|
|
508
|
+
"transistor_portal_button_text": {
|
|
509
|
+
"defaultValue": "View",
|
|
510
|
+
"type": "string",
|
|
511
|
+
"flags": "PUBLIC"
|
|
512
|
+
},
|
|
513
|
+
"transistor_portal_url_template": {
|
|
514
|
+
"defaultValue": "https://partner.transistor.fm/ghost/{memberUuid}",
|
|
515
|
+
"type": "string",
|
|
516
|
+
"flags": "PUBLIC"
|
|
489
517
|
}
|
|
490
518
|
},
|
|
491
519
|
"views": {
|
|
@@ -487,9 +487,9 @@ module.exports = {
|
|
|
487
487
|
stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
|
|
488
488
|
interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
|
|
489
489
|
currency: {type: 'string', maxlength: 50, nullable: true},
|
|
490
|
-
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial']]}},
|
|
490
|
+
discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial', 'free_months']]}},
|
|
491
491
|
discount_amount: {type: 'integer', nullable: false},
|
|
492
|
-
duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'once', 'repeating', 'forever']]}},
|
|
492
|
+
duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'free_months', 'once', 'repeating', 'forever']]}},
|
|
493
493
|
duration_in_months: {type: 'integer', nullable: true},
|
|
494
494
|
portal_title: {type: 'string', maxlength: 191, nullable: true},
|
|
495
495
|
portal_description: {type: 'string', maxlength: 2000, nullable: true},
|
|
@@ -38,9 +38,9 @@ class OffersImporter extends TableImporter {
|
|
|
38
38
|
// stripe_coupon_id: {type: 'string', maxlength: 255, nullable: true, unique: true},
|
|
39
39
|
// interval: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['month', 'year']]}},
|
|
40
40
|
// currency: {type: 'string', maxlength: 50, nullable: true},
|
|
41
|
-
// discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial']]}},
|
|
41
|
+
// discount_type: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['percent', 'amount', 'trial', 'free_months']]}},
|
|
42
42
|
// discount_amount: {type: 'integer', nullable: false},
|
|
43
|
-
// duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'once', 'repeating', 'forever']]}},
|
|
43
|
+
// duration: {type: 'string', maxlength: 50, nullable: false, validations: {isIn: [['trial', 'free_months', 'once', 'repeating', 'forever']]}},
|
|
44
44
|
// duration_in_months: {type: 'integer', nullable: true},
|
|
45
45
|
// portal_title: {type: 'string', maxlength: 191, nullable: true},
|
|
46
46
|
// portal_description: {type: 'string', maxlength: 2000, nullable: true},
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# api_top_pages_router - Smart router that delegates to optimal top pages implementation
|
|
2
|
+
#
|
|
3
|
+
# ROUTING LOGIC:
|
|
4
|
+
# - If session-level or location filters are present (device, location, source, utm_*),
|
|
5
|
+
# use api_top_pages which joins with filtered_sessions to support these filters
|
|
6
|
+
# - Otherwise, use api_top_pages_v3 which uses the daily materialized view
|
|
7
|
+
# for 3-4x faster queries
|
|
8
|
+
#
|
|
9
|
+
# Query parameters cascade automatically to the underlying pipe.
|
|
10
|
+
|
|
11
|
+
TOKEN "stats_page" READ
|
|
12
|
+
TOKEN "axis" READ
|
|
13
|
+
|
|
14
|
+
NODE router
|
|
15
|
+
DESCRIPTION >
|
|
16
|
+
Routes to the appropriate implementation based on filter parameters.
|
|
17
|
+
Session-level filters (device, location, source, utm_*) require the original pipe.
|
|
18
|
+
Simple queries use the faster v3 materialized view.
|
|
19
|
+
|
|
20
|
+
SQL >
|
|
21
|
+
%
|
|
22
|
+
{% if defined(device) or defined(location) or defined(source) or defined(utm_source) or defined(utm_medium) or defined(utm_campaign) or defined(utm_term) or defined(utm_content) %}
|
|
23
|
+
SELECT * FROM api_top_pages
|
|
24
|
+
{% else %}
|
|
25
|
+
SELECT * FROM api_top_pages_v3
|
|
26
|
+
{% end %}
|
|
27
|
+
|
|
28
|
+
TYPE ENDPOINT
|
|
@@ -34,8 +34,10 @@ SQL >
|
|
|
34
34
|
FROM _mv_daily_pages
|
|
35
35
|
WHERE
|
|
36
36
|
site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
|
|
37
|
-
|
|
38
|
-
AND day
|
|
37
|
+
{% if defined(date_from) %}
|
|
38
|
+
AND day >= toDate({{ Date(date_from, description="Start date for filtering", required=False) }}, {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }})
|
|
39
|
+
{% end %}
|
|
40
|
+
AND day <= toDate({% if defined(date_to) %}{{ Date(date_to, description="End date for filtering", required=False) }}{% else %}now(){% end %}, {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }})
|
|
39
41
|
AND day != toDate(now(), {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }})
|
|
40
42
|
{% if defined(member_status) %}
|
|
41
43
|
AND member_status IN (
|
|
@@ -70,8 +72,10 @@ SQL >
|
|
|
70
72
|
WHERE
|
|
71
73
|
site_uuid = {{ String(site_uuid, 'mock_site_uuid', description="Tenant ID", required=True) }}
|
|
72
74
|
AND toDate(timestamp, {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }}) = toDate(now(), {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }})
|
|
73
|
-
|
|
74
|
-
AND timestamp
|
|
75
|
+
{% if defined(date_from) %}
|
|
76
|
+
AND timestamp >= toDateTime({{ Date(date_from, description="Start date for filtering", required=False) }}, {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }})
|
|
77
|
+
{% end %}
|
|
78
|
+
AND timestamp < toDateTime({% if defined(date_to) %}{{ Date(date_to, description="End date for filtering", required=False) }}{% else %}now(){% end %}, {{ String(timezone, 'Etc/UTC', description="Site timezone", required=True) }}) + interval 1 day
|
|
75
79
|
{% if defined(member_status) %}
|
|
76
80
|
AND member_status IN (
|
|
77
81
|
SELECT arrayJoin(
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
|
|
2
|
+
- name: no_session_level_filters
|
|
3
|
+
description: Query without session-level filters (should route to optimized pipe)
|
|
4
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC
|
|
5
|
+
expected_result: |
|
|
6
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
7
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
8
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
9
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
10
|
+
|
|
11
|
+
- name: pathname_filter
|
|
12
|
+
description: Query with pathname filter (should route to optimized pipe)
|
|
13
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&pathname=%2Fabout%2F
|
|
14
|
+
expected_result: |
|
|
15
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
16
|
+
|
|
17
|
+
- name: post_uuid_filter
|
|
18
|
+
description: Query with post_uuid filter (should route to optimized pipe)
|
|
19
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&post_uuid=6b8635fb-292f-4422-9fe4-d76cfab2ba31
|
|
20
|
+
expected_result: |
|
|
21
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
22
|
+
|
|
23
|
+
- name: device_filter
|
|
24
|
+
description: Query with device filter routes to api_top_pages
|
|
25
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&device=desktop
|
|
26
|
+
expected_result: |
|
|
27
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
28
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":8}
|
|
29
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
30
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
31
|
+
|
|
32
|
+
- name: location_filter
|
|
33
|
+
description: Query with location filter routes to api_top_pages
|
|
34
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&location=GB
|
|
35
|
+
expected_result: |
|
|
36
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":6}
|
|
37
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":4}
|
|
38
|
+
{"post_uuid":"","pathname":"\/","visits":4}
|
|
39
|
+
|
|
40
|
+
- name: source_filter
|
|
41
|
+
description: Query with source filter routes to api_top_pages
|
|
42
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&source=bing.com
|
|
43
|
+
expected_result: |
|
|
44
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":2}
|
|
45
|
+
{"post_uuid":"","pathname":"\/","visits":1}
|
|
46
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":1}
|
|
47
|
+
|
|
48
|
+
- name: utm_source_filter
|
|
49
|
+
description: Query with utm_source filter routes to api_top_pages
|
|
50
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&date_to=2100-01-07&timezone=Etc/UTC&utm_source=google
|
|
51
|
+
expected_result: |
|
|
52
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":2}
|
|
53
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
54
|
+
|
|
55
|
+
- name: date_from_only
|
|
56
|
+
description: Query with only date_from (no date_to) routes to v3 without error (fixture data is far into the future)
|
|
57
|
+
parameters: site_uuid=mock_site_uuid&date_from=2100-01-01&timezone=Etc/UTC
|
|
58
|
+
expected_result: ''
|
|
59
|
+
|
|
60
|
+
- name: date_to_only
|
|
61
|
+
description: Query with only date_to (no date_from) routes to v3 and returns all
|
|
62
|
+
data up to date_to
|
|
63
|
+
parameters: site_uuid=mock_site_uuid&date_to=2100-01-07&timezone=Etc/UTC
|
|
64
|
+
expected_result: |
|
|
65
|
+
{"post_uuid":"6b8635fb-292f-4422-9fe4-d76cfab2ba31","pathname":"\/blog\/hello-world\/","visits":9}
|
|
66
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1fc","pathname":"\/about\/","visits":8}
|
|
67
|
+
{"post_uuid":"","pathname":"\/","visits":7}
|
|
68
|
+
{"post_uuid":"06b1b0c9-fb53-4a15-a060-3db3fde7b1dd","pathname":"\/blog\/hello-world\/","visits":1}
|
|
69
|
+
|
|
70
|
+
- name: no_date_filters
|
|
71
|
+
description: Query with no date filters routes to v3 without error (date_to defaults
|
|
72
|
+
to now and fixture data is far into the future)
|
|
73
|
+
parameters: site_uuid=mock_site_uuid&timezone=Etc/UTC
|
|
74
|
+
expected_result: ''
|
|
@@ -26,10 +26,7 @@ class CommentsServiceEmails {
|
|
|
26
26
|
*/
|
|
27
27
|
getPostUrl(postId, commentId) {
|
|
28
28
|
const baseUrl = this.urlService.getUrlByResourceId(postId, {absolute: true});
|
|
29
|
-
|
|
30
|
-
return `${baseUrl}#ghost-comments-${commentId}`;
|
|
31
|
-
}
|
|
32
|
-
return `${baseUrl}#ghost-comments-root`;
|
|
29
|
+
return `${baseUrl}#ghost-comments-${commentId}`;
|
|
33
30
|
}
|
|
34
31
|
|
|
35
32
|
async notifyPostAuthors(comment) {
|
|
@@ -1788,6 +1788,49 @@ img.kg-cta-image {
|
|
|
1788
1788
|
color: rgba(0, 0, 0, 0.6) !important;
|
|
1789
1789
|
{{/if}}
|
|
1790
1790
|
}
|
|
1791
|
+
|
|
1792
|
+
.kg-transistor-card {
|
|
1793
|
+
width: auto;
|
|
1794
|
+
width: 100%;
|
|
1795
|
+
margin: 0 auto 1.5em;
|
|
1796
|
+
border-radius: 3px;
|
|
1797
|
+
{{#if backgroundIsDark}}
|
|
1798
|
+
background-color: #15212A;
|
|
1799
|
+
background-color: rgba(0, 0, 0, 0.15);
|
|
1800
|
+
border: 1px solid #343434;
|
|
1801
|
+
border: 1px solid rgba(255, 255, 255, 0.25);
|
|
1802
|
+
{{else}}
|
|
1803
|
+
background-color: #ffffff;
|
|
1804
|
+
background-color: rgba(255, 255, 255, 0.25);
|
|
1805
|
+
border: 1px solid #e0e7eb;
|
|
1806
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
1807
|
+
{{/if}}
|
|
1808
|
+
}
|
|
1809
|
+
.kg-transistor-title {
|
|
1810
|
+
display: block;
|
|
1811
|
+
padding-right: 20px;
|
|
1812
|
+
padding-bottom: 4px;
|
|
1813
|
+
padding-top: 4px;
|
|
1814
|
+
font-size: 16px;
|
|
1815
|
+
font-weight: 600;
|
|
1816
|
+
line-height: 18px;
|
|
1817
|
+
text-decoration: none !important;
|
|
1818
|
+
color: {{#if backgroundIsDark}}#ffffff{{else}}#15212A{{/if}} !important;
|
|
1819
|
+
}
|
|
1820
|
+
.kg-transistor-description {
|
|
1821
|
+
display: block;
|
|
1822
|
+
font-size: 13px;
|
|
1823
|
+
line-height: 1.35em;
|
|
1824
|
+
text-decoration: none !important;
|
|
1825
|
+
{{#if backgroundIsDark}}
|
|
1826
|
+
color: #ffffff !important;
|
|
1827
|
+
color: rgba(255, 255, 255, 0.6) !important;
|
|
1828
|
+
{{else}}
|
|
1829
|
+
color: #15212a !important;
|
|
1830
|
+
color: rgba(0, 0, 0, 0.6) !important;
|
|
1831
|
+
{{/if}}
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1791
1834
|
.kg-file-card {
|
|
1792
1835
|
width: auto;
|
|
1793
1836
|
width: 100%;
|
|
@@ -43,8 +43,8 @@ function frontendTemplate(node, document, options) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function emailTemplate(node, document, options) {
|
|
46
|
-
|
|
47
|
-
const
|
|
46
|
+
// Use the site accent color from the newsletter/email design settings
|
|
47
|
+
const accentColor = options.design?.accentColor || '#15171A';
|
|
48
48
|
|
|
49
49
|
// Use {uuid} replacement string - wrapReplacementStrings converts this to %%{uuid}%%
|
|
50
50
|
// which gets replaced with actual member UUID when email is sent to each recipient
|
|
@@ -53,21 +53,27 @@ function emailTemplate(node, document, options) {
|
|
|
53
53
|
const cardHtml = html`
|
|
54
54
|
<table class="kg-card kg-transistor-card" cellspacing="0" cellpadding="0" border="0" width="100%">
|
|
55
55
|
<tr>
|
|
56
|
-
<td
|
|
57
|
-
<
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
56
|
+
<td style="padding: 4px;">
|
|
57
|
+
<table cellspacing="0" cellpadding="0" border="0" width="100%">
|
|
58
|
+
<tr>
|
|
59
|
+
<td valign="middle" width="56" style="padding-right: 14px;">
|
|
60
|
+
<a href="${transistorUrl}" style="display: block; width: 52px; height: 52px; padding-top: 4px; padding-right: 4px; padding-bottom: 4px; padding-left: 4px; border-radius: 2px; background-color: ${accentColor}">
|
|
61
|
+
<img src="https://static.ghost.org/v6.0.0/images/transistor-logo-ondark.png"
|
|
62
|
+
width="36" height="36"
|
|
63
|
+
alt="Transistor"
|
|
64
|
+
style="width: 36px; height: 36px; padding: 8px;">
|
|
65
|
+
</a>
|
|
66
|
+
</td>
|
|
67
|
+
<td valign="middle" style="vertical-align: middle;">
|
|
68
|
+
<a href="${transistorUrl}" class="kg-transistor-title">
|
|
69
|
+
Listen to your podcasts
|
|
70
|
+
</a>
|
|
71
|
+
<a href="${transistorUrl}" class="kg-transistor-description">
|
|
72
|
+
Get your personal podcast feed to subscribe and listen in your favorite podcast app.
|
|
73
|
+
</a>
|
|
74
|
+
</td>
|
|
75
|
+
</tr>
|
|
76
|
+
</table>
|
|
71
77
|
</td>
|
|
72
78
|
</tr>
|
|
73
79
|
</table>
|
|
@@ -8,6 +8,7 @@ const ObjectId = require('bson-objectid').default;
|
|
|
8
8
|
const {NotFoundError} = require('@tryghost/errors');
|
|
9
9
|
const validator = require('@tryghost/validator');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
|
+
const addCalendarMonths = require('../utils/add-calendar-months');
|
|
11
12
|
const StartOutboxProcessingEvent = require('../../../outbox/events/start-outbox-processing-event');
|
|
12
13
|
const {MEMBER_WELCOME_EMAIL_SLUGS} = require('../../../member-welcome-emails/constants');
|
|
13
14
|
const messages = {
|
|
@@ -1800,23 +1801,46 @@ module.exports = class MemberRepository {
|
|
|
1800
1801
|
});
|
|
1801
1802
|
}
|
|
1802
1803
|
|
|
1803
|
-
|
|
1804
|
+
const stripeSubscriptionId = subscriptionModel.get('subscription_id');
|
|
1805
|
+
const isFreeMonthsOffer = offer.type === 'free_months';
|
|
1806
|
+
|
|
1807
|
+
if (isFreeMonthsOffer) {
|
|
1808
|
+
const currentPeriodEnd = subscriptionModel.get('current_period_end');
|
|
1809
|
+
if (!currentPeriodEnd) {
|
|
1810
|
+
throw new errors.BadRequestError({
|
|
1811
|
+
message: tpl(messages.subscriptionNotActive)
|
|
1812
|
+
});
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const trialEndDate = addCalendarMonths(currentPeriodEnd, offer.amount);
|
|
1816
|
+
const trialEnd = Math.floor(trialEndDate.getTime() / 1000);
|
|
1817
|
+
|
|
1818
|
+
const updatedSubscription = await this._stripeAPIService.updateSubscriptionTrialEnd(
|
|
1819
|
+
stripeSubscriptionId,
|
|
1820
|
+
trialEnd,
|
|
1821
|
+
{prorationBehavior: 'none'}
|
|
1822
|
+
);
|
|
1823
|
+
|
|
1824
|
+
return this.linkSubscription({
|
|
1825
|
+
id: member.id,
|
|
1826
|
+
subscription: updatedSubscription,
|
|
1827
|
+
offerId: data.offerId
|
|
1828
|
+
}, options);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
// Besides free_months and trial offers, all other offers rely on a Stripe Coupon
|
|
1804
1832
|
if (!data.couponId) {
|
|
1805
1833
|
throw new errors.BadRequestError({
|
|
1806
1834
|
message: tpl(messages.offerNoCoupon)
|
|
1807
1835
|
});
|
|
1808
1836
|
}
|
|
1809
1837
|
|
|
1810
|
-
const stripeSubscriptionId = subscriptionModel.get('subscription_id');
|
|
1811
|
-
|
|
1812
|
-
// Apply coupon to Stripe subscription
|
|
1813
1838
|
const updatedSubscription = await this._stripeAPIService.addCouponToSubscription(
|
|
1814
1839
|
stripeSubscriptionId,
|
|
1815
1840
|
data.couponId
|
|
1816
1841
|
);
|
|
1817
1842
|
|
|
1818
|
-
|
|
1819
|
-
await this.linkSubscription({
|
|
1843
|
+
return this.linkSubscription({
|
|
1820
1844
|
id: member.id,
|
|
1821
1845
|
subscription: updatedSubscription,
|
|
1822
1846
|
offerId: data.offerId
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* @prop {string} offer_id
|
|
8
8
|
* @prop {string} start
|
|
9
9
|
* @prop {string|null} end
|
|
10
|
-
* @prop {'once'|'repeating'|'forever'} duration
|
|
11
|
-
* @prop {'percent'|'fixed'} type
|
|
10
|
+
* @prop {'once'|'repeating'|'forever'|'free_months'} duration
|
|
11
|
+
* @prop {'percent'|'fixed'|'free_months'} type
|
|
12
12
|
* @prop {number} amount
|
|
13
13
|
*/
|
|
14
14
|
|
|
@@ -47,36 +47,29 @@ class NextPaymentCalculator {
|
|
|
47
47
|
const interval = subscription.plan.interval;
|
|
48
48
|
const currency = subscription.plan.currency;
|
|
49
49
|
const offer = subscription.offer || null;
|
|
50
|
+
const defaultNextPayment = {
|
|
51
|
+
original_amount: originalAmount,
|
|
52
|
+
amount: originalAmount,
|
|
53
|
+
interval,
|
|
54
|
+
currency,
|
|
55
|
+
discount: null
|
|
56
|
+
};
|
|
50
57
|
|
|
51
58
|
if (!offer || offer.type === 'trial') {
|
|
52
|
-
return
|
|
53
|
-
original_amount: originalAmount,
|
|
54
|
-
amount: originalAmount,
|
|
55
|
-
interval,
|
|
56
|
-
currency,
|
|
57
|
-
discount: null
|
|
58
|
-
};
|
|
59
|
+
return defaultNextPayment;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
const activeDiscount = this._getActiveDiscount(subscription, offer);
|
|
62
63
|
|
|
63
64
|
if (!activeDiscount) {
|
|
64
|
-
return
|
|
65
|
-
original_amount: originalAmount,
|
|
66
|
-
amount: originalAmount,
|
|
67
|
-
interval,
|
|
68
|
-
currency,
|
|
69
|
-
discount: null
|
|
70
|
-
};
|
|
65
|
+
return defaultNextPayment;
|
|
71
66
|
}
|
|
72
67
|
|
|
73
68
|
const discountedAmount = this._calculateDiscountedAmount(originalAmount, offer);
|
|
74
69
|
|
|
75
70
|
return {
|
|
76
|
-
|
|
71
|
+
...defaultNextPayment,
|
|
77
72
|
amount: discountedAmount,
|
|
78
|
-
interval,
|
|
79
|
-
currency,
|
|
80
73
|
discount: {
|
|
81
74
|
offer_id: offer.id,
|
|
82
75
|
start: activeDiscount.start ? new Date(activeDiscount.start).toISOString() : null,
|
|
@@ -106,6 +99,21 @@ class NextPaymentCalculator {
|
|
|
106
99
|
* @returns {ActiveDiscount|null}
|
|
107
100
|
*/
|
|
108
101
|
_getActiveDiscount(subscription, offer) {
|
|
102
|
+
// Free months are based on trial periods in Stripe: they're active if the trial period is still ongoing
|
|
103
|
+
if (offer.type === 'free_months') {
|
|
104
|
+
const end = new Date(subscription.trial_end_at);
|
|
105
|
+
|
|
106
|
+
if (new Date() >= end) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
start: subscription.trial_start_at,
|
|
112
|
+
end
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Other offers are based on a Stripe coupon, with a discount_start / discount_end
|
|
109
117
|
if (subscription.discount_start) {
|
|
110
118
|
return {
|
|
111
119
|
start: subscription.discount_start,
|
|
@@ -113,7 +121,7 @@ class NextPaymentCalculator {
|
|
|
113
121
|
};
|
|
114
122
|
}
|
|
115
123
|
|
|
116
|
-
// Backportability for signup offers without discount_start / discount_end
|
|
124
|
+
// Backportability for old signup offers without discount_start / discount_end
|
|
117
125
|
if (offer.redemption_type !== 'signup') {
|
|
118
126
|
return null;
|
|
119
127
|
}
|
|
@@ -482,7 +482,7 @@ class PaymentsService {
|
|
|
482
482
|
*/
|
|
483
483
|
async getCouponForOffer(offerId) {
|
|
484
484
|
const row = await this.OfferModel.where({id: offerId}).query().select('stripe_coupon_id', 'discount_type').first();
|
|
485
|
-
if (!row || row.discount_type === 'trial') {
|
|
485
|
+
if (!row || row.discount_type === 'trial' || row.discount_type === 'free_months') {
|
|
486
486
|
return null;
|
|
487
487
|
}
|
|
488
488
|
if (!row.stripe_coupon_id) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const errors = require('@tryghost/errors');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Add calendar months in UTC while preserving month-end behavior.
|
|
5
|
+
* Example: Jan 31 + 1 month => Feb 28/29 (not Mar 2/3).
|
|
6
|
+
*
|
|
7
|
+
* @param {Date|string|number} inputDate
|
|
8
|
+
* @param {number} months
|
|
9
|
+
* @returns {Date}
|
|
10
|
+
*/
|
|
11
|
+
module.exports = function addCalendarMonths(inputDate, months) {
|
|
12
|
+
const sourceDate = new Date(inputDate);
|
|
13
|
+
const normalizedMonths = Number(months);
|
|
14
|
+
|
|
15
|
+
if (Number.isNaN(sourceDate.getTime())) {
|
|
16
|
+
throw new errors.BadRequestError({
|
|
17
|
+
message: 'inputDate must be a valid date'
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!Number.isInteger(normalizedMonths) || normalizedMonths < 1) {
|
|
22
|
+
throw new errors.BadRequestError({
|
|
23
|
+
message: 'months must be a positive integer'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const target = new Date(Date.UTC(
|
|
28
|
+
sourceDate.getUTCFullYear(),
|
|
29
|
+
sourceDate.getUTCMonth(),
|
|
30
|
+
1,
|
|
31
|
+
sourceDate.getUTCHours(),
|
|
32
|
+
sourceDate.getUTCMinutes(),
|
|
33
|
+
sourceDate.getUTCSeconds(),
|
|
34
|
+
sourceDate.getUTCMilliseconds()
|
|
35
|
+
));
|
|
36
|
+
|
|
37
|
+
target.setUTCMonth(target.getUTCMonth() + normalizedMonths);
|
|
38
|
+
|
|
39
|
+
const originalDay = sourceDate.getUTCDate();
|
|
40
|
+
const daysInTargetMonth = new Date(Date.UTC(
|
|
41
|
+
target.getUTCFullYear(),
|
|
42
|
+
target.getUTCMonth() + 1,
|
|
43
|
+
0
|
|
44
|
+
)).getUTCDate();
|
|
45
|
+
|
|
46
|
+
target.setUTCDate(Math.min(originalDay, daysInTargetMonth));
|
|
47
|
+
|
|
48
|
+
return target;
|
|
49
|
+
};
|