ghost 6.17.1 → 6.18.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.18.0.tgz +0 -0
- package/components/{tryghost-parse-email-address-6.17.1.tgz → tryghost-parse-email-address-6.18.0.tgz} +0 -0
- package/core/built/admin/assets/{PolarAngleAxis-DVj7Sw2n.js → PolarAngleAxis-Dod0DwfL.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-CrCkbXur.js → _baseAssignValue-DnkbkowM.js} +1 -1
- package/core/built/admin/assets/{a-large-small-BLquRSNr.js → a-large-small-C5mgFBRg.js} +1 -1
- package/core/built/admin/assets/activitypub/activitypub.js +1 -1
- package/core/built/admin/assets/activitypub/{at-sign-Fsk3x72r.mjs → at-sign-BR2C2gdz.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{avatar-flipboard-C10JfFS_.mjs → avatar-flipboard-CJV3KhLU.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{bluesky-sharing-C1xeGSL6.mjs → bluesky-sharing-DY4KCGPZ.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{copy-C1fElSkQ.mjs → copy-CBFwbAdE.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{deleted-feed-item-Bun4tY2_.mjs → deleted-feed-item-DOm9GxTQ.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{edit-profile-CYh00FZ7.mjs → edit-profile-CcbnZWkM.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{feed-BxUqmcN9.mjs → feed-BHiZU0Q_.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{hash-CNgwAx-U.mjs → hash-Csjloy5t.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{inbox-DqNqII4a.mjs → inbox-DGKbBux9.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index-CONoLlDU.mjs → index-Bt_qFJNY.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index--Q6orQkb.mjs → index-CCYuVHjm.mjs} +21 -21
- package/core/built/admin/assets/activitypub/{index-jhjmoHwu.mjs → index-CTV39jCH.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{index-Dvh9q3jy.mjs → index-ClNx7qzi.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{index-C9pnotJK.mjs → index-D6Y6ywyl.mjs} +6 -6
- package/core/built/admin/assets/activitypub/{index-BueIufRq.mjs → index-YLvIz6pJ.mjs} +5 -5
- package/core/built/admin/assets/activitypub/{index-C3KJXzZE.mjs → index-tWANcGjH.mjs} +7 -7
- package/core/built/admin/assets/activitypub/{index-CJJXnqq1.mjs → index-vVqFEPIX.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{moderation-CYhwUFi2.mjs → moderation-CUGqRfT1.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{note-COVa8CMw.mjs → note-DMy39JHG.mjs} +4 -4
- package/core/built/admin/assets/activitypub/{reply-BHpKVBxx.mjs → reply-BboFcDsv.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{separator-DP7q5sFH.mjs → separator-DBvIXQnV.mjs} +2 -2
- package/core/built/admin/assets/activitypub/{settings-3n7zo_3K.mjs → settings-D4qRhs9E.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{step-1-BmUukywZ.mjs → step-1-C-ubwwIT.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{step-2-C--I3xxp.mjs → step-2-Dsd2XY1_.mjs} +5 -5
- package/core/built/admin/assets/activitypub/{step-3-0Deh5N9c.mjs → step-3-BHz3ow0t.mjs} +7 -7
- package/core/built/admin/assets/activitypub/{tabs-D_vmoLBo.mjs → tabs-s_P4XeyF.mjs} +3 -3
- package/core/built/admin/assets/activitypub/{topic-filter-DJMrhH-c.mjs → topic-filter-CCf15CR-.mjs} +2 -2
- 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-BzvYs1fs.mjs → code-editor-view-C9yQgDwn.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BXChUyr6.mjs → index-Cb0MgPGv.mjs} +30 -19
- package/core/built/admin/assets/admin-x-settings/{index-DYEF3NiM.mjs → index-DGoxlHIt.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DdMo_pvD.mjs → index-jzFpi950.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{modals-Ck3AHDx3.mjs → modals-CCI0FGRh.mjs} +13 -12
- package/core/built/admin/assets/{at-sign-Cj4_ORaa.js → at-sign-Bz-SU-S_.js} +1 -1
- package/core/built/admin/assets/{audience-ACTBvUSt.js → audience-DmVdEmIe.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-BeKL4BEF.js → avatar-flipboard-C7sxDVEM.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-CP7ej5rz.js → bluesky-sharing-DJniSF6N.js} +1 -1
- package/core/built/admin/assets/{chart-B6jk72FT.js → chart-mYz3IJwm.js} +1 -1
- package/core/built/admin/assets/{chunk.524.6c12997c856012ea06f5.js → chunk.524.d2c4d1c78e72685d04c9.js} +6 -6
- package/core/built/admin/assets/{chunk.582.a0abd5b93eb1bc50335a.js → chunk.582.a3333324fa5a373a6148.js} +8 -8
- package/core/built/admin/assets/{code-editor-view-CVRtsOBM.js → code-editor-view-BIQWFJ01.js} +1 -1
- package/core/built/admin/assets/comments-B_-hdjc6.js +1 -0
- package/core/built/admin/assets/{copy-YhtJexiS.js → copy-Bpq3uhnh.js} +1 -1
- package/core/built/admin/assets/{data-list-BPSXm3ej.js → data-list-BYNMbRIq.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-D8BHDEHh.js → deleted-feed-item-B_AwUKZy.js} +1 -1
- package/core/built/admin/assets/{edit-profile-FrkkXMG7.js → edit-profile-DZo2ZcOu.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-SsCyCd50.js → empty-indicator-Bn5wG9-T.js} +1 -1
- package/core/built/admin/assets/{en-ZWZBSdFY.js → en-D4zIrMLN.js} +1 -1
- package/core/built/admin/assets/{feed-BvVJCTy8.js → feed-Bgq4RarU.js} +1 -1
- package/core/built/admin/assets/{filters-BupJ4mUj.js → filters-CpVwu1Gk.js} +1 -1
- package/core/built/admin/assets/{gh-chart-Dp9VXeON.js → gh-chart-Br7NXn_b.js} +1 -1
- package/core/built/admin/assets/{ghost-78b420974adc056bf1c9570275a46535.css → ghost-3c48d37b32f4fcd7eb15cfd739905a03.css} +1 -1
- package/core/built/admin/assets/{ghost-a9268074d0a9b02198594a05ecd7a626.js → ghost-98a3c5fd6235ebd344594f491cb6d17b.js} +241 -246
- package/core/built/admin/assets/{ghost-dark-12abca67e79c76fe6bfed6cb81ba2a53.css → ghost-dark-0100fe3ec7b4c3f6d8d043c904254a63.css} +1 -1
- package/core/built/admin/assets/{growth-CRDHOGrZ.js → growth-CiPDnWxJ.js} +1 -1
- package/core/built/admin/assets/{hash-CPAX2PwN.js → hash-Bc1e38oH.js} +1 -1
- package/core/built/admin/assets/{inbox-DuT2edmx.js → inbox-BcZNBNhk.js} +1 -1
- package/core/built/admin/assets/{index-CQP2hNBo.js → index-BFEx_ouC.js} +1 -1
- package/core/built/admin/assets/{index-ByYZ5TiM.js → index-B_7QsC5T.js} +1 -1
- package/core/built/admin/assets/{index-BX3kQzlo.js → index-Bm5ZeLtt.js} +1 -1
- package/core/built/admin/assets/{index-Dk-Wh_Fw.js → index-BpcL7RmI.js} +2 -2
- package/core/built/admin/assets/{index--Nwo3958.js → index-Brg4tecQ.js} +1 -1
- package/core/built/admin/assets/{index-exFCQBCk.js → index-CMTzNTew.js} +29 -29
- package/core/built/admin/assets/{index-8ECl7z0l.js → index-CTfJflJ2.js} +1 -1
- package/core/built/admin/assets/{index-B2oDZJ9B.js → index-Cn32t_gv.js} +1 -1
- package/core/built/admin/assets/{index-BwUQiVrX.js → index-CsidX7Si.js} +1 -1
- package/core/built/admin/assets/{index-DnY4rls4.js → index-D212VvOz.js} +1 -1
- package/core/built/admin/assets/{index-bl5YmBtG.js → index-D67LJ_H4.js} +1 -1
- package/core/built/admin/assets/{index-BPYePamp.js → index-DU3bv9jz.js} +1 -1
- package/core/built/admin/assets/{index-CjzFwwKZ.js → index-Dv13wKfX.js} +1 -1
- package/core/built/admin/assets/{index-B1rQYn0h.js → index-ETsoQLbU.js} +1 -1
- package/core/built/admin/assets/{index-CeFAaI2v.js → index-ZYDRMgcT.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-eRS1y7PS.js → koenig-lexical-DZUWzN0P.js} +1 -1
- package/core/built/admin/assets/{kpi-card-DShFKtte.js → kpi-card-DENsK2xK.js} +1 -1
- package/core/built/admin/assets/{kpis-Bxyr_zYY.js → kpis-CDrs2iS1.js} +1 -1
- package/core/built/admin/assets/{label-hgdYQhmm.js → label-BznQtEEo.js} +1 -1
- package/core/built/admin/assets/{links-5NNDFN65.js → links-lpAC3T1p.js} +1 -1
- package/core/built/admin/assets/{lucide-react-f58FO8MB.js → lucide-react-CBigk-fq.js} +1 -1
- package/core/built/admin/assets/{main-layout-BDz_2UZs.js → main-layout-_SYQRjIl.js} +1 -1
- package/core/built/admin/assets/{message-square-text-6dTKbQKA.js → message-square-text-cV8O_qKq.js} +1 -1
- package/core/built/admin/assets/{minus-D2C19Fiu.js → minus-NvnQTlW7.js} +1 -1
- package/core/built/admin/assets/{modals-DzetQgXf.js → modals-XRSkribf.js} +3 -3
- package/core/built/admin/assets/{moderation-87QsKbyf.js → moderation-BQp1GEWG.js} +1 -1
- package/core/built/admin/assets/{newsletter-BhI9Lgkn.js → newsletter-foM6KNNV.js} +1 -1
- package/core/built/admin/assets/{newsletters-adLgF_Re.js → newsletters-BexdXUhn.js} +1 -1
- package/core/built/admin/assets/{note-BbPR57Yw.js → note-DuaUGOeZ.js} +1 -1
- package/core/built/admin/assets/{overview-CqyRlIVM.js → overview-DgKBNqyc.js} +1 -1
- package/core/built/admin/assets/{pagemenu-DlQF5jN4.js → pagemenu-CZyroidv.js} +1 -1
- package/core/built/admin/assets/{post-analytics-BudH7GCA.js → post-analytics-DLK2SOSQ.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-CxuzZluw.js → post-analytics-context-CF7C67-0.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-BV_8onIF.js → post-analytics-header-7GtJCx0W.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-CnM21JFK.js → post-share-modal-D8R7DUZP.js} +1 -1
- package/core/built/admin/assets/posts/{comments-CHEqbZPq.mjs → comments-Bn_vn_sb.mjs} +434 -407
- package/core/built/admin/assets/posts/{dialog-VFomJYla.mjs → dialog-CKPcMrj7.mjs} +3 -3
- package/core/built/admin/assets/posts/{empty-indicator-CT9qQsMO.mjs → empty-indicator-kkrhP6K_.mjs} +2 -2
- package/core/built/admin/assets/posts/{filters-BXCfSKKS.mjs → filters-DHsxxy0F.mjs} +6 -6
- package/core/built/admin/assets/posts/{growth-xMitvyvt.mjs → growth-D7ACh-oR.mjs} +12 -12
- package/core/built/admin/assets/posts/{heading-DIulQwgD.mjs → heading-BrKVbOxA.mjs} +2 -2
- package/core/built/admin/assets/posts/{hooks-BiBMDYOA.mjs → hooks-BBEMuLiW.mjs} +3 -3
- package/core/built/admin/assets/posts/{index-uYyMBANY.mjs → index-C0rRguZx.mjs} +9 -9
- package/core/built/admin/assets/posts/{kpis-DK6j0CJA.mjs → kpis-1o524OIK.mjs} +10 -10
- package/core/built/admin/assets/posts/{links-CBiYZsyk.mjs → links-JpeATx1f.mjs} +4 -4
- package/core/built/admin/assets/posts/{loading-indicator-BPPeKpVb.mjs → loading-indicator-BHAmSf8j.mjs} +3 -3
- package/core/built/admin/assets/posts/{main-layout-f-PcNn-6.mjs → main-layout-BTItAOQE.mjs} +2 -2
- package/core/built/admin/assets/posts/{newsletter-CSOyAfvO.mjs → newsletter-CQdsCEHv.mjs} +13 -13
- package/core/built/admin/assets/posts/{overview-Dve3Uxt4.mjs → overview-sYh4JN1D.mjs} +15 -15
- package/core/built/admin/assets/posts/{post-analytics-CPKxH7yg.mjs → post-analytics-DtLt2SWx.mjs} +6 -6
- package/core/built/admin/assets/posts/{post-analytics-context-B5zQoflZ.mjs → post-analytics-context-CB1yM9fk.mjs} +4 -4
- package/core/built/admin/assets/posts/{post-analytics-header-BBxuaptK.mjs → post-analytics-header-BYaWuL9W.mjs} +9 -9
- package/core/built/admin/assets/posts/{post-share-modal-D_iMXvnI.mjs → post-share-modal-V0HTOBaf.mjs} +4 -4
- package/core/built/admin/assets/posts/{posts-DW6ne0od.mjs → posts-DeQT3knv.mjs} +2 -2
- package/core/built/admin/assets/posts/posts.js +1 -1
- package/core/built/admin/assets/posts/{search-CVlPqGMA.mjs → search-CLjC37AT.mjs} +2 -2
- package/core/built/admin/assets/posts/{separator-YrJkDGVP.mjs → separator-KNoTIaJx.mjs} +5 -5
- package/core/built/admin/assets/posts/{sheet-T5GWxxrn.mjs → sheet-DBg_SCDt.mjs} +3 -3
- package/core/built/admin/assets/posts/{skeleton-qnvlkDy-.mjs → skeleton-DvsoolWu.mjs} +3 -3
- package/core/built/admin/assets/posts/{source-icon-DBU0eZwS.mjs → source-icon-OmRaTU2G.mjs} +3 -3
- package/core/built/admin/assets/posts/{stats-udwJVZP8.mjs → stats-BC2DzntY.mjs} +4 -4
- package/core/built/admin/assets/posts/{table-Cu0BZfBU.mjs → table-CxX9OKAj.mjs} +2 -2
- package/core/built/admin/assets/posts/{tabs-B-RaoD3I.mjs → tabs-B1jw7cBi.mjs} +10 -10
- package/core/built/admin/assets/posts/{tags-DSDGzArh.mjs → tags-BBilTo1a.mjs} +11 -11
- package/core/built/admin/assets/posts/{tags-DByfrDKO.mjs → tags-BitrLT_j.mjs} +2 -2
- package/core/built/admin/assets/posts/{use-infinite-virtual-scroll-B7dD2nWW.mjs → use-infinite-virtual-scroll-DaijA1ao.mjs} +3 -3
- package/core/built/admin/assets/posts/{web-B9wvEtJ_.mjs → web-CKmyC4Xj.mjs} +15 -15
- package/core/built/admin/assets/{posts-C5zAbpdG.js → posts-CL9UDYoW.js} +1 -1
- package/core/built/admin/assets/{repeat-BbSnXois.js → repeat-DgH39UKE.js} +1 -1
- package/core/built/admin/assets/{reply-CuCJJfZj.js → reply-DAaNxiy8.js} +1 -1
- package/core/built/admin/assets/{select-Bv-dkHeU.js → select-Cor2wFXT.js} +1 -1
- package/core/built/admin/assets/{settings-Cit2-dRQ.js → settings-BeumESEN.js} +5 -5
- package/core/built/admin/assets/{settings-wlK_oNsg.js → settings-xRx917Gj.js} +1 -1
- package/core/built/admin/assets/{sort-button-Dmyldyzm.js → sort-button-BNW3i4Lb.js} +1 -1
- package/core/built/admin/assets/{source-icon-DjyV6RS6.js → source-icon-DvDuzw73.js} +1 -1
- package/core/built/admin/assets/{sprout-jOQR37se.js → sprout-C3cc0c-K.js} +1 -1
- package/core/built/admin/assets/{square-IrYxuAJL.js → square-tZp0_n7e.js} +1 -1
- package/core/built/admin/assets/stats/{audience-Dfo7BmNu.mjs → audience-BWqU7WWT.mjs} +3 -3
- package/core/built/admin/assets/stats/{index-DUWoiDE_.mjs → index-CUuQaROI.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-Yo58temM.mjs → index-CcCyLMxL.mjs} +7 -7
- package/core/built/admin/assets/stats/{index-h0rsyxDz.mjs → index-D5mlMG4l.mjs} +8 -8
- package/core/built/admin/assets/stats/{index-_vL3Zubi.mjs → index-DXU2rE9t.mjs} +5 -5
- package/core/built/admin/assets/stats/{index-Banm1wtA.mjs → index-K7ASx7EG.mjs} +6 -6
- package/core/built/admin/assets/stats/{sort-button-i6PS-TUn.mjs → sort-button-CELUx6Zp.mjs} +3 -3
- package/core/built/admin/assets/stats/{stats-CjepXEWS.mjs → stats-d_u_in4l.mjs} +2 -2
- package/core/built/admin/assets/stats/stats.js +2 -2
- package/core/built/admin/assets/stats/{tabs-Q20S1oup.mjs → tabs-3wLZsy0v.mjs} +3 -3
- package/core/built/admin/assets/stats/{url-helpers-DrGoeEH1.mjs → url-helpers-Drq3xg0l.mjs} +4 -4
- package/core/built/admin/assets/stats/{use-growth-stats-BnffY2W3.mjs → use-growth-stats-28Sr42va.mjs} +3 -3
- package/core/built/admin/assets/{stats-C4LbW8IV.js → stats-2Jelnn-Q.js} +1 -1
- package/core/built/admin/assets/{stats-view-BwaLDryw.js → stats-view-CESy8ELH.js} +1 -1
- package/core/built/admin/assets/{step-1-C0QApeYD.js → step-1-DrqdolAh.js} +1 -1
- package/core/built/admin/assets/{step-2-h0b6Njyb.js → step-2-DmEpKck5.js} +1 -1
- package/core/built/admin/assets/{step-3-CaM-Hz3F.js → step-3-Bus-0o0n.js} +1 -1
- package/core/built/admin/assets/{table-P4FGxGxN.js → table-BQUcKHfm.js} +1 -1
- package/core/built/admin/assets/{tabs-Cj65mCsJ.js → tabs-BmdL0X4U.js} +1 -1
- package/core/built/admin/assets/{tags-Dnv6zTag.js → tags-CLxXZlOO.js} +1 -1
- package/core/built/admin/assets/{tags-CDvEkvxr.js → tags-EchqlZUJ.js} +1 -1
- package/core/built/admin/assets/{tiers-Y3OCOT03.js → tiers-nCGyTly9.js} +1 -1
- package/core/built/admin/assets/{toggle-group-kZZo_vdJ.js → toggle-group-CM5uf7J1.js} +1 -1
- package/core/built/admin/assets/{topic-filter-Cn1_Z9I7.js → topic-filter-LTRvZ8aU.js} +1 -1
- package/core/built/admin/assets/{trash-ln90Rlgi.js → trash-u5BxolyH.js} +1 -1
- package/core/built/admin/assets/{url-helpers-B5_oMSPb.js → url-helpers-D41fEt51.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-DQsJauHH.js → use-growth-stats-BJ0O9ewi.js} +1 -1
- package/core/built/admin/assets/{use-infinite-virtual-scroll-4OIpSNMH.js → use-infinite-virtual-scroll-APZWciOk.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-BedA-MMT.js → use-simple-pagination-DVRHeaAR.js} +1 -1
- package/core/built/admin/assets/{user-round-check-CYI147XP.js → user-round-check-B6j98D6d.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-xpilOd6G.js → wallet-cards-KmOh29LP.js} +1 -1
- package/core/built/admin/assets/{web-VPMnLvnh.js → web-Cclotbnz.js} +1 -1
- package/core/built/admin/index.html +5 -5
- package/core/frontend/meta/asset-url.js +126 -15
- package/core/frontend/services/asset-hash/index.js +73 -0
- package/core/frontend/services/theme-engine/active.js +4 -2
- package/core/server/api/endpoints/utils/serializers/output/utils/post-gating.js +6 -0
- package/core/server/data/migrations/versions/6.17/2026-02-04-10-42-20-offers-nullable-product-id.js +11 -0
- package/core/server/data/migrations/versions/6.18/2026-02-04-10-42-20-offers-nullable-product-id.js +4 -0
- package/core/server/data/schema/schema.js +1 -1
- package/core/server/data/tinybird/datasources/_mv_daily_pages.datasource +13 -0
- package/core/server/data/tinybird/endpoints/api_top_pages_v3.pipe +113 -0
- package/core/server/data/tinybird/pipes/mv_daily_pages.pipe +23 -0
- package/core/server/data/tinybird/scripts/benchmark-top-pages.sh +186 -0
- package/core/server/data/tinybird/scripts/compare-top-pages.sh +262 -0
- package/core/server/data/tinybird/tests/api_top_pages_v3.yaml +112 -0
- package/core/server/models/automated-email.js +34 -0
- package/core/server/services/email-address/email-address-service-wrapper.js +3 -0
- package/core/server/services/email-address/email-address-service.js +11 -0
- package/core/server/services/email-address/email-address-service.ts +13 -0
- package/core/server/services/koenig/node-renderers/transistor-renderer.js +3 -8
- package/core/server/services/lib/member-signup-contexts.js +23 -0
- package/core/server/services/lib/member-signup-contexts.ts +22 -0
- package/core/server/services/member-welcome-emails/member-welcome-email-renderer.js +13 -7
- package/core/server/services/member-welcome-emails/service.js +8 -12
- package/core/server/services/members/api.js +3 -1
- package/core/server/services/members/members-api/controllers/router-controller.js +19 -2
- package/core/server/services/members/members-api/members-api.js +4 -2
- package/core/server/services/members/members-api/repositories/member-repository.js +1 -1
- package/core/server/services/members/members-api/services/payments-service.js +5 -0
- package/core/server/services/oembed/oembed-service.js +1 -1
- package/core/server/services/offers/application/offer-mapper.js +6 -7
- package/core/server/services/offers/application/offers-api.js +3 -3
- package/core/server/services/offers/domain/errors/index.js +2 -0
- package/core/server/services/offers/domain/models/offer.js +15 -3
- package/core/server/services/offers/offer-bookshelf-repository.js +4 -5
- package/core/server/services/stats/utils/tinybird.js +5 -3
- package/core/server/services/stripe/services/webhook/checkout-session-event-service.js +17 -1
- package/core/server/services/stripe/stripe-service.js +8 -0
- package/core/shared/config/defaults.json +6 -0
- package/core/shared/labs.js +3 -3
- package/core/shared/url-utils.js +2 -1
- package/package.json +6 -6
- package/tsconfig.tsbuildinfo +1 -1
- package/yarn.lock +23 -2
- package/components/tryghost-i18n-6.17.1.tgz +0 -0
- package/core/built/admin/assets/comments-CxjhZH_T.js +0 -1
|
@@ -24,6 +24,7 @@ export class EmailAddressService {
|
|
|
24
24
|
#getDefaultEmail: () => EmailAddress;
|
|
25
25
|
#getFallbackDomain: () => string | null;
|
|
26
26
|
#getFallbackEmail: () => EmailAddress | null;
|
|
27
|
+
#getMembersSupportAddress: () => string;
|
|
27
28
|
#isValidEmailAddress: (email: string) => boolean;
|
|
28
29
|
|
|
29
30
|
constructor(dependencies: {
|
|
@@ -32,12 +33,14 @@ export class EmailAddressService {
|
|
|
32
33
|
getFallbackDomain: () => string | null,
|
|
33
34
|
getDefaultEmail: () => EmailAddress,
|
|
34
35
|
getFallbackEmail: () => string | null,
|
|
36
|
+
getMembersSupportAddress: () => string,
|
|
35
37
|
isValidEmailAddress: (email: string) => boolean
|
|
36
38
|
}) {
|
|
37
39
|
this.#getManagedEmailEnabled = dependencies.getManagedEmailEnabled;
|
|
38
40
|
this.#getSendingDomain = dependencies.getSendingDomain;
|
|
39
41
|
this.#getFallbackDomain = dependencies.getFallbackDomain;
|
|
40
42
|
this.#getDefaultEmail = dependencies.getDefaultEmail;
|
|
43
|
+
this.#getMembersSupportAddress = dependencies.getMembersSupportAddress;
|
|
41
44
|
this.#getFallbackEmail = () => {
|
|
42
45
|
const fallbackAddress = dependencies.getFallbackEmail();
|
|
43
46
|
if (!fallbackAddress) {
|
|
@@ -68,6 +71,16 @@ export class EmailAddressService {
|
|
|
68
71
|
return this.#getFallbackEmail();
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Get the actual sender address for member emails after DMARC transformation.
|
|
76
|
+
* On managed platforms, this may differ from the configured support address to ensure DMARC compliance.
|
|
77
|
+
*/
|
|
78
|
+
getMembersSupportAddress(): string {
|
|
79
|
+
const configuredAddress = this.#getMembersSupportAddress();
|
|
80
|
+
const transformed = this.getAddressFromString(configuredAddress);
|
|
81
|
+
return transformed.from.address;
|
|
82
|
+
}
|
|
83
|
+
|
|
71
84
|
getAddressFromString(from: string, replyTo?: string): EmailAddresses {
|
|
72
85
|
const parsedFrom = EmailAddressParser.parse(from);
|
|
73
86
|
const parsedReplyTo = replyTo ? EmailAddressParser.parse(replyTo) : undefined;
|
|
@@ -16,14 +16,9 @@ function renderTransistorNode(node, options = {}) {
|
|
|
16
16
|
function frontendTemplate(node, document, options) {
|
|
17
17
|
const figure = document.createElement('figure');
|
|
18
18
|
figure.setAttribute('class', 'kg-card kg-transistor-card');
|
|
19
|
-
const memberUuid = options.memberUuid;
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const embedUrl = new URL(`https://partner.transistor.fm/ghost/embed/${memberUuid}`);
|
|
20
|
+
// Use {uuid} placeholder - content.js will substitute with member UUID at request time
|
|
21
|
+
const embedUrl = new URL(`https://partner.transistor.fm/ghost/embed/{uuid}`);
|
|
27
22
|
|
|
28
23
|
if (node.accentColor) {
|
|
29
24
|
embedUrl.searchParams.set('color', node.accentColor.replace(/^#/, ''));
|
|
@@ -34,7 +29,7 @@ function frontendTemplate(node, document, options) {
|
|
|
34
29
|
|
|
35
30
|
const iframe = document.createElement('iframe');
|
|
36
31
|
iframe.setAttribute('width', '100%');
|
|
37
|
-
iframe.setAttribute('height', '
|
|
32
|
+
iframe.setAttribute('height', '325');
|
|
38
33
|
iframe.setAttribute('title', 'Transistor podcasts');
|
|
39
34
|
iframe.setAttribute('frameborder', 'no');
|
|
40
35
|
iframe.setAttribute('scrolling', 'no');
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SIGNUP_CONTEXTS = void 0;
|
|
4
|
+
exports.canWelcomeEmailReplaceSignupPaidEmail = canWelcomeEmailReplaceSignupPaidEmail;
|
|
5
|
+
// Signup context describes the sign-in state when the Stripe checkout session is created.
|
|
6
|
+
// NEEDS_MAGIC_LINK_EMAIL: No guaranteed sign-in path exists yet (custom/direct checkout paths).
|
|
7
|
+
// HAS_PRECHECKOUT_MAGIC_LINK: Ghost generated a signup magic-link before Stripe (standard Portal flow).
|
|
8
|
+
// ALREADY_AUTHENTICATED: Request came from a signed-in member identity (for example, opening a paid signup link directly).
|
|
9
|
+
exports.SIGNUP_CONTEXTS = {
|
|
10
|
+
NEEDS_MAGIC_LINK_EMAIL: 'needs_magic_link_email',
|
|
11
|
+
HAS_PRECHECKOUT_MAGIC_LINK: 'has_precheckout_magic_link',
|
|
12
|
+
ALREADY_AUTHENTICATED: 'already_authenticated'
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Signup-paid email can be skipped when welcome email is active only if
|
|
16
|
+
* checkout already has a reliable sign-in path.
|
|
17
|
+
*
|
|
18
|
+
* - HAS_PRECHECKOUT_MAGIC_LINK: standard Portal flow generated signup link before Stripe
|
|
19
|
+
* - ALREADY_AUTHENTICATED: checkout request came from an already signed-in member
|
|
20
|
+
*/
|
|
21
|
+
function canWelcomeEmailReplaceSignupPaidEmail(signupContext) {
|
|
22
|
+
return signupContext === exports.SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK || signupContext === exports.SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED;
|
|
23
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Signup context describes the sign-in state when the Stripe checkout session is created.
|
|
2
|
+
// NEEDS_MAGIC_LINK_EMAIL: No guaranteed sign-in path exists yet (custom/direct checkout paths).
|
|
3
|
+
// HAS_PRECHECKOUT_MAGIC_LINK: Ghost generated a signup magic-link before Stripe (standard Portal flow).
|
|
4
|
+
// ALREADY_AUTHENTICATED: Request came from a signed-in member identity (for example, opening a paid signup link directly).
|
|
5
|
+
export const SIGNUP_CONTEXTS = {
|
|
6
|
+
NEEDS_MAGIC_LINK_EMAIL: 'needs_magic_link_email',
|
|
7
|
+
HAS_PRECHECKOUT_MAGIC_LINK: 'has_precheckout_magic_link',
|
|
8
|
+
ALREADY_AUTHENTICATED: 'already_authenticated'
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export type SignupContext = typeof SIGNUP_CONTEXTS[keyof typeof SIGNUP_CONTEXTS];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Signup-paid email can be skipped when welcome email is active only if
|
|
15
|
+
* checkout already has a reliable sign-in path.
|
|
16
|
+
*
|
|
17
|
+
* - HAS_PRECHECKOUT_MAGIC_LINK: standard Portal flow generated signup link before Stripe
|
|
18
|
+
* - ALREADY_AUTHENTICATED: checkout request came from an already signed-in member
|
|
19
|
+
*/
|
|
20
|
+
export function canWelcomeEmailReplaceSignupPaidEmail(signupContext?: SignupContext) {
|
|
21
|
+
return signupContext === SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK || signupContext === SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED;
|
|
22
|
+
}
|
|
@@ -53,14 +53,12 @@ class MemberWelcomeEmailRenderer {
|
|
|
53
53
|
* Applies replacement tokens to a string
|
|
54
54
|
* Supports fallback values: {first_name, "friend"} renders "friend" if name is empty
|
|
55
55
|
* @param {Object} options
|
|
56
|
+
* @param {{id: string, getValue: () => string|undefined}[]} options.definitions - Replacement token definitions
|
|
56
57
|
* @param {string} options.text - The text to process (content body or subject line)
|
|
57
|
-
* @param {Object} options.member - Member data
|
|
58
|
-
* @param {Object} options.siteSettings - Site settings
|
|
59
58
|
* @param {boolean} [options.escapeHtml=false] - Whether to HTML-escape replaced values
|
|
60
59
|
* @returns {string}
|
|
61
60
|
*/
|
|
62
|
-
#applyReplacements({
|
|
63
|
-
const definitions = this.#buildReplacementDefinitions({member, siteSettings});
|
|
61
|
+
#applyReplacements({definitions, text, escapeHtml = false}) {
|
|
64
62
|
let processed = wrapReplacementStrings(text);
|
|
65
63
|
|
|
66
64
|
processed = processed.replace(REPLACEMENT_REGEX, (match, property, fallback) => {
|
|
@@ -96,8 +94,17 @@ class MemberWelcomeEmailRenderer {
|
|
|
96
94
|
});
|
|
97
95
|
}
|
|
98
96
|
|
|
99
|
-
const
|
|
100
|
-
|
|
97
|
+
const definitions = this.#buildReplacementDefinitions({member, siteSettings});
|
|
98
|
+
|
|
99
|
+
// Remove <code> wrappers around replacement strings (Lexical treats curly braces as inline code)
|
|
100
|
+
const tokenIds = definitions.map(d => d.id).join('|');
|
|
101
|
+
content = content.replace(
|
|
102
|
+
new RegExp(`<code>(\\{(?:${tokenIds})(?:\\s*,?\\s*"[^"]*")?\\})<\\/code>`, 'g'),
|
|
103
|
+
'$1'
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
const contentWithReplacements = this.#applyReplacements({definitions, text: content, escapeHtml: true});
|
|
107
|
+
const subjectWithReplacements = this.#applyReplacements({definitions, text: subject, escapeHtml: false});
|
|
101
108
|
|
|
102
109
|
const managePreferencesUrl = new URL('#/portal/account/newsletters', siteSettings.url).href;
|
|
103
110
|
const year = new Date().getFullYear();
|
|
@@ -124,4 +131,3 @@ class MemberWelcomeEmailRenderer {
|
|
|
124
131
|
}
|
|
125
132
|
|
|
126
133
|
module.exports = MemberWelcomeEmailRenderer;
|
|
127
|
-
|
|
@@ -2,7 +2,6 @@ const logging = require('@tryghost/logging');
|
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
3
|
const urlUtils = require('../../../shared/url-utils');
|
|
4
4
|
const settingsCache = require('../../../shared/settings-cache');
|
|
5
|
-
const config = require('../../../shared/config');
|
|
6
5
|
const emailAddressService = require('../email-address');
|
|
7
6
|
const mail = require('../mail');
|
|
8
7
|
// @ts-expect-error type checker has trouble with the dynamic exporting in models
|
|
@@ -50,8 +49,14 @@ class MemberWelcomeEmailService {
|
|
|
50
49
|
}
|
|
51
50
|
|
|
52
51
|
async send({member, memberStatus}) {
|
|
52
|
+
if (!member.email) {
|
|
53
|
+
throw new errors.IncorrectUsageError({
|
|
54
|
+
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
53
58
|
const name = member?.name ? `${member.name} at ` : '';
|
|
54
|
-
logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member
|
|
59
|
+
logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member.email}`);
|
|
55
60
|
|
|
56
61
|
const memberWelcomeEmail = this.#memberWelcomeEmails[memberStatus];
|
|
57
62
|
|
|
@@ -77,17 +82,8 @@ class MemberWelcomeEmailService {
|
|
|
77
82
|
siteSettings: this.#getSiteSettings()
|
|
78
83
|
});
|
|
79
84
|
|
|
80
|
-
const testInbox = config.get('memberWelcomeEmailTestInbox');
|
|
81
|
-
const toEmail = testInbox || member.email;
|
|
82
|
-
|
|
83
|
-
if (!toEmail) {
|
|
84
|
-
throw new errors.IncorrectUsageError({
|
|
85
|
-
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
85
|
await this.#mailer.send({
|
|
90
|
-
to:
|
|
86
|
+
to: member.email,
|
|
91
87
|
subject,
|
|
92
88
|
html,
|
|
93
89
|
text,
|
|
@@ -19,6 +19,7 @@ const newslettersService = require('../newsletters');
|
|
|
19
19
|
const memberAttributionService = require('../member-attribution');
|
|
20
20
|
const emailSuppressionList = require('../email-suppression-list');
|
|
21
21
|
const commentsService = require('../comments');
|
|
22
|
+
const emailAddressService = require('../email-address');
|
|
22
23
|
const {t} = require('../i18n');
|
|
23
24
|
const sentry = require('../../../shared/sentry');
|
|
24
25
|
|
|
@@ -254,7 +255,8 @@ function createApiInstance(config) {
|
|
|
254
255
|
sentry,
|
|
255
256
|
settingsHelpers,
|
|
256
257
|
urlUtils,
|
|
257
|
-
commentsService
|
|
258
|
+
commentsService,
|
|
259
|
+
emailAddressService: emailAddressService.service
|
|
258
260
|
});
|
|
259
261
|
|
|
260
262
|
return membersApiInstance;
|
|
@@ -7,6 +7,8 @@ const errors = require('@tryghost/errors');
|
|
|
7
7
|
const {isEmail} = require('@tryghost/validator');
|
|
8
8
|
const normalizeEmail = require('../utils/normalize-email');
|
|
9
9
|
const {getInboxLinks} = require('../../../../lib/get-inbox-links');
|
|
10
|
+
const {SIGNUP_CONTEXTS} = require('../../../lib/member-signup-contexts');
|
|
11
|
+
/** @typedef {import('../../../lib/member-signup-contexts').SignupContext} SignupContext */
|
|
10
12
|
|
|
11
13
|
const messages = {
|
|
12
14
|
emailRequired: 'Email is required.',
|
|
@@ -75,6 +77,7 @@ module.exports = class RouterController {
|
|
|
75
77
|
* @param {any} deps.settingsCache
|
|
76
78
|
* @param {any} deps.settingsHelpers
|
|
77
79
|
* @param {any} deps.urlUtils
|
|
80
|
+
* @param {any} deps.emailAddressService
|
|
78
81
|
*/
|
|
79
82
|
constructor({
|
|
80
83
|
offersAPI,
|
|
@@ -93,7 +96,8 @@ module.exports = class RouterController {
|
|
|
93
96
|
sentry,
|
|
94
97
|
settingsCache,
|
|
95
98
|
settingsHelpers,
|
|
96
|
-
urlUtils
|
|
99
|
+
urlUtils,
|
|
100
|
+
emailAddressService
|
|
97
101
|
}) {
|
|
98
102
|
this._offersAPI = offersAPI;
|
|
99
103
|
this._paymentsService = paymentsService;
|
|
@@ -112,6 +116,7 @@ module.exports = class RouterController {
|
|
|
112
116
|
this._settingsCache = settingsCache;
|
|
113
117
|
this._settingsHelpers = settingsHelpers;
|
|
114
118
|
this._urlUtils = urlUtils;
|
|
119
|
+
this._emailAddressService = emailAddressService;
|
|
115
120
|
}
|
|
116
121
|
|
|
117
122
|
async ensureStripe(_req, res, next) {
|
|
@@ -389,6 +394,12 @@ module.exports = class RouterController {
|
|
|
389
394
|
});
|
|
390
395
|
}
|
|
391
396
|
|
|
397
|
+
if (!offer.tier) {
|
|
398
|
+
throw new BadRequestError({
|
|
399
|
+
message: 'Offer does not have a tier'
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
392
403
|
tier = await this._tiersService.api.read(offer.tier.id);
|
|
393
404
|
cadence = offer.cadence;
|
|
394
405
|
} else if (tierId) {
|
|
@@ -466,6 +477,8 @@ module.exports = class RouterController {
|
|
|
466
477
|
}
|
|
467
478
|
|
|
468
479
|
const member = options.member;
|
|
480
|
+
/** @type {SignupContext} */
|
|
481
|
+
let ghostSignupContext = (options.isAuthenticated && member) ? SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED : SIGNUP_CONTEXTS.NEEDS_MAGIC_LINK_EMAIL;
|
|
469
482
|
|
|
470
483
|
if (!member && options.email) {
|
|
471
484
|
// Create a signup link if there is no member with this email address
|
|
@@ -482,6 +495,7 @@ module.exports = class RouterController {
|
|
|
482
495
|
// Redirect to the original success url after sign up
|
|
483
496
|
referrer: options.successUrl
|
|
484
497
|
});
|
|
498
|
+
ghostSignupContext = SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK;
|
|
485
499
|
}
|
|
486
500
|
|
|
487
501
|
if (member) {
|
|
@@ -506,6 +520,9 @@ module.exports = class RouterController {
|
|
|
506
520
|
}
|
|
507
521
|
}
|
|
508
522
|
|
|
523
|
+
// Set by server to distinguish between checkout flows in Stripe webhooks.
|
|
524
|
+
options.metadata.ghostSignupContext = ghostSignupContext;
|
|
525
|
+
|
|
509
526
|
try {
|
|
510
527
|
const paymentLink = await this._paymentsService.getPaymentLink(options);
|
|
511
528
|
|
|
@@ -733,7 +750,7 @@ module.exports = class RouterController {
|
|
|
733
750
|
|
|
734
751
|
const inboxLinks = await getInboxLinks({
|
|
735
752
|
recipient: normalizedEmail,
|
|
736
|
-
sender: this.
|
|
753
|
+
sender: this._emailAddressService.getMembersSupportAddress(),
|
|
737
754
|
dnsResolver: this.#inboxLinksDnsResolver
|
|
738
755
|
});
|
|
739
756
|
if (inboxLinks) {
|
|
@@ -79,7 +79,8 @@ module.exports = function MembersAPI({
|
|
|
79
79
|
sentry,
|
|
80
80
|
settingsHelpers,
|
|
81
81
|
urlUtils,
|
|
82
|
-
commentsService
|
|
82
|
+
commentsService,
|
|
83
|
+
emailAddressService
|
|
83
84
|
}) {
|
|
84
85
|
const tokenService = new TokenService({
|
|
85
86
|
privateKey,
|
|
@@ -213,7 +214,8 @@ module.exports = function MembersAPI({
|
|
|
213
214
|
settingsCache,
|
|
214
215
|
settingsHelpers,
|
|
215
216
|
sentry,
|
|
216
|
-
urlUtils
|
|
217
|
+
urlUtils,
|
|
218
|
+
emailAddressService
|
|
217
219
|
});
|
|
218
220
|
|
|
219
221
|
const wellKnownController = new WellKnownController({
|
|
@@ -1769,7 +1769,7 @@ module.exports = class MemberRepository {
|
|
|
1769
1769
|
});
|
|
1770
1770
|
}
|
|
1771
1771
|
|
|
1772
|
-
if (offer.tier.id !== tierId) {
|
|
1772
|
+
if (offer.tier && offer.tier.id !== tierId) {
|
|
1773
1773
|
throw new errors.BadRequestError({
|
|
1774
1774
|
message: tpl(messages.offerTierMismatch)
|
|
1775
1775
|
});
|
|
@@ -72,6 +72,11 @@ class PaymentsService {
|
|
|
72
72
|
let coupon = null;
|
|
73
73
|
let trialDays = null;
|
|
74
74
|
if (offer) {
|
|
75
|
+
if (!offer.tier) {
|
|
76
|
+
throw new BadRequestError({
|
|
77
|
+
message: 'Offer does not have a tier'
|
|
78
|
+
});
|
|
79
|
+
}
|
|
75
80
|
if (!tier.id.equals(offer.tier.id)) {
|
|
76
81
|
throw new BadRequestError({
|
|
77
82
|
message: 'This Offer is not valid for the Tier'
|
|
@@ -10,7 +10,7 @@ const iconv = require('iconv-lite');
|
|
|
10
10
|
const path = require('path');
|
|
11
11
|
|
|
12
12
|
// Some sites block non-standard user agents so we need to mimic a typical browser
|
|
13
|
-
const USER_AGENT = 'Mozilla/5.0 (
|
|
13
|
+
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36';
|
|
14
14
|
|
|
15
15
|
const messages = {
|
|
16
16
|
noUrlProvided: 'No url provided.',
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
* @prop {number} redemption_count
|
|
27
27
|
* @prop {'signup'|'retention'} redemption_type
|
|
28
28
|
*
|
|
29
|
-
* @prop {object} tier
|
|
30
|
-
* @prop {string} tier.id
|
|
31
|
-
* @prop {string} tier.name
|
|
29
|
+
* @prop {object|null} tier
|
|
30
|
+
* @prop {string} [tier.id]
|
|
31
|
+
* @prop {string} [tier.name]
|
|
32
32
|
* @prop {string} created_at
|
|
33
33
|
* @prop {string|null} last_redeemed
|
|
34
34
|
*/
|
|
@@ -55,10 +55,9 @@ class OfferMapper {
|
|
|
55
55
|
status: offer.status.value,
|
|
56
56
|
redemption_count: offer.redemptionCount,
|
|
57
57
|
redemption_type: offer.redemptionType.value,
|
|
58
|
-
tier:
|
|
59
|
-
id: offer.tier.id,
|
|
60
|
-
|
|
61
|
-
},
|
|
58
|
+
tier: offer.tier
|
|
59
|
+
? {id: offer.tier.id, name: offer.tier.name}
|
|
60
|
+
: null,
|
|
62
61
|
created_at: offer.createdAt,
|
|
63
62
|
last_redeemed: offer.lastRedeemed
|
|
64
63
|
};
|
|
@@ -165,12 +165,12 @@ class OffersAPI {
|
|
|
165
165
|
return [];
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Filter by tier and cadence
|
|
169
|
-
let available = allOffers.filter(offer => offer.tier.id === tierId && offer.cadence.value === cadence);
|
|
168
|
+
// Filter by tier and cadence - Null-tier offers (retention) match any tier with the correct cadence
|
|
169
|
+
let available = allOffers.filter(offer => (offer.tier === null || offer.tier.id === tierId) && offer.cadence.value === cadence);
|
|
170
170
|
debug(`listOffersAvailableToSubscription: ${available.length} offers match tier and cadence`);
|
|
171
171
|
|
|
172
172
|
if (available.length === 0) {
|
|
173
|
-
const tierIds = [...new Set(allOffers.map(o => o.tier.id))];
|
|
173
|
+
const tierIds = [...new Set(allOffers.filter(o => o.tier !== null).map(o => o.tier.id))];
|
|
174
174
|
const cadences = [...new Set(allOffers.map(o => o.cadence.value))];
|
|
175
175
|
debug(`listOffersAvailableToSubscription: no offers match - available tiers: [${tierIds.join(', ')}], available cadences: [${cadences.join(', ')}]`);
|
|
176
176
|
|
|
@@ -18,6 +18,7 @@ class InvalidOfferCode extends InvalidPropError {}
|
|
|
18
18
|
class InvalidOfferType extends InvalidPropError {}
|
|
19
19
|
class InvalidOfferAmount extends InvalidPropError {}
|
|
20
20
|
class InvalidOfferCurrency extends InvalidPropError {}
|
|
21
|
+
class InvalidOfferTier extends InvalidPropError {}
|
|
21
22
|
class InvalidOfferTierName extends InvalidPropError {}
|
|
22
23
|
class InvalidOfferCadence extends InvalidPropError {}
|
|
23
24
|
class InvalidOfferDuration extends InvalidPropError {}
|
|
@@ -36,6 +37,7 @@ module.exports = {
|
|
|
36
37
|
InvalidOfferCurrency,
|
|
37
38
|
InvalidOfferCadence,
|
|
38
39
|
InvalidOfferDuration,
|
|
40
|
+
InvalidOfferTier,
|
|
39
41
|
InvalidOfferTierName,
|
|
40
42
|
InvalidOfferCoupon,
|
|
41
43
|
InvalidOfferStatus,
|
|
@@ -31,7 +31,7 @@ const StripeCoupon = require('./stripe-coupon');
|
|
|
31
31
|
* @prop {OfferCurrency} [currency]
|
|
32
32
|
* @prop {OfferStatus} status
|
|
33
33
|
* @prop {string|null} [stripeCouponId]
|
|
34
|
-
* @prop {OfferTier} tier
|
|
34
|
+
* @prop {OfferTier|null} tier
|
|
35
35
|
* @prop {number} redemptionCount
|
|
36
36
|
* @prop {OfferRedemptionType} redemptionType
|
|
37
37
|
* @prop {string} createdAt
|
|
@@ -55,7 +55,7 @@ const StripeCoupon = require('./stripe-coupon');
|
|
|
55
55
|
* @prop {string} [stripe_coupon_id]
|
|
56
56
|
* @prop {number} [redemptionCount]
|
|
57
57
|
* @prop {string} [redemption_type]
|
|
58
|
-
* @prop {TierProps|OfferTier} tier
|
|
58
|
+
* @prop {TierProps|OfferTier|null} tier
|
|
59
59
|
* @prop {Date} [created_at]
|
|
60
60
|
* @prop {Date} [last_redeemed]
|
|
61
61
|
*/
|
|
@@ -356,8 +356,20 @@ class Offer {
|
|
|
356
356
|
}
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
const tier = OfferTier.create(data.tier);
|
|
360
359
|
const redemptionType = OfferRedemptionType.create(data.redemption_type || 'signup');
|
|
360
|
+
const tier = data.tier ? OfferTier.create(data.tier) : null;
|
|
361
|
+
|
|
362
|
+
if (redemptionType.value === 'signup' && !tier) {
|
|
363
|
+
throw new errors.InvalidOfferTier({
|
|
364
|
+
message: 'Signup offers must be associated with a tier'
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (redemptionType.value === 'retention' && tier) {
|
|
369
|
+
throw new errors.InvalidOfferTier({
|
|
370
|
+
message: 'Retention offers cannot be associated with a specific tier'
|
|
371
|
+
});
|
|
372
|
+
}
|
|
361
373
|
|
|
362
374
|
return new Offer({
|
|
363
375
|
id,
|
|
@@ -125,10 +125,9 @@ class OfferBookshelfRepository {
|
|
|
125
125
|
redemptionCount: count,
|
|
126
126
|
redemption_type: json.redemption_type,
|
|
127
127
|
status: json.active ? 'active' : 'archived',
|
|
128
|
-
tier:
|
|
129
|
-
id: json.product.id,
|
|
130
|
-
|
|
131
|
-
},
|
|
128
|
+
tier: json.product && json.product.id
|
|
129
|
+
? {id: json.product.id, name: json.product.name}
|
|
130
|
+
: null,
|
|
132
131
|
created_at: json.created_at,
|
|
133
132
|
last_redeemed: lastRedeemedObject.length > 0 ? lastRedeemedObject[0].created_at : null
|
|
134
133
|
}, null);
|
|
@@ -225,7 +224,7 @@ class OfferBookshelfRepository {
|
|
|
225
224
|
discount_type: offer.type.value === 'fixed' ? 'amount' : offer.type.value,
|
|
226
225
|
discount_amount: offer.amount.value,
|
|
227
226
|
interval: offer.cadence.value,
|
|
228
|
-
product_id: offer.tier.id,
|
|
227
|
+
product_id: offer.tier ? offer.tier.id : null,
|
|
229
228
|
duration: offer.duration.value.type,
|
|
230
229
|
duration_in_months: offer.duration.value.type === 'repeating' ? offer.duration.value.months : null,
|
|
231
230
|
currency: offer.currency ? offer.currency.value : null,
|
|
@@ -19,6 +19,7 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
|
|
|
19
19
|
* @param {string} [options.timezone] - Timezone for the query
|
|
20
20
|
* @param {string} [options.memberStatus] - Member status filter (defaults to 'all')
|
|
21
21
|
* @param {string} [options.postType] - Post type filter
|
|
22
|
+
* @param {string} [options.version] - Version override (e.g., 'v3') - bypasses config version
|
|
22
23
|
* @returns {Object} Object with URL and request options
|
|
23
24
|
*/
|
|
24
25
|
const buildRequest = (pipeName, options = {}) => {
|
|
@@ -32,9 +33,10 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
|
|
|
32
33
|
const tokenData = tinybirdService.getToken();
|
|
33
34
|
const token = tokenData?.token;
|
|
34
35
|
|
|
35
|
-
// Use version from
|
|
36
|
+
// Use version from options if provided, otherwise fall back to config
|
|
36
37
|
// Pattern: api_kpis -> api_kpis_v2 (single underscore + version)
|
|
37
|
-
|
|
38
|
+
// Pass empty string to force unversioned endpoint
|
|
39
|
+
const version = options.version !== undefined ? options.version : statsConfig?.version;
|
|
38
40
|
const pipeUrl = version ?
|
|
39
41
|
`/v0/pipes/${pipeName}_${version}.json` :
|
|
40
42
|
`/v0/pipes/${pipeName}.json`;
|
|
@@ -64,7 +66,7 @@ const create = ({config, request, settingsCache, tinybirdService}) => {
|
|
|
64
66
|
}
|
|
65
67
|
// Add any other options that might be needed
|
|
66
68
|
Object.entries(options).forEach(([key, value]) => {
|
|
67
|
-
if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus', 'postType'].includes(key) && value !== undefined && value !== null) {
|
|
69
|
+
if (!['dateFrom', 'dateTo', 'timezone', 'memberStatus', 'postType', 'version'].includes(key) && value !== undefined && value !== null) {
|
|
68
70
|
// Convert camelCase to snake_case for Tinybird API
|
|
69
71
|
const snakeKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
70
72
|
// Handle arrays by converting them to comma-separated strings for Tinybird
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const _ = require('lodash');
|
|
2
2
|
const errors = require('@tryghost/errors');
|
|
3
3
|
const logging = require('@tryghost/logging');
|
|
4
|
+
const {
|
|
5
|
+
canWelcomeEmailReplaceSignupPaidEmail
|
|
6
|
+
} = require('../../../lib/member-signup-contexts');
|
|
7
|
+
/** @typedef {import('../../../lib/member-signup-contexts').SignupContext} SignupContext */
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* Handles `checkout.session.completed` webhook events
|
|
@@ -24,6 +28,7 @@ module.exports = class CheckoutSessionEventService {
|
|
|
24
28
|
* @param {object} deps.donationRepository
|
|
25
29
|
* @param {object} deps.staffServiceEmails
|
|
26
30
|
* @param {function} deps.sendSignupEmail
|
|
31
|
+
* @param {function} deps.isPaidWelcomeEmailActive
|
|
27
32
|
*/
|
|
28
33
|
constructor(deps) {
|
|
29
34
|
this.api = deps.api;
|
|
@@ -260,7 +265,18 @@ module.exports = class CheckoutSessionEventService {
|
|
|
260
265
|
}
|
|
261
266
|
|
|
262
267
|
if (checkoutType !== 'upgrade') {
|
|
263
|
-
|
|
268
|
+
const ghostSignupContext = /** @type {SignupContext | undefined} */ (session.metadata?.ghostSignupContext);
|
|
269
|
+
const shouldSkipSignupEmailWhenWelcomeEmailActive = canWelcomeEmailReplaceSignupPaidEmail(ghostSignupContext);
|
|
270
|
+
|
|
271
|
+
if (shouldSkipSignupEmailWhenWelcomeEmailActive) {
|
|
272
|
+
const isPaidWelcomeEmailActive = await this.deps.isPaidWelcomeEmailActive();
|
|
273
|
+
if (!isPaidWelcomeEmailActive) {
|
|
274
|
+
this.deps.sendSignupEmail(customer.email);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
// Direct checkout flows do not have a pre-checkout sign-in path.
|
|
278
|
+
this.deps.sendSignupEmail(customer.email);
|
|
279
|
+
}
|
|
264
280
|
}
|
|
265
281
|
}
|
|
266
282
|
};
|
|
@@ -8,6 +8,7 @@ const {StripeLiveEnabledEvent, StripeLiveDisabledEvent} = require('./events');
|
|
|
8
8
|
const SubscriptionEventService = require('./services/webhook/subscription-event-service');
|
|
9
9
|
const InvoiceEventService = require('./services/webhook/invoice-event-service');
|
|
10
10
|
const CheckoutSessionEventService = require('./services/webhook/checkout-session-event-service');
|
|
11
|
+
const memberWelcomeEmailService = require('../member-welcome-emails/service');
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @typedef {object} IStripeServiceConfig
|
|
@@ -120,6 +121,13 @@ module.exports = class StripeService {
|
|
|
120
121
|
},
|
|
121
122
|
tokenData: {}
|
|
122
123
|
});
|
|
124
|
+
},
|
|
125
|
+
async isPaidWelcomeEmailActive() {
|
|
126
|
+
if (!labs.isSet('welcomeEmails')) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
memberWelcomeEmailService.init();
|
|
130
|
+
return memberWelcomeEmailService.api.isMemberWelcomeEmailActive('paid');
|
|
123
131
|
}
|
|
124
132
|
});
|
|
125
133
|
|
package/core/shared/labs.js
CHANGED
|
@@ -27,7 +27,9 @@ const GA_FEATURES = [
|
|
|
27
27
|
'explore',
|
|
28
28
|
'inboxlinks',
|
|
29
29
|
'commentModeration',
|
|
30
|
-
'commentPermalinks'
|
|
30
|
+
'commentPermalinks',
|
|
31
|
+
'featurebaseFeedback',
|
|
32
|
+
'welcomeEmails'
|
|
31
33
|
];
|
|
32
34
|
|
|
33
35
|
// These features are considered publicly available and can be enabled/disabled by users
|
|
@@ -48,10 +50,8 @@ const PRIVATE_FEATURES = [
|
|
|
48
50
|
'emailCustomization',
|
|
49
51
|
'tagsX',
|
|
50
52
|
'emailUniqueid',
|
|
51
|
-
'welcomeEmails',
|
|
52
53
|
'themeTranslation',
|
|
53
54
|
'indexnow',
|
|
54
|
-
'featurebaseFeedback',
|
|
55
55
|
'transistor'
|
|
56
56
|
];
|
|
57
57
|
|
package/core/shared/url-utils.js
CHANGED
|
@@ -8,7 +8,8 @@ const urlUtils = new UrlUtils({
|
|
|
8
8
|
getAdminUrl: config.getAdminUrl,
|
|
9
9
|
assetBaseUrls: {
|
|
10
10
|
media: config.get('urls:media'),
|
|
11
|
-
files: config.get('urls:files')
|
|
11
|
+
files: config.get('urls:files'),
|
|
12
|
+
image: config.get('urls:image')
|
|
12
13
|
},
|
|
13
14
|
slugs: config.get('slugs').protected,
|
|
14
15
|
redirectCacheMaxAge: config.get('caching:301:maxAge'),
|