ghost 6.17.2 → 6.18.1
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.17.2.tgz → tryghost-i18n-6.18.1.tgz} +0 -0
- package/components/{tryghost-parse-email-address-6.17.2.tgz → tryghost-parse-email-address-6.18.1.tgz} +0 -0
- package/core/built/admin/assets/{PolarAngleAxis-B8JrAxf_.js → PolarAngleAxis-Dod0DwfL.js} +1 -1
- package/core/built/admin/assets/{_baseAssignValue-Bbalamaq.js → _baseAssignValue-DnkbkowM.js} +1 -1
- package/core/built/admin/assets/{a-large-small-CgWPeKpi.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-BWi3-ftq.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DdMo_pvD.mjs → index-B3PtvbUw.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-DYEF3NiM.mjs → index-Did70N9h.mjs} +2 -2
- package/core/built/admin/assets/admin-x-settings/{index-BXChUyr6.mjs → index-JaMlaX9G.mjs} +30 -19
- package/core/built/admin/assets/admin-x-settings/{modals-Ck3AHDx3.mjs → modals-Bwrf9ptQ.mjs} +8060 -8067
- package/core/built/admin/assets/{at-sign-BottTo2X.js → at-sign-Bz-SU-S_.js} +1 -1
- package/core/built/admin/assets/{audience-CcGOt5Fb.js → audience-DmVdEmIe.js} +1 -1
- package/core/built/admin/assets/{avatar-flipboard-MxW2k124.js → avatar-flipboard-C7sxDVEM.js} +1 -1
- package/core/built/admin/assets/{bluesky-sharing-BBmDBlxr.js → bluesky-sharing-DJniSF6N.js} +1 -1
- package/core/built/admin/assets/{chart-pimL5Chk.js → chart-mYz3IJwm.js} +1 -1
- package/core/built/admin/assets/{chunk.524.9a8f5123fd3ecf3e4451.js → chunk.524.9d300778a63b42b0de62.js} +7 -7
- package/core/built/admin/assets/{chunk.582.41886d06af2895d571bd.js → chunk.582.2e363cd976d9bba998b9.js} +10 -10
- package/core/built/admin/assets/{code-editor-view-tCZOpc_h.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-aC0WUVtq.js → copy-Bpq3uhnh.js} +1 -1
- package/core/built/admin/assets/{data-list-BOdyZGWI.js → data-list-BYNMbRIq.js} +1 -1
- package/core/built/admin/assets/{deleted-feed-item-BnUJ8bWP.js → deleted-feed-item-B_AwUKZy.js} +1 -1
- package/core/built/admin/assets/{edit-profile-CEgPhWMJ.js → edit-profile-DZo2ZcOu.js} +1 -1
- package/core/built/admin/assets/{empty-indicator-CR_9BHAH.js → empty-indicator-Bn5wG9-T.js} +1 -1
- package/core/built/admin/assets/{en-ajofhc7o.js → en-D4zIrMLN.js} +1 -1
- package/core/built/admin/assets/{feed-Ci-ODCHE.js → feed-Bgq4RarU.js} +1 -1
- package/core/built/admin/assets/{filters-CmJJOv3I.js → filters-CpVwu1Gk.js} +1 -1
- package/core/built/admin/assets/{gh-chart-hn7wEnik.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-CQceZQ1g.js → growth-CiPDnWxJ.js} +1 -1
- package/core/built/admin/assets/{hash-BcaWmkw5.js → hash-Bc1e38oH.js} +1 -1
- package/core/built/admin/assets/{inbox-D9hID-rO.js → inbox-BcZNBNhk.js} +1 -1
- package/core/built/admin/assets/{index-d_UH71zm.js → index-BFEx_ouC.js} +1 -1
- package/core/built/admin/assets/{index-kXF42nga.js → index-B_7QsC5T.js} +1 -1
- package/core/built/admin/assets/{index-CZ3lARQ2.js → index-Bm5ZeLtt.js} +1 -1
- package/core/built/admin/assets/{index-DJDgMk-T.js → index-BpcL7RmI.js} +2 -2
- package/core/built/admin/assets/{index-g4BWcHhl.js → index-Brg4tecQ.js} +1 -1
- package/core/built/admin/assets/{index-f5Lv9kLb.js → index-CMTzNTew.js} +29 -29
- package/core/built/admin/assets/{index-Cl2eSBwg.js → index-CTfJflJ2.js} +1 -1
- package/core/built/admin/assets/{index-BJS8H75W.js → index-Cn32t_gv.js} +1 -1
- package/core/built/admin/assets/{index-D8syEvSm.js → index-CsidX7Si.js} +1 -1
- package/core/built/admin/assets/{index-DO76vL0R.js → index-D212VvOz.js} +1 -1
- package/core/built/admin/assets/{index-B1rKIiOr.js → index-D67LJ_H4.js} +1 -1
- package/core/built/admin/assets/{index-BmK6ThYl.js → index-DU3bv9jz.js} +1 -1
- package/core/built/admin/assets/{index-DYM1MbXR.js → index-Dv13wKfX.js} +1 -1
- package/core/built/admin/assets/{index-Do091GLu.js → index-ETsoQLbU.js} +1 -1
- package/core/built/admin/assets/{index-rluYcfd-.js → index-ZYDRMgcT.js} +1 -1
- package/core/built/admin/assets/{koenig-lexical-D2XHvS2J.js → koenig-lexical-DZUWzN0P.js} +1 -1
- package/core/built/admin/assets/{kpi-card-Dga9rFZ5.js → kpi-card-DENsK2xK.js} +1 -1
- package/core/built/admin/assets/{kpis-BbnqD1za.js → kpis-CDrs2iS1.js} +1 -1
- package/core/built/admin/assets/{label-D8WHEOe0.js → label-BznQtEEo.js} +1 -1
- package/core/built/admin/assets/{links-dgwx7ko1.js → links-lpAC3T1p.js} +1 -1
- package/core/built/admin/assets/{lucide-react-BARFK2PZ.js → lucide-react-CBigk-fq.js} +1 -1
- package/core/built/admin/assets/{main-layout-CcjtCuF_.js → main-layout-_SYQRjIl.js} +1 -1
- package/core/built/admin/assets/{message-square-text-BxquqPhA.js → message-square-text-cV8O_qKq.js} +1 -1
- package/core/built/admin/assets/{minus-Ziy4LSYD.js → minus-NvnQTlW7.js} +1 -1
- package/core/built/admin/assets/{modals-fMQzR7L6.js → modals-XRSkribf.js} +3 -3
- package/core/built/admin/assets/{moderation-Ci9UNwiZ.js → moderation-BQp1GEWG.js} +1 -1
- package/core/built/admin/assets/{newsletter-DnrS7Fvi.js → newsletter-foM6KNNV.js} +1 -1
- package/core/built/admin/assets/{newsletters-D78pgnas.js → newsletters-BexdXUhn.js} +1 -1
- package/core/built/admin/assets/{note-BAfNweZN.js → note-DuaUGOeZ.js} +1 -1
- package/core/built/admin/assets/{overview-CWQPNOF0.js → overview-DgKBNqyc.js} +1 -1
- package/core/built/admin/assets/{pagemenu-DjSqq3Wu.js → pagemenu-CZyroidv.js} +1 -1
- package/core/built/admin/assets/{post-analytics-c8kRV0YB.js → post-analytics-DLK2SOSQ.js} +1 -1
- package/core/built/admin/assets/{post-analytics-context-CEqWyqgm.js → post-analytics-context-CF7C67-0.js} +1 -1
- package/core/built/admin/assets/{post-analytics-header-CpPPBAz4.js → post-analytics-header-7GtJCx0W.js} +1 -1
- package/core/built/admin/assets/{post-share-modal-Dln0kZhn.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-C1Eh5Ypq.js → posts-CL9UDYoW.js} +1 -1
- package/core/built/admin/assets/{repeat-CN4fiG5H.js → repeat-DgH39UKE.js} +1 -1
- package/core/built/admin/assets/{reply-BfWgvH7y.js → reply-DAaNxiy8.js} +1 -1
- package/core/built/admin/assets/{select-B_0NBkLz.js → select-Cor2wFXT.js} +1 -1
- package/core/built/admin/assets/{settings-BqaVcJee.js → settings-BeumESEN.js} +5 -5
- package/core/built/admin/assets/{settings-Dk9E7Fqv.js → settings-xRx917Gj.js} +1 -1
- package/core/built/admin/assets/{sort-button-C2M35dLh.js → sort-button-BNW3i4Lb.js} +1 -1
- package/core/built/admin/assets/{source-icon-DkGi13e_.js → source-icon-DvDuzw73.js} +1 -1
- package/core/built/admin/assets/{sprout-BKiwQsGn.js → sprout-C3cc0c-K.js} +1 -1
- package/core/built/admin/assets/{square-BEpRc_fY.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-CRgy_cWm.js → stats-2Jelnn-Q.js} +1 -1
- package/core/built/admin/assets/{stats-view-CTQ4qO3S.js → stats-view-CESy8ELH.js} +1 -1
- package/core/built/admin/assets/{step-1-CNbdmB8C.js → step-1-DrqdolAh.js} +1 -1
- package/core/built/admin/assets/{step-2-Ddrl2uae.js → step-2-DmEpKck5.js} +1 -1
- package/core/built/admin/assets/{step-3-CbqkEtwh.js → step-3-Bus-0o0n.js} +1 -1
- package/core/built/admin/assets/{table-Dw9xFlFd.js → table-BQUcKHfm.js} +1 -1
- package/core/built/admin/assets/{tabs-DCHagOip.js → tabs-BmdL0X4U.js} +1 -1
- package/core/built/admin/assets/{tags-DV2SSdMK.js → tags-CLxXZlOO.js} +1 -1
- package/core/built/admin/assets/{tags-BsDlfsPN.js → tags-EchqlZUJ.js} +1 -1
- package/core/built/admin/assets/{tiers-tc5tafWV.js → tiers-nCGyTly9.js} +1 -1
- package/core/built/admin/assets/{toggle-group-DS-ZVhCr.js → toggle-group-CM5uf7J1.js} +1 -1
- package/core/built/admin/assets/{topic-filter-BfoEqHRD.js → topic-filter-LTRvZ8aU.js} +1 -1
- package/core/built/admin/assets/{trash-CpnxYen-.js → trash-u5BxolyH.js} +1 -1
- package/core/built/admin/assets/{url-helpers-wKPPeCtD.js → url-helpers-D41fEt51.js} +1 -1
- package/core/built/admin/assets/{use-growth-stats-CbHIETF_.js → use-growth-stats-BJ0O9ewi.js} +1 -1
- package/core/built/admin/assets/{use-infinite-virtual-scroll-DU1EUhyj.js → use-infinite-virtual-scroll-APZWciOk.js} +1 -1
- package/core/built/admin/assets/{use-simple-pagination-X6kmRz2u.js → use-simple-pagination-DVRHeaAR.js} +1 -1
- package/core/built/admin/assets/{user-round-check-DYkrjHxF.js → user-round-check-B6j98D6d.js} +1 -1
- package/core/built/admin/assets/{wallet-cards-CSdWOmJE.js → wallet-cards-KmOh29LP.js} +1 -1
- package/core/built/admin/assets/{web-C39IKnvl.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/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 +84 -15
- package/core/server/services/members/members-api/controllers/router-controller.js +14 -0
- 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/core/built/admin/assets/comments-OXqhcC0t.js +0 -1
|
@@ -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,11 +2,12 @@ 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');
|
|
6
|
+
const settingsHelpers = require('../settings-helpers');
|
|
7
|
+
const EmailAddressParser = require('../email-address/email-address-parser');
|
|
7
8
|
const mail = require('../mail');
|
|
8
9
|
// @ts-expect-error type checker has trouble with the dynamic exporting in models
|
|
9
|
-
const {AutomatedEmail} = require('../../models');
|
|
10
|
+
const {AutomatedEmail, Newsletter} = require('../../models');
|
|
10
11
|
const MemberWelcomeEmailRenderer = require('./member-welcome-email-renderer');
|
|
11
12
|
const {MEMBER_WELCOME_EMAIL_LOG_KEY, MEMBER_WELCOME_EMAIL_SLUGS, MESSAGES} = require('./constants');
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ class MemberWelcomeEmailService {
|
|
|
14
15
|
#mailer;
|
|
15
16
|
#renderer;
|
|
16
17
|
#memberWelcomeEmails = {free: null, paid: null};
|
|
18
|
+
#defaultNewsletterSenderOptions = null;
|
|
17
19
|
|
|
18
20
|
constructor() {
|
|
19
21
|
emailAddressService.init();
|
|
@@ -29,7 +31,71 @@ class MemberWelcomeEmailService {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
async #getDefaultNewsletterSenderOptions() {
|
|
35
|
+
const newsletter = await Newsletter.getDefaultNewsletter();
|
|
36
|
+
if (!newsletter) {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let senderName = settingsCache.get('title') || '';
|
|
41
|
+
if (newsletter.get('sender_name')) {
|
|
42
|
+
senderName = newsletter.get('sender_name');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let fromAddress = settingsHelpers.getNoReplyAddress();
|
|
46
|
+
if (newsletter.get('sender_email')) {
|
|
47
|
+
fromAddress = newsletter.get('sender_email');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const fromAddresses = emailAddressService.service.getAddress({
|
|
51
|
+
from: {
|
|
52
|
+
address: fromAddress,
|
|
53
|
+
name: senderName || undefined
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const from = EmailAddressParser.stringify(fromAddresses.from);
|
|
58
|
+
const replyToSetting = newsletter.get('sender_reply_to');
|
|
59
|
+
let replyTo = null;
|
|
60
|
+
|
|
61
|
+
if (replyToSetting === 'support') {
|
|
62
|
+
replyTo = settingsHelpers.getMembersSupportAddress();
|
|
63
|
+
} else if (replyToSetting === 'newsletter' && !emailAddressService.service.managedEmailEnabled) {
|
|
64
|
+
replyTo = from;
|
|
65
|
+
} else {
|
|
66
|
+
const addresses = emailAddressService.service.getAddress({
|
|
67
|
+
from: {
|
|
68
|
+
address: fromAddress,
|
|
69
|
+
name: senderName || undefined
|
|
70
|
+
},
|
|
71
|
+
replyTo: replyToSetting === 'newsletter' ? undefined : {address: replyToSetting}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (addresses.replyTo) {
|
|
75
|
+
replyTo = EmailAddressParser.stringify(addresses.replyTo);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
from,
|
|
81
|
+
...(replyTo ? {
|
|
82
|
+
replyTo
|
|
83
|
+
} : {})
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async #getSenderOptions() {
|
|
88
|
+
if (this.#defaultNewsletterSenderOptions) {
|
|
89
|
+
return this.#defaultNewsletterSenderOptions;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions();
|
|
93
|
+
return this.#defaultNewsletterSenderOptions;
|
|
94
|
+
}
|
|
95
|
+
|
|
32
96
|
async loadMemberWelcomeEmails() {
|
|
97
|
+
this.#defaultNewsletterSenderOptions = await this.#getDefaultNewsletterSenderOptions();
|
|
98
|
+
|
|
33
99
|
for (const [memberStatus, slug] of Object.entries(MEMBER_WELCOME_EMAIL_SLUGS)) {
|
|
34
100
|
const row = await AutomatedEmail.findOne({slug});
|
|
35
101
|
|
|
@@ -50,8 +116,14 @@ class MemberWelcomeEmailService {
|
|
|
50
116
|
}
|
|
51
117
|
|
|
52
118
|
async send({member, memberStatus}) {
|
|
119
|
+
if (!member.email) {
|
|
120
|
+
throw new errors.IncorrectUsageError({
|
|
121
|
+
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
53
125
|
const name = member?.name ? `${member.name} at ` : '';
|
|
54
|
-
logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member
|
|
126
|
+
logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending welcome email to ${name}${member.email}`);
|
|
55
127
|
|
|
56
128
|
const memberWelcomeEmail = this.#memberWelcomeEmails[memberStatus];
|
|
57
129
|
|
|
@@ -77,21 +149,15 @@ class MemberWelcomeEmailService {
|
|
|
77
149
|
siteSettings: this.#getSiteSettings()
|
|
78
150
|
});
|
|
79
151
|
|
|
80
|
-
const
|
|
81
|
-
const toEmail = testInbox || member.email;
|
|
82
|
-
|
|
83
|
-
if (!toEmail) {
|
|
84
|
-
throw new errors.IncorrectUsageError({
|
|
85
|
-
message: MESSAGES.MISSING_RECIPIENT_EMAIL
|
|
86
|
-
});
|
|
87
|
-
}
|
|
152
|
+
const senderOptions = await this.#getSenderOptions();
|
|
88
153
|
|
|
89
154
|
await this.#mailer.send({
|
|
90
|
-
to:
|
|
155
|
+
to: member.email,
|
|
91
156
|
subject,
|
|
92
157
|
html,
|
|
93
158
|
text,
|
|
94
|
-
forceTextContent: true
|
|
159
|
+
forceTextContent: true,
|
|
160
|
+
...senderOptions
|
|
95
161
|
});
|
|
96
162
|
}
|
|
97
163
|
|
|
@@ -140,12 +206,16 @@ class MemberWelcomeEmailService {
|
|
|
140
206
|
siteSettings: this.#getSiteSettings()
|
|
141
207
|
});
|
|
142
208
|
|
|
209
|
+
// Test sends should always reflect the latest newsletter sender settings.
|
|
210
|
+
const senderOptions = await this.#getDefaultNewsletterSenderOptions();
|
|
211
|
+
|
|
143
212
|
await this.#mailer.send({
|
|
144
213
|
to: email,
|
|
145
214
|
subject: `[Test] ${renderedSubject}`,
|
|
146
215
|
html,
|
|
147
216
|
text,
|
|
148
|
-
forceTextContent: true
|
|
217
|
+
forceTextContent: true,
|
|
218
|
+
...senderOptions
|
|
149
219
|
});
|
|
150
220
|
}
|
|
151
221
|
}
|
|
@@ -160,4 +230,3 @@ class MemberWelcomeEmailServiceWrapper {
|
|
|
160
230
|
}
|
|
161
231
|
|
|
162
232
|
module.exports = new MemberWelcomeEmailServiceWrapper();
|
|
163
|
-
|
|
@@ -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.',
|
|
@@ -392,6 +394,12 @@ module.exports = class RouterController {
|
|
|
392
394
|
});
|
|
393
395
|
}
|
|
394
396
|
|
|
397
|
+
if (!offer.tier) {
|
|
398
|
+
throw new BadRequestError({
|
|
399
|
+
message: 'Offer does not have a tier'
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
395
403
|
tier = await this._tiersService.api.read(offer.tier.id);
|
|
396
404
|
cadence = offer.cadence;
|
|
397
405
|
} else if (tierId) {
|
|
@@ -469,6 +477,8 @@ module.exports = class RouterController {
|
|
|
469
477
|
}
|
|
470
478
|
|
|
471
479
|
const member = options.member;
|
|
480
|
+
/** @type {SignupContext} */
|
|
481
|
+
let ghostSignupContext = (options.isAuthenticated && member) ? SIGNUP_CONTEXTS.ALREADY_AUTHENTICATED : SIGNUP_CONTEXTS.NEEDS_MAGIC_LINK_EMAIL;
|
|
472
482
|
|
|
473
483
|
if (!member && options.email) {
|
|
474
484
|
// Create a signup link if there is no member with this email address
|
|
@@ -485,6 +495,7 @@ module.exports = class RouterController {
|
|
|
485
495
|
// Redirect to the original success url after sign up
|
|
486
496
|
referrer: options.successUrl
|
|
487
497
|
});
|
|
498
|
+
ghostSignupContext = SIGNUP_CONTEXTS.HAS_PRECHECKOUT_MAGIC_LINK;
|
|
488
499
|
}
|
|
489
500
|
|
|
490
501
|
if (member) {
|
|
@@ -509,6 +520,9 @@ module.exports = class RouterController {
|
|
|
509
520
|
}
|
|
510
521
|
}
|
|
511
522
|
|
|
523
|
+
// Set by server to distinguish between checkout flows in Stripe webhooks.
|
|
524
|
+
options.metadata.ghostSignupContext = ghostSignupContext;
|
|
525
|
+
|
|
512
526
|
try {
|
|
513
527
|
const paymentLink = await this._paymentsService.getPaymentLink(options);
|
|
514
528
|
|
|
@@ -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'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ghost",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.18.1",
|
|
4
4
|
"description": "The professional publishing platform",
|
|
5
5
|
"author": "Ghost Foundation",
|
|
6
6
|
"homepage": "https://ghost.org",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"@tryghost/helpers": "1.1.97",
|
|
84
84
|
"@tryghost/html-to-plaintext": "1.0.4",
|
|
85
85
|
"@tryghost/http-cache-utils": "0.1.20",
|
|
86
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.
|
|
86
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
|
|
87
87
|
"@tryghost/image-transform": "1.4.6",
|
|
88
88
|
"@tryghost/job-manager": "1.0.3",
|
|
89
89
|
"@tryghost/kg-card-factory": "5.1.7",
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"@tryghost/mw-vhost": "1.0.1",
|
|
106
106
|
"@tryghost/nodemailer": "0.3.48",
|
|
107
107
|
"@tryghost/nql": "0.12.8",
|
|
108
|
-
"@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.
|
|
108
|
+
"@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz",
|
|
109
109
|
"@tryghost/pretty-cli": "1.2.47",
|
|
110
110
|
"@tryghost/prometheus-metrics": "1.0.2",
|
|
111
111
|
"@tryghost/promise": "0.3.15",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"intl-messageformat": "5.4.3",
|
|
172
172
|
"js-yaml": "4.1.0",
|
|
173
173
|
"jsonc-parser": "3.3.1",
|
|
174
|
-
"jsonpath": "1.
|
|
174
|
+
"jsonpath": "1.2.1",
|
|
175
175
|
"jsonwebtoken": "8.5.1",
|
|
176
176
|
"juice": "9.1.0",
|
|
177
177
|
"keypair": "1.0.4",
|
|
@@ -271,8 +271,8 @@
|
|
|
271
271
|
"jackspeak": "2.3.6",
|
|
272
272
|
"moment": "2.24.0",
|
|
273
273
|
"moment-timezone": "0.5.45",
|
|
274
|
-
"@tryghost/i18n": "file:components/tryghost-i18n-6.
|
|
275
|
-
"@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.
|
|
274
|
+
"@tryghost/i18n": "file:components/tryghost-i18n-6.18.1.tgz",
|
|
275
|
+
"@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.18.1.tgz"
|
|
276
276
|
},
|
|
277
277
|
"nx": {
|
|
278
278
|
"targets": {
|