ghost 6.21.0 → 6.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/components/tryghost-i18n-6.21.2.tgz +0 -0
  2. package/components/{tryghost-parse-email-address-6.21.0.tgz → tryghost-parse-email-address-6.21.2.tgz} +0 -0
  3. package/content/themes/casper/LICENSE +1 -1
  4. package/content/themes/casper/README.md +7 -2
  5. package/content/themes/casper/assets/built/casper.js +1 -2
  6. package/content/themes/casper/assets/built/casper.js.map +1 -1
  7. package/content/themes/casper/assets/built/global.css +1 -2
  8. package/content/themes/casper/assets/built/global.css.map +1 -1
  9. package/content/themes/casper/assets/built/screen.css +1 -2
  10. package/content/themes/casper/assets/built/screen.css.map +1 -1
  11. package/content/themes/casper/assets/css/global.css +4 -0
  12. package/content/themes/casper/default.hbs +5 -5
  13. package/content/themes/casper/error-404.hbs +1 -1
  14. package/content/themes/casper/error.hbs +2 -2
  15. package/content/themes/casper/gulpfile.js +13 -2
  16. package/content/themes/casper/locales/context.json +112 -0
  17. package/content/themes/casper/locales/de-CH.json +112 -0
  18. package/content/themes/casper/locales/de.json +112 -0
  19. package/content/themes/casper/locales/en.json +112 -0
  20. package/content/themes/casper/locales/fr.json +112 -0
  21. package/content/themes/casper/locales/ga.json +112 -0
  22. package/content/themes/casper/locales/gd.json +112 -0
  23. package/content/themes/casper/locales/nl.json +112 -0
  24. package/content/themes/casper/locales/pt-BR.json +112 -0
  25. package/content/themes/casper/locales/sv.json +112 -0
  26. package/content/themes/casper/locales/uk.json +112 -0
  27. package/content/themes/casper/locales/zh.json +112 -0
  28. package/content/themes/casper/package.json +12 -11
  29. package/content/themes/casper/partials/post-card.hbs +4 -4
  30. package/content/themes/casper/post.hbs +4 -4
  31. package/content/themes/casper/tag.hbs +1 -1
  32. package/content/themes/source/LICENSE +1 -1
  33. package/content/themes/source/README.md +4 -1
  34. package/content/themes/source/assets/built/screen.css +1 -2
  35. package/content/themes/source/assets/built/screen.css.map +1 -1
  36. package/content/themes/source/assets/built/source.js +1 -2
  37. package/content/themes/source/assets/built/source.js.map +1 -1
  38. package/content/themes/source/assets/css/screen.css +18 -16
  39. package/content/themes/source/default.hbs +1 -1
  40. package/content/themes/source/gulpfile.js +5 -3
  41. package/content/themes/source/locales/context.json +112 -0
  42. package/content/themes/source/locales/de-CH.json +112 -0
  43. package/content/themes/source/locales/de.json +112 -0
  44. package/content/themes/source/locales/en.json +112 -0
  45. package/content/themes/source/locales/fr.json +112 -0
  46. package/content/themes/source/locales/ga.json +112 -0
  47. package/content/themes/source/locales/gd.json +112 -0
  48. package/content/themes/source/locales/nl.json +112 -0
  49. package/content/themes/source/locales/pt-BR.json +112 -0
  50. package/content/themes/source/locales/sv.json +112 -0
  51. package/content/themes/source/locales/uk.json +112 -0
  52. package/content/themes/source/locales/zh.json +112 -0
  53. package/content/themes/source/package.json +13 -11
  54. package/content/themes/source/partials/components/featured.hbs +1 -1
  55. package/content/themes/source/partials/components/footer.hbs +1 -1
  56. package/content/themes/source/partials/components/header-content.hbs +1 -1
  57. package/content/themes/source/partials/components/navigation.hbs +5 -5
  58. package/content/themes/source/partials/components/post-list.hbs +7 -7
  59. package/content/themes/source/partials/email-subscription.hbs +5 -5
  60. package/content/themes/source/partials/lightbox.hbs +6 -6
  61. package/content/themes/source/partials/post-card.hbs +1 -1
  62. package/content/themes/source/partials/search-toggle.hbs +1 -1
  63. package/content/themes/source/post.hbs +2 -2
  64. package/core/built/admin/assets/{PolarAngleAxis-a31TecIQ.js → PolarAngleAxis-D8t9UwHk.js} +1 -1
  65. package/core/built/admin/assets/{_baseAssignValue-Dw5pcaUJ.js → _baseAssignValue-DPyNU5o6.js} +1 -1
  66. package/core/built/admin/assets/{a-large-small-Cf_hUupE.js → a-large-small-BijOKCoa.js} +1 -1
  67. package/core/built/admin/assets/admin-x-settings/admin-x-settings.js +1 -1
  68. package/core/built/admin/assets/admin-x-settings/{code-editor-view-C1ZFtR6M.mjs → code-editor-view-DJsXOcZ2.mjs} +2 -2
  69. package/core/built/admin/assets/admin-x-settings/{index-zyVbQw03.mjs → index-BnhIi5NU.mjs} +2 -2
  70. package/core/built/admin/assets/admin-x-settings/{index-D96qFjP9.mjs → index-CnwJhGbs.mjs} +2 -2
  71. package/core/built/admin/assets/admin-x-settings/{index-ILVAmH8Q.mjs → index-DaGaokMn.mjs} +1067 -1063
  72. package/core/built/admin/assets/admin-x-settings/{modals-BbDVa22r.mjs → modals-D02VJnTV.mjs} +2838 -2840
  73. package/core/built/admin/assets/{at-sign-DyMz5le1.js → at-sign-_JfhlimD.js} +1 -1
  74. package/core/built/admin/assets/{audience-BevM-Udw.js → audience-BdrhpNZ4.js} +1 -1
  75. package/core/built/admin/assets/{avatar-flipboard-DiGYhH_s.js → avatar-flipboard-BdmM8eOn.js} +1 -1
  76. package/core/built/admin/assets/{bluesky-sharing-Dj4VOEDi.js → bluesky-sharing-yeM8KkY8.js} +1 -1
  77. package/core/built/admin/assets/{chart-BDdnkPgC.js → chart-CULaDoVB.js} +1 -1
  78. package/core/built/admin/assets/{chunk.138.5c4b0d0ae3f21c9c726d.js → chunk.482.c45c97fe666cd1a6b060.js} +4 -4
  79. package/core/built/admin/assets/{chunk.524.54ba5b50c191627c46f2.js → chunk.524.7fc2692374515780e66e.js} +4 -4
  80. package/core/built/admin/assets/{chunk.582.3232d44081d84d135e3d.js → chunk.582.fd8c4fa8e637e2d5611e.js} +8 -8
  81. package/core/built/admin/assets/{code-editor-view-V0wBeYuE.js → code-editor-view-UA2xKjk1.js} +1 -1
  82. package/core/built/admin/assets/{comments-CqurDRGu.js → comments-DoNjlMYA.js} +1 -1
  83. package/core/built/admin/assets/{content-helpers-C8lVEp2H.js → content-helpers-BRzxAgjP.js} +1 -1
  84. package/core/built/admin/assets/{copy-BdseBMKx.js → copy-CbX-_mHj.js} +1 -1
  85. package/core/built/admin/assets/{data-list--5Xfh5gm.js → data-list-nZOiCWV9.js} +1 -1
  86. package/core/built/admin/assets/{deleted-feed-item-Bmxp6PAR.js → deleted-feed-item-DjkZpKDC.js} +1 -1
  87. package/core/built/admin/assets/{edit-profile-Bi-G8vrq.js → edit-profile-BY_oJrQn.js} +1 -1
  88. package/core/built/admin/assets/{empty-indicator-C1cLHSRX.js → empty-indicator-DoxAratw.js} +1 -1
  89. package/core/built/admin/assets/{en-DPpVO1wK.js → en-B9ScC5j9.js} +1 -1
  90. package/core/built/admin/assets/{feed-lCu_EJ0a.js → feed-BsgqABLQ.js} +1 -1
  91. package/core/built/admin/assets/{filters-BYYV0fmK.js → filters-CscMb_Gl.js} +1 -1
  92. package/core/built/admin/assets/{gh-chart-BYWaeEuY.js → gh-chart-GokHrG5_.js} +1 -1
  93. package/core/built/admin/assets/{ghost-565670d46ad5b30935426b5dfa95bc81.js → ghost-d1843f84a9476ccbdf2a350848ac88aa.js} +2 -2
  94. package/core/built/admin/assets/{growth-DXHnB1EQ.js → growth-CKOWA1_V.js} +1 -1
  95. package/core/built/admin/assets/{hash-tZATAzZa.js → hash-twG7-iIB.js} +1 -1
  96. package/core/built/admin/assets/{inbox-B-LuWyHY.js → inbox-BdkIJbSq.js} +1 -1
  97. package/core/built/admin/assets/{index-BDrJ3N3y.js → index-B7OzWwwP.js} +1 -1
  98. package/core/built/admin/assets/{index-B8tGZsrG.js → index-BFySh9qo.js} +1 -1
  99. package/core/built/admin/assets/{index-EiGE3Aiy.js → index-BOWUajrT.js} +1 -1
  100. package/core/built/admin/assets/{index-ig4J4RpO.js → index-BQLCImHQ.js} +1 -1
  101. package/core/built/admin/assets/{index-BmKPrbBK.js → index-C398bG84.js} +1 -1
  102. package/core/built/admin/assets/{index-BX0_v5NB.js → index-C5YCjM6z.js} +1 -1
  103. package/core/built/admin/assets/{index-DEwhVcR5.js → index-CHpxzPRR.js} +1 -1
  104. package/core/built/admin/assets/{index-CX1A5TwO.js → index-CM-iA6b-.js} +3 -3
  105. package/core/built/admin/assets/{index-9bj69FKq.js → index-CgCHCdle.js} +1 -1
  106. package/core/built/admin/assets/{index-BocVu6WC.js → index-Cw441AFq.js} +1 -1
  107. package/core/built/admin/assets/{index-jHIJgh6L.js → index-D9S88kRl.js} +1 -1
  108. package/core/built/admin/assets/{index-CqvBC0jL.js → index-DbgA2vK8.js} +1 -1
  109. package/core/built/admin/assets/{index-2H-Jp89Z.js → index-ghPdV1ln.js} +1 -1
  110. package/core/built/admin/assets/{index-BcOzgmTC.js → index-tX0pajfP.js} +1 -1
  111. package/core/built/admin/assets/{index-BxVUX0dN.js → index-upXLfQxx.js} +1 -1
  112. package/core/built/admin/assets/{koenig-lexical-BdX9U-oB.js → koenig-lexical-GZS7bHDz.js} +1 -1
  113. package/core/built/admin/assets/{kpi-card-DQ1AbLJB.js → kpi-card-B6fmS7IH.js} +1 -1
  114. package/core/built/admin/assets/{kpis-vcbQvnLh.js → kpis-B-JXQ-4C.js} +1 -1
  115. package/core/built/admin/assets/{label-CNmRJkLy.js → label-ChHoKmj0.js} +1 -1
  116. package/core/built/admin/assets/{links-BE51SJxR.js → links-DQSOljcR.js} +1 -1
  117. package/core/built/admin/assets/{list-filter-BwnQ0Xlp.js → list-filter-cWuCDOLb.js} +1 -1
  118. package/core/built/admin/assets/{loader-circle-CcLEGvsN.js → loader-circle-B5ILzHHu.js} +1 -1
  119. package/core/built/admin/assets/{lucide-react-DHFgHS1l.js → lucide-react-CM0rB7CS.js} +1 -1
  120. package/core/built/admin/assets/{main-layout-DsxBNVo-.js → main-layout-BE7vZmy0.js} +1 -1
  121. package/core/built/admin/assets/{members-3okEkvWv.js → members-C10Z_BuG.js} +1 -1
  122. package/core/built/admin/assets/{message-square-text-BkERj4f5.js → message-square-text-DVZjnBBF.js} +1 -1
  123. package/core/built/admin/assets/{minus-C6oMPJo2.js → minus-BWrVDnd-.js} +1 -1
  124. package/core/built/admin/assets/{modals-CfUec0CL.js → modals-Bx7uDUHI.js} +15 -15
  125. package/core/built/admin/assets/{moderation-DEHUqi1e.js → moderation-DwlKZoB5.js} +1 -1
  126. package/core/built/admin/assets/{newsletter-Cp-39ksw.js → newsletter-DkOl42UB.js} +1 -1
  127. package/core/built/admin/assets/{newsletters-Bj58VQa2.js → newsletters-DNt9qd4h.js} +1 -1
  128. package/core/built/admin/assets/{note-9ysQWJz6.js → note-C-BLqULD.js} +1 -1
  129. package/core/built/admin/assets/{offers-DORdeAfE.js → offers-CU0KFuyW.js} +1 -1
  130. package/core/built/admin/assets/{overview-aZmI_71o.js → overview-DAEelSKq.js} +1 -1
  131. package/core/built/admin/assets/{pagemenu-DK1tbdsq.js → pagemenu-Cr7_YYR8.js} +1 -1
  132. package/core/built/admin/assets/{post-analytics-context-D8nk51yA.js → post-analytics-context-B2ytyOU0.js} +1 -1
  133. package/core/built/admin/assets/{post-analytics-header-qh1LEasb.js → post-analytics-header-CvR25xxU.js} +1 -1
  134. package/core/built/admin/assets/{post-analytics-HdZI-1J7.js → post-analytics-qpfoP-57.js} +1 -1
  135. package/core/built/admin/assets/{post-share-modal-CvTf3PP9.js → post-share-modal-DDhiG0X4.js} +1 -1
  136. package/core/built/admin/assets/{posts-mIxckqHa.js → posts-DTy4lguT.js} +1 -1
  137. package/core/built/admin/assets/{referrers-qhD64pmp.js → referrers-BzKz9fT9.js} +1 -1
  138. package/core/built/admin/assets/{repeat-Idtsb62O.js → repeat-C_xWSLK1.js} +1 -1
  139. package/core/built/admin/assets/{reply-DXS-C-Rk.js → reply-CXnb6Juu.js} +1 -1
  140. package/core/built/admin/assets/{select-CeD8IhXy.js → select-WZUCQgjI.js} +1 -1
  141. package/core/built/admin/assets/{settings-BHfStwmS.js → settings-BC82mM6N.js} +1 -1
  142. package/core/built/admin/assets/{settings-M0QTt_tT.js → settings-EbntXZw9.js} +7 -7
  143. package/core/built/admin/assets/{sort-button-BoTFsBYE.js → sort-button-CFYf_NOQ.js} +1 -1
  144. package/core/built/admin/assets/{source-icon-opy8g_Nk.js → source-icon-xS8eeWX-.js} +1 -1
  145. package/core/built/admin/assets/{sprout-BKOFkdV8.js → sprout-CLmu2pln.js} +1 -1
  146. package/core/built/admin/assets/{square-BjZk7vJx.js → square-D0Y5-h0C.js} +1 -1
  147. package/core/built/admin/assets/{stats-DXXxmfRV.js → stats-B5O9DjYi.js} +1 -1
  148. package/core/built/admin/assets/{stats-view-D_5J0Nf7.js → stats-view-Cr6M1ycv.js} +1 -1
  149. package/core/built/admin/assets/{step-1-BcaR_MDC.js → step-1-DGbl6AAj.js} +1 -1
  150. package/core/built/admin/assets/{step-2-BR0jqEO_.js → step-2-Rf8oissi.js} +1 -1
  151. package/core/built/admin/assets/{step-3-CMPME0ky.js → step-3-tMPmbeBc.js} +1 -1
  152. package/core/built/admin/assets/{table-CwbnKMYe.js → table-CXo8NKU2.js} +1 -1
  153. package/core/built/admin/assets/{tabs-hfT89L_O.js → tabs-BMk71Tf2.js} +1 -1
  154. package/core/built/admin/assets/{tags-BSwYlj_e.js → tags-CSl0BjsA.js} +1 -1
  155. package/core/built/admin/assets/{tags-C3b5AHTm.js → tags-CqHx8i6Q.js} +1 -1
  156. package/core/built/admin/assets/{ticket-BnxPoED1.js → ticket-BtYGyQvH.js} +1 -1
  157. package/core/built/admin/assets/{tiers-DHKKzaBK.js → tiers-CPwRaPSt.js} +1 -1
  158. package/core/built/admin/assets/{toggle-group-U-Oqm2AA.js → toggle-group-QFge4qQs.js} +1 -1
  159. package/core/built/admin/assets/{topic-filter-28veMlW4.js → topic-filter-DnYtZ5e5.js} +1 -1
  160. package/core/built/admin/assets/{trash-D6TuYuIs.js → trash-B4ncKmRu.js} +1 -1
  161. package/core/built/admin/assets/{use-growth-stats-COmCRCzY.js → use-growth-stats-eFO-f1o1.js} +1 -1
  162. package/core/built/admin/assets/{use-infinite-virtual-scroll-mIEgtLoq.js → use-infinite-virtual-scroll-DTqnBtHM.js} +1 -1
  163. package/core/built/admin/assets/{use-scroll-restoration-CXVseKDj.js → use-scroll-restoration-o1DrSdlb.js} +1 -1
  164. package/core/built/admin/assets/{use-simple-pagination-5-LKveLR.js → use-simple-pagination-BsBGNcxc.js} +1 -1
  165. package/core/built/admin/assets/{user-plus-CKQ-d0d4.js → user-plus-BS8SGBmk.js} +1 -1
  166. package/core/built/admin/assets/{user-round-check-BWc7Da4S.js → user-round-check-BgOPeBFs.js} +1 -1
  167. package/core/built/admin/assets/{wallet-cards-BlDtHc6_.js → wallet-cards-Be7G77lR.js} +1 -1
  168. package/core/built/admin/assets/{web-Bvvtl44p.js → web-BNs01IAt.js} +1 -1
  169. package/core/built/admin/index.html +5 -5
  170. package/core/frontend/helpers/cancel_link.js +2 -2
  171. package/core/server/api/endpoints/authors-public.js +3 -3
  172. package/core/server/api/endpoints/files.js +2 -1
  173. package/core/server/api/endpoints/pages-public.js +3 -3
  174. package/core/server/api/endpoints/posts-public.js +3 -3
  175. package/core/server/api/endpoints/users.js +6 -2
  176. package/core/server/api/endpoints/utils/api-filter-utils.js +22 -0
  177. package/core/server/api/endpoints/utils/api-filter-utils.ts +22 -0
  178. package/core/server/lib/lexical.js +2 -1
  179. package/core/server/lib/mobiledoc.js +1 -1
  180. package/core/server/lib/request-external.js +78 -2
  181. package/core/server/services/email-service/email-templates/partials/styles.hbs +3 -1
  182. package/core/server/services/koenig/node-renderers/image-renderer.js +80 -10
  183. package/core/server/services/koenig/render-utils/srcset-attribute.js +15 -3
  184. package/core/server/services/members/members-api/controllers/router-controller.js +5 -5
  185. package/core/server/services/members/members-api/repositories/member-repository.js +1 -1
  186. package/core/server/services/members/members-api/utils/get-discount-window.js +70 -4
  187. package/core/server/services/members/members-api/utils/has-active-offer.js +10 -14
  188. package/core/server/services/oembed/oembed-service.js +9 -1
  189. package/core/server/services/offers/application/offer-mapper.js +48 -0
  190. package/core/server/services/offers/application/offers-api.js +2 -2
  191. package/core/shared/config/defaults.json +1 -1
  192. package/core/shared/labs.js +2 -1
  193. package/package.json +6 -6
  194. package/tsconfig.tsbuildinfo +1 -1
  195. package/yarn.lock +148 -16
  196. package/components/tryghost-i18n-6.21.0.tgz +0 -0
  197. package/core/server/api/endpoints/utils/public-endpoint-utils.js +0 -18
  198. package/core/server/api/endpoints/utils/public-endpoint-utils.ts +0 -17
@@ -1,4 +1,5 @@
1
1
  const dns = require('node:dns/promises');
2
+ const crypto = require('node:crypto');
2
3
  const tpl = require('@tryghost/tpl');
3
4
  const logging = require('@tryghost/logging');
4
5
  const sanitizeHtml = require('sanitize-html');
@@ -26,7 +27,6 @@ const messages = {
26
27
  inviteOnly: 'This site is invite-only, contact the owner for access.',
27
28
  paidOnly: 'This site only accepts paid members.',
28
29
  memberNotFound: 'No member exists with this e-mail address.',
29
- memberNotFoundSignUp: 'No member exists with this e-mail address. Please sign up first.',
30
30
  invalidType: 'Invalid checkout type.',
31
31
  notConfigured: 'This site is not accepting payments at the moment.',
32
32
  invalidNewsletters: 'Cannot subscribe to invalid newsletters {newsletters}',
@@ -799,7 +799,7 @@ module.exports = class RouterController {
799
799
  if (!tokenValue) {
800
800
  throw new errors.BadRequestError({
801
801
  message: tpl(messages.invalidCode),
802
- code: 'INVALID_OTC_REF'
802
+ code: 'INVALID_OTC'
803
803
  });
804
804
  }
805
805
 
@@ -887,9 +887,9 @@ module.exports = class RouterController {
887
887
  const member = await this._memberRepository.get({email: normalizedEmail});
888
888
 
889
889
  if (!member) {
890
- throw new errors.BadRequestError({
891
- message: this._allowSelfSignup() ? tpl(messages.memberNotFoundSignUp) : tpl(messages.memberNotFound)
892
- });
890
+ // Return a fake otcRef when OTC was requested so the response
891
+ // shape is identical regardless of whether a member exists
892
+ return includeOTC ? {otcRef: crypto.randomUUID()} : {};
893
893
  }
894
894
 
895
895
  const tokenData = {};
@@ -1725,7 +1725,7 @@ module.exports = class MemberRepository {
1725
1725
  }
1726
1726
 
1727
1727
  // Check subscription doesn't already have an active offer
1728
- if (await hasActiveOffer(subscriptionModel, this._offersAPI)) {
1728
+ if (await hasActiveOffer(subscriptionModel, this._offersAPI, options)) {
1729
1729
  throw new errors.BadRequestError({
1730
1730
  message: tpl(messages.subscriptionHasOffer)
1731
1731
  });
@@ -1,3 +1,43 @@
1
+ function getLastDayOfMonth(year, month) {
2
+ return new Date(Date.UTC(year, month + 1, 0)).getUTCDate();
3
+ }
4
+
5
+ function isLastDayOfMonth(date) {
6
+ return date.getUTCDate() === getLastDayOfMonth(date.getUTCFullYear(), date.getUTCMonth());
7
+ }
8
+
9
+ function getAnchoredBillingDate(anchorDate, monthOffset) {
10
+ const targetMonthIndex = anchorDate.getUTCMonth() + monthOffset;
11
+ const targetYear = anchorDate.getUTCFullYear() + Math.floor(targetMonthIndex / 12);
12
+ const targetMonth = ((targetMonthIndex % 12) + 12) % 12;
13
+ const targetLastDay = getLastDayOfMonth(targetYear, targetMonth);
14
+ const targetDay = isLastDayOfMonth(anchorDate) ? targetLastDay : Math.min(anchorDate.getUTCDate(), targetLastDay);
15
+
16
+ return new Date(Date.UTC(
17
+ targetYear,
18
+ targetMonth,
19
+ targetDay,
20
+ anchorDate.getUTCHours(),
21
+ anchorDate.getUTCMinutes(),
22
+ anchorDate.getUTCSeconds(),
23
+ anchorDate.getUTCMilliseconds()
24
+ ));
25
+ }
26
+
27
+ function getLastDiscountedPayment(nextBillingDate, discountEnd) {
28
+ const monthOffset =
29
+ ((discountEnd.getUTCFullYear() - nextBillingDate.getUTCFullYear()) * 12) +
30
+ (discountEnd.getUTCMonth() - nextBillingDate.getUTCMonth());
31
+
32
+ let lastDiscountedBillingDate = getAnchoredBillingDate(nextBillingDate, monthOffset);
33
+
34
+ if (lastDiscountedBillingDate > discountEnd) {
35
+ lastDiscountedBillingDate = getAnchoredBillingDate(nextBillingDate, monthOffset - 1);
36
+ }
37
+
38
+ return lastDiscountedBillingDate;
39
+ }
40
+
1
41
  /**
2
42
  * Computes the discount window for a subscription based on available data.
3
43
  * Returns {start, end} if a discount window can be determined, null otherwise.
@@ -6,14 +46,35 @@
6
46
  * 1. Stripe coupon discounts (post-6.16) - uses discount_start / discount_end
7
47
  * 2. Legacy fallback - computes from offer duration and start_date
8
48
  *
9
- * @param {object} subscription - plain object with: discount_start, discount_end, start_date
49
+ * @param {object} subscription - plain object with: discount_start, discount_end, start_date, current_period_end, plan.interval, plan_interval
10
50
  * @param {object|null} offer - offer data with: duration, duration_in_months.
11
51
  * Pass null to skip offer-dependent checks.
12
52
  * @returns {{start: *, end: *}|null}
13
53
  */
54
+
14
55
  module.exports = function getDiscountWindow(subscription, offer) {
15
56
  // Stripe coupon discount (post-6.16 data)
16
57
  if (subscription.discount_start) {
58
+ if (subscription.discount_end && offer?.duration === 'repeating') {
59
+ const discountEnd = new Date(subscription.discount_end);
60
+ const currentPeriodEnd = new Date(subscription.current_period_end);
61
+
62
+ if (discountEnd <= new Date()) {
63
+ return null;
64
+ }
65
+
66
+ // A discount ending at, or before, the current billing period end won't affect the next payment
67
+ if (discountEnd <= currentPeriodEnd) {
68
+ return null;
69
+ }
70
+
71
+ // Match the end date with the last discounted payment
72
+ return {
73
+ start: subscription.discount_start,
74
+ end: getLastDiscountedPayment(currentPeriodEnd, discountEnd)
75
+ };
76
+ }
77
+
17
78
  return {
18
79
  start: subscription.discount_start,
19
80
  end: subscription.discount_end || null
@@ -34,10 +95,15 @@ module.exports = function getDiscountWindow(subscription, offer) {
34
95
  }
35
96
 
36
97
  if (offer.duration === 'repeating' && offer.duration_in_months > 0) {
37
- const end = new Date(subscription.start_date);
38
- end.setUTCMonth(end.getUTCMonth() + offer.duration_in_months);
98
+ const end = getAnchoredBillingDate(new Date(subscription.start_date), offer.duration_in_months - 1);
99
+ const currentPeriodEnd = new Date(subscription.current_period_end);
100
+
101
+ if (end <= new Date()) {
102
+ return null;
103
+ }
39
104
 
40
- if (new Date() >= end) {
105
+ // A discount ending before the end of the current billing period won't affect the next payment
106
+ if (end < currentPeriodEnd) {
41
107
  return null;
42
108
  }
43
109
 
@@ -1,28 +1,24 @@
1
1
  const getDiscountWindow = require('./get-discount-window');
2
2
 
3
3
  /**
4
- * Determines if a subscription currently has an active offer.
4
+ * Determines if a subscription has an offer that still affects the next payment,
5
+ * or an active trial.
5
6
  * Uses discount_start/discount_end (synced from Stripe) when available,
6
7
  * falls back to offer duration lookup for legacy data (pre-6.16).
7
8
  *
8
9
  * @param {object} subscriptionModel - Bookshelf model for members_stripe_customers_subscriptions
9
10
  * @param {object} offersAPI - OffersAPI instance with getOffer()
11
+ * @param {object} [options] - Optional query options such as transacting
10
12
  * @returns {Promise<boolean>}
11
13
  */
12
- module.exports = async function hasActiveOffer(subscriptionModel, offersAPI) {
14
+ module.exports = async function hasActiveOffer(subscriptionModel, offersAPI, options = {}) {
13
15
  const subscriptionData = {
14
16
  discount_start: subscriptionModel.get('discount_start'),
15
17
  discount_end: subscriptionModel.get('discount_end'),
16
- start_date: subscriptionModel.get('start_date')
18
+ start_date: subscriptionModel.get('start_date'),
19
+ current_period_end: subscriptionModel.get('current_period_end')
17
20
  };
18
21
 
19
- // Check for active Stripe discount (post-6.16 data)
20
- // discount_start takes precedence over trial and legacy fallback
21
- const discountWindow = getDiscountWindow(subscriptionData, null);
22
- if (discountWindow) {
23
- return !discountWindow.end || new Date(discountWindow.end) > new Date();
24
- }
25
-
26
22
  // Check for active trial (trial offers)
27
23
  const trialEndAt = subscriptionModel.get('trial_end_at');
28
24
  if (trialEndAt && new Date(trialEndAt) > new Date()) {
@@ -37,14 +33,14 @@ module.exports = async function hasActiveOffer(subscriptionModel, offersAPI) {
37
33
 
38
34
  // Look up the offer to determine if it's still active based on duration
39
35
  try {
40
- const offer = await offersAPI.getOffer({id: offerId});
36
+ const offer = await offersAPI.getOffer({id: offerId}, options);
41
37
  if (!offer) {
42
38
  return false;
43
39
  }
44
40
 
45
- const legacyWindow = getDiscountWindow(subscriptionData, offer);
46
- if (legacyWindow) {
47
- return !legacyWindow.end || new Date(legacyWindow.end) > new Date();
41
+ const discountWindow = getDiscountWindow(subscriptionData, offer);
42
+ if (discountWindow) {
43
+ return !discountWindow.end || new Date(discountWindow.end) > new Date();
48
44
  }
49
45
 
50
46
  return false;
@@ -262,7 +262,15 @@ class OEmbedService {
262
262
  * @param {string} url
263
263
  * @param {string} html
264
264
  *
265
- * @returns {Promise<Object>}
265
+ * @returns {Promise<{
266
+ * version: '1.0',
267
+ * type: 'bookmark',
268
+ * url: string,
269
+ * metadata: Omit<import('metascraper').Metadata, 'image'|'logo'> & {
270
+ * thumbnail?: string,
271
+ * icon?: string
272
+ * }
273
+ * }>}
266
274
  */
267
275
  async fetchBookmarkData(url, html, type) {
268
276
  const got = require('got');
@@ -33,6 +33,29 @@
33
33
  * @prop {string|null} last_redeemed
34
34
  */
35
35
 
36
+ /**
37
+ * @typedef {object} PublicOfferDTO
38
+ * @prop {string} id
39
+ *
40
+ * @prop {string} display_title
41
+ * @prop {string} display_description
42
+ *
43
+ * @prop {'percent'|'fixed'|'trial'} type
44
+ *
45
+ * @prop {'month'|'year'} cadence
46
+ * @prop {number} amount
47
+ *
48
+ * @prop {'once'|'repeating'|'forever'|'trial'} duration
49
+ * @prop {null|number} duration_in_months
50
+ *
51
+ * @prop {string|null} currency
52
+ *
53
+ * @prop {'active'|'archived'} status
54
+ * @prop {'signup'|'retention'} redemption_type
55
+ *
56
+ * @prop {{id: string}|null} tier
57
+ */
58
+
36
59
  class OfferMapper {
37
60
  /**
38
61
  * @param {Offer} offer
@@ -62,6 +85,31 @@ class OfferMapper {
62
85
  last_redeemed: offer.lastRedeemed
63
86
  };
64
87
  }
88
+
89
+ /**
90
+ * Returns a DTO for a public facing offer (e.g. Portal's retention offer UI)
91
+ *
92
+ * @param {Offer} offer
93
+ * @returns {PublicOfferDTO}
94
+ */
95
+ static toPublicDTO(offer) {
96
+ return {
97
+ id: offer.id,
98
+ display_title: offer.displayTitle.value,
99
+ display_description: offer.displayDescription.value,
100
+ type: offer.type.value,
101
+ cadence: offer.cadence.value,
102
+ amount: offer.amount.value,
103
+ duration: offer.duration.value.type,
104
+ duration_in_months: offer.duration.value.type === 'repeating' ? offer.duration.value.months : null,
105
+ currency: offer.type.value === 'fixed' ? offer.currency.value : null,
106
+ status: offer.status.value,
107
+ redemption_type: offer.redemptionType.value,
108
+ tier: offer.tier
109
+ ? {id: offer.tier.id}
110
+ : null
111
+ };
112
+ }
65
113
  }
66
114
 
67
115
  module.exports = OfferMapper;
@@ -180,7 +180,7 @@ class OffersAPI {
180
180
  * @param {string} options.tierId
181
181
  * @param {'month'|'year'} options.cadence
182
182
  * @param {'signup'|'retention'} [options.redemptionType]
183
- * @returns {Promise<OfferMapper.OfferDTO[]>}
183
+ * @returns {Promise<OfferMapper.PublicOfferDTO[]>}
184
184
  */
185
185
  async listOffersAvailableToSubscription({subscriptionId, tierId, cadence, redemptionType}) {
186
186
  debug(`listOffersAvailableToSubscription: subscriptionId=${subscriptionId}, tierId=${tierId}, cadence=${cadence}, redemptionType=${redemptionType}`);
@@ -246,7 +246,7 @@ class OffersAPI {
246
246
  }
247
247
 
248
248
  debug(`listOffersAvailableToSubscription: returning ${available.length} available offers`);
249
- return available.map(OfferMapper.toDTO);
249
+ return available.map(OfferMapper.toPublicDTO);
250
250
  });
251
251
  }
252
252
 
@@ -240,7 +240,7 @@
240
240
  },
241
241
  "portal": {
242
242
  "url": "https://cdn.jsdelivr.net/ghost/portal@~{version}/umd/portal.min.js",
243
- "version": "2.65"
243
+ "version": "2.66"
244
244
  },
245
245
  "sodoSearch": {
246
246
  "url": "https://cdn.jsdelivr.net/ghost/sodo-search@~{version}/umd/sodo-search.min.js",
@@ -50,7 +50,8 @@ const PRIVATE_FEATURES = [
50
50
  'transistor',
51
51
  'retentionOffers',
52
52
  'membersForward',
53
- 'welcomeEmailsDesignCustomization'
53
+ 'welcomeEmailsDesignCustomization',
54
+ 'pictureImageFormats'
54
55
  ];
55
56
 
56
57
  module.exports.GA_KEYS = [...GA_FEATURES];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost",
3
- "version": "6.21.0",
3
+ "version": "6.21.2",
4
4
  "description": "The professional publishing platform",
5
5
  "author": "Ghost Foundation",
6
6
  "homepage": "https://ghost.org",
@@ -85,7 +85,7 @@
85
85
  "@tryghost/helpers": "1.1.97",
86
86
  "@tryghost/html-to-plaintext": "1.0.4",
87
87
  "@tryghost/http-cache-utils": "0.1.20",
88
- "@tryghost/i18n": "file:components/tryghost-i18n-6.21.0.tgz",
88
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.21.2.tgz",
89
89
  "@tryghost/image-transform": "1.4.6",
90
90
  "@tryghost/job-manager": "1.0.3",
91
91
  "@tryghost/kg-card-factory": "5.1.10",
@@ -107,7 +107,7 @@
107
107
  "@tryghost/mw-vhost": "1.0.1",
108
108
  "@tryghost/nodemailer": "0.3.48",
109
109
  "@tryghost/nql": "0.12.10",
110
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.21.0.tgz",
110
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.21.2.tgz",
111
111
  "@tryghost/pretty-cli": "1.2.47",
112
112
  "@tryghost/prometheus-metrics": "1.0.2",
113
113
  "@tryghost/promise": "0.3.15",
@@ -195,7 +195,7 @@
195
195
  "moment": "2.24.0",
196
196
  "moment-timezone": "0.5.45",
197
197
  "multer": "2.0.2",
198
- "mysql2": "3.18.0",
198
+ "mysql2": "3.18.1",
199
199
  "nconf": "0.13.0",
200
200
  "node-fetch": "2.7.0",
201
201
  "node-jose": "2.2.0",
@@ -272,8 +272,8 @@
272
272
  "jackspeak": "2.3.6",
273
273
  "moment": "2.24.0",
274
274
  "moment-timezone": "0.5.45",
275
- "@tryghost/i18n": "file:components/tryghost-i18n-6.21.0.tgz",
276
- "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.21.0.tgz"
275
+ "@tryghost/i18n": "file:components/tryghost-i18n-6.21.2.tgz",
276
+ "@tryghost/parse-email-address": "file:components/tryghost-parse-email-address-6.21.2.tgz"
277
277
  },
278
278
  "nx": {
279
279
  "targets": {