@automattic/plans-grid-next 1.0.2 → 1.0.3

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 (298) hide show
  1. package/CHANGELOG.md +4 -1
  2. package/dist/cjs/_shared.scss +4 -3
  3. package/dist/cjs/components/comparison-grid/index.js +99 -92
  4. package/dist/cjs/components/comparison-grid/index.js.map +1 -1
  5. package/dist/cjs/components/comparison-grid/style.scss +10 -2
  6. package/dist/cjs/components/features-grid/client-logo-list/client-list.js +0 -12
  7. package/dist/cjs/components/features-grid/client-logo-list/client-list.js.map +1 -1
  8. package/dist/cjs/components/features-grid/index.js +9 -6
  9. package/dist/cjs/components/features-grid/index.js.map +1 -1
  10. package/dist/cjs/components/features-grid/plan-features-list.js +10 -3
  11. package/dist/cjs/components/features-grid/plan-features-list.js.map +1 -1
  12. package/dist/cjs/components/features-grid/plan-headers.js +2 -2
  13. package/dist/cjs/components/features-grid/plan-headers.js.map +1 -1
  14. package/dist/cjs/components/features-grid/plan-tagline.js +1 -1
  15. package/dist/cjs/components/features-grid/plan-tagline.js.map +1 -1
  16. package/dist/cjs/components/features-grid/style.scss +107 -19
  17. package/dist/cjs/components/features-grid/table.js +1 -1
  18. package/dist/cjs/components/features-grid/table.js.map +1 -1
  19. package/dist/cjs/components/features.js +43 -4
  20. package/dist/cjs/components/features.js.map +1 -1
  21. package/dist/cjs/components/item.js +1 -1
  22. package/dist/cjs/components/item.js.map +1 -1
  23. package/dist/cjs/components/plan-button/index.js +5 -3
  24. package/dist/cjs/components/plan-button/index.js.map +1 -1
  25. package/dist/cjs/components/plan-button/style.scss +75 -51
  26. package/dist/cjs/components/plan-div-td-container.js +4 -1
  27. package/dist/cjs/components/plan-div-td-container.js.map +1 -1
  28. package/dist/cjs/components/plan-logo.js +6 -3
  29. package/dist/cjs/components/plan-logo.js.map +1 -1
  30. package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
  31. package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
  32. package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js +4 -33
  33. package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
  34. package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
  35. package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
  36. package/dist/cjs/components/plans-2023-tooltip.js +16 -5
  37. package/dist/cjs/components/plans-2023-tooltip.js.map +1 -1
  38. package/dist/cjs/components/shared/action-button/index.js +22 -7
  39. package/dist/cjs/components/shared/action-button/index.js.map +1 -1
  40. package/dist/cjs/components/shared/action-button/style.scss +4 -0
  41. package/dist/cjs/components/shared/billing-timeframe/index.js +8 -4
  42. package/dist/cjs/components/shared/billing-timeframe/index.js.map +1 -1
  43. package/dist/cjs/components/shared/header-price/index.js +60 -15
  44. package/dist/cjs/components/shared/header-price/index.js.map +1 -1
  45. package/dist/cjs/components/shared/header-price/style.scss +9 -3
  46. package/dist/cjs/components/shared/storage/components/plan-storage.js +2 -2
  47. package/dist/cjs/components/shared/storage/components/plan-storage.js.map +1 -1
  48. package/dist/cjs/components/shared/storage/components/storage-dropdown.js +29 -6
  49. package/dist/cjs/components/shared/storage/components/storage-dropdown.js.map +1 -1
  50. package/dist/cjs/components/shared/storage/components/storage-feature-label.js +2 -1
  51. package/dist/cjs/components/shared/storage/components/storage-feature-label.js.map +1 -1
  52. package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js +2 -0
  53. package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
  54. package/dist/cjs/fixtures/sites-purchases.js +2 -4
  55. package/dist/cjs/fixtures/sites-purchases.js.map +1 -1
  56. package/dist/cjs/grid-context.js +4 -1
  57. package/dist/cjs/grid-context.js.map +1 -1
  58. package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js +50 -0
  59. package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
  60. package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
  61. package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
  62. package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
  63. package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
  64. package/dist/cjs/hooks/data-store/use-grid-plans.js +175 -21
  65. package/dist/cjs/hooks/data-store/use-grid-plans.js.map +1 -1
  66. package/dist/cjs/hooks/data-store/use-highlight-labels.js +13 -4
  67. package/dist/cjs/hooks/data-store/use-highlight-labels.js.map +1 -1
  68. package/dist/cjs/hooks/data-store/use-plan-billing-description.js +68 -13
  69. package/dist/cjs/hooks/data-store/use-plan-billing-description.js.map +1 -1
  70. package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js +76 -2
  71. package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
  72. package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +60 -12
  73. package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
  74. package/dist/cjs/hooks/data-store/use-title-badges.js +19 -0
  75. package/dist/cjs/hooks/data-store/use-title-badges.js.map +1 -0
  76. package/dist/cjs/hooks/use-is-large-currency.js +2 -2
  77. package/dist/cjs/hooks/use-is-large-currency.js.map +1 -1
  78. package/dist/cjs/hooks/use-visible-grid-plans.js +70 -0
  79. package/dist/cjs/hooks/use-visible-grid-plans.js.map +1 -0
  80. package/dist/cjs/index.js +6 -1
  81. package/dist/cjs/index.js.map +1 -1
  82. package/dist/cjs/lib/get-plan-features-object.js +15 -2
  83. package/dist/cjs/lib/get-plan-features-object.js.map +1 -1
  84. package/dist/cjs/lib/plan-pricing-utils.js +135 -0
  85. package/dist/cjs/lib/plan-pricing-utils.js.map +1 -0
  86. package/dist/esm/_shared.scss +4 -3
  87. package/dist/esm/components/comparison-grid/index.js +100 -93
  88. package/dist/esm/components/comparison-grid/index.js.map +1 -1
  89. package/dist/esm/components/comparison-grid/style.scss +10 -2
  90. package/dist/esm/components/features-grid/client-logo-list/client-list.js +0 -12
  91. package/dist/esm/components/features-grid/client-logo-list/client-list.js.map +1 -1
  92. package/dist/esm/components/features-grid/index.js +9 -6
  93. package/dist/esm/components/features-grid/index.js.map +1 -1
  94. package/dist/esm/components/features-grid/plan-features-list.js +10 -3
  95. package/dist/esm/components/features-grid/plan-features-list.js.map +1 -1
  96. package/dist/esm/components/features-grid/plan-headers.js +3 -3
  97. package/dist/esm/components/features-grid/plan-headers.js.map +1 -1
  98. package/dist/esm/components/features-grid/plan-tagline.js +1 -1
  99. package/dist/esm/components/features-grid/plan-tagline.js.map +1 -1
  100. package/dist/esm/components/features-grid/style.scss +107 -19
  101. package/dist/esm/components/features-grid/table.js +1 -1
  102. package/dist/esm/components/features-grid/table.js.map +1 -1
  103. package/dist/esm/components/features.js +44 -5
  104. package/dist/esm/components/features.js.map +1 -1
  105. package/dist/esm/components/item.js +1 -1
  106. package/dist/esm/components/item.js.map +1 -1
  107. package/dist/esm/components/plan-button/index.js +5 -3
  108. package/dist/esm/components/plan-button/index.js.map +1 -1
  109. package/dist/esm/components/plan-button/style.scss +75 -51
  110. package/dist/esm/components/plan-div-td-container.js +4 -1
  111. package/dist/esm/components/plan-div-td-container.js.map +1 -1
  112. package/dist/esm/components/plan-logo.js +7 -4
  113. package/dist/esm/components/plan-logo.js.map +1 -1
  114. package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
  115. package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
  116. package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js +3 -33
  117. package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
  118. package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
  119. package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
  120. package/dist/esm/components/plans-2023-tooltip.js +16 -5
  121. package/dist/esm/components/plans-2023-tooltip.js.map +1 -1
  122. package/dist/esm/components/shared/action-button/index.js +22 -7
  123. package/dist/esm/components/shared/action-button/index.js.map +1 -1
  124. package/dist/esm/components/shared/action-button/style.scss +4 -0
  125. package/dist/esm/components/shared/billing-timeframe/index.js +8 -4
  126. package/dist/esm/components/shared/billing-timeframe/index.js.map +1 -1
  127. package/dist/esm/components/shared/header-price/index.js +60 -15
  128. package/dist/esm/components/shared/header-price/index.js.map +1 -1
  129. package/dist/esm/components/shared/header-price/style.scss +9 -3
  130. package/dist/esm/components/shared/storage/components/plan-storage.js +2 -2
  131. package/dist/esm/components/shared/storage/components/plan-storage.js.map +1 -1
  132. package/dist/esm/components/shared/storage/components/storage-dropdown.js +30 -7
  133. package/dist/esm/components/shared/storage/components/storage-dropdown.js.map +1 -1
  134. package/dist/esm/components/shared/storage/components/storage-feature-label.js +2 -1
  135. package/dist/esm/components/shared/storage/components/storage-feature-label.js.map +1 -1
  136. package/dist/esm/components/shared/storage/hooks/use-plan-storage.js +3 -1
  137. package/dist/esm/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
  138. package/dist/esm/fixtures/sites-purchases.js +2 -4
  139. package/dist/esm/fixtures/sites-purchases.js.map +1 -1
  140. package/dist/esm/grid-context.js +4 -1
  141. package/dist/esm/grid-context.js.map +1 -1
  142. package/dist/esm/hooks/data-store/get-renewal-pricing-text.js +47 -0
  143. package/dist/esm/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
  144. package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
  145. package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
  146. package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
  147. package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
  148. package/dist/esm/hooks/data-store/use-grid-plans.js +176 -22
  149. package/dist/esm/hooks/data-store/use-grid-plans.js.map +1 -1
  150. package/dist/esm/hooks/data-store/use-highlight-labels.js +14 -5
  151. package/dist/esm/hooks/data-store/use-highlight-labels.js.map +1 -1
  152. package/dist/esm/hooks/data-store/use-plan-billing-description.js +66 -11
  153. package/dist/esm/hooks/data-store/use-plan-billing-description.js.map +1 -1
  154. package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js +77 -3
  155. package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
  156. package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +59 -11
  157. package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
  158. package/dist/esm/hooks/data-store/use-title-badges.js +17 -0
  159. package/dist/esm/hooks/data-store/use-title-badges.js.map +1 -0
  160. package/dist/esm/hooks/use-is-large-currency.js +1 -1
  161. package/dist/esm/hooks/use-is-large-currency.js.map +1 -1
  162. package/dist/esm/hooks/use-visible-grid-plans.js +66 -0
  163. package/dist/esm/hooks/use-visible-grid-plans.js.map +1 -0
  164. package/dist/esm/index.js +2 -0
  165. package/dist/esm/index.js.map +1 -1
  166. package/dist/esm/lib/get-plan-features-object.js +15 -2
  167. package/dist/esm/lib/get-plan-features-object.js.map +1 -1
  168. package/dist/esm/lib/plan-pricing-utils.js +129 -0
  169. package/dist/esm/lib/plan-pricing-utils.js.map +1 -0
  170. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  171. package/dist/tsconfig.tsbuildinfo +1 -1
  172. package/dist/types/components/comparison-grid/index.d.ts +1 -1
  173. package/dist/types/components/comparison-grid/index.d.ts.map +1 -1
  174. package/dist/types/components/features-grid/client-logo-list/client-list.d.ts.map +1 -1
  175. package/dist/types/components/features-grid/index.d.ts.map +1 -1
  176. package/dist/types/components/features-grid/plan-features-list.d.ts.map +1 -1
  177. package/dist/types/components/features-grid/plan-headers.d.ts +2 -0
  178. package/dist/types/components/features-grid/plan-headers.d.ts.map +1 -1
  179. package/dist/types/components/features-grid/table.d.ts.map +1 -1
  180. package/dist/types/components/features.d.ts.map +1 -1
  181. package/dist/types/components/item.d.ts +2 -1
  182. package/dist/types/components/item.d.ts.map +1 -1
  183. package/dist/types/components/plan-button/index.d.ts +2 -1
  184. package/dist/types/components/plan-button/index.d.ts.map +1 -1
  185. package/dist/types/components/plan-div-td-container.d.ts +2 -0
  186. package/dist/types/components/plan-div-td-container.d.ts.map +1 -1
  187. package/dist/types/components/plan-logo.d.ts.map +1 -1
  188. package/dist/types/components/plan-type-selector/components/interval-type-dropdown.d.ts.map +1 -1
  189. package/dist/types/components/plan-type-selector/hooks/use-max-discount.d.ts.map +1 -1
  190. package/dist/types/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.d.ts.map +1 -1
  191. package/dist/types/components/plans-2023-tooltip.d.ts.map +1 -1
  192. package/dist/types/components/shared/action-button/index.d.ts +2 -1
  193. package/dist/types/components/shared/action-button/index.d.ts.map +1 -1
  194. package/dist/types/components/shared/billing-timeframe/index.d.ts.map +1 -1
  195. package/dist/types/components/shared/header-price/index.d.ts.map +1 -1
  196. package/dist/types/components/shared/storage/components/storage-dropdown.d.ts.map +1 -1
  197. package/dist/types/components/shared/storage/components/storage-feature-label.d.ts.map +1 -1
  198. package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts +1 -1
  199. package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts.map +1 -1
  200. package/dist/types/fixtures/sites-purchases.d.ts +2 -4
  201. package/dist/types/fixtures/sites-purchases.d.ts.map +1 -1
  202. package/dist/types/grid-context.d.ts +4 -1
  203. package/dist/types/grid-context.d.ts.map +1 -1
  204. package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts +14 -0
  205. package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts.map +1 -0
  206. package/dist/types/hooks/data-store/types.d.ts +21 -0
  207. package/dist/types/hooks/data-store/types.d.ts.map +1 -1
  208. package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts +1 -1
  209. package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts.map +1 -1
  210. package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts +1 -1
  211. package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts.map +1 -1
  212. package/dist/types/hooks/data-store/use-grid-plans.d.ts.map +1 -1
  213. package/dist/types/hooks/data-store/use-highlight-labels.d.ts.map +1 -1
  214. package/dist/types/hooks/data-store/use-plan-billing-description.d.ts.map +1 -1
  215. package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts +4 -1
  216. package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts.map +1 -1
  217. package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts +4 -1
  218. package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts.map +1 -1
  219. package/dist/types/hooks/data-store/use-title-badges.d.ts +9 -0
  220. package/dist/types/hooks/data-store/use-title-badges.d.ts.map +1 -0
  221. package/dist/types/hooks/use-visible-grid-plans.d.ts +14 -0
  222. package/dist/types/hooks/use-visible-grid-plans.d.ts.map +1 -0
  223. package/dist/types/index.d.ts +7 -0
  224. package/dist/types/index.d.ts.map +1 -1
  225. package/dist/types/lib/get-plan-features-object.d.ts +1 -1
  226. package/dist/types/lib/get-plan-features-object.d.ts.map +1 -1
  227. package/dist/types/lib/plan-pricing-utils.d.ts +105 -0
  228. package/dist/types/lib/plan-pricing-utils.d.ts.map +1 -0
  229. package/dist/types/types.d.ts +29 -2
  230. package/dist/types/types.d.ts.map +1 -1
  231. package/package.json +38 -28
  232. package/src/_shared.scss +4 -3
  233. package/src/components/comparison-grid/index.tsx +258 -181
  234. package/src/components/comparison-grid/style.scss +10 -2
  235. package/src/components/features-grid/client-logo-list/client-list.tsx +0 -25
  236. package/src/components/features-grid/index.tsx +35 -18
  237. package/src/components/features-grid/plan-features-list.tsx +15 -4
  238. package/src/components/features-grid/plan-headers.tsx +10 -3
  239. package/src/components/features-grid/plan-tagline.tsx +1 -1
  240. package/src/components/features-grid/style.scss +107 -19
  241. package/src/components/features-grid/table.tsx +4 -2
  242. package/src/components/features.tsx +66 -6
  243. package/src/components/item.tsx +6 -3
  244. package/src/components/plan-button/index.tsx +7 -1
  245. package/src/components/plan-button/style.scss +75 -51
  246. package/src/components/plan-div-td-container.tsx +6 -2
  247. package/src/components/plan-logo.tsx +16 -9
  248. package/src/components/plan-type-selector/components/interval-type-dropdown.tsx +14 -1
  249. package/src/components/plan-type-selector/hooks/use-max-discount.ts +8 -47
  250. package/src/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.ts +19 -17
  251. package/src/components/plans-2023-tooltip.tsx +17 -5
  252. package/src/components/shared/action-button/index.tsx +46 -5
  253. package/src/components/shared/action-button/style.scss +4 -0
  254. package/src/components/shared/billing-timeframe/index.tsx +12 -7
  255. package/src/components/shared/header-price/index.tsx +129 -27
  256. package/src/components/shared/header-price/style.scss +9 -3
  257. package/src/components/shared/storage/components/plan-storage.tsx +2 -2
  258. package/src/components/shared/storage/components/storage-dropdown.tsx +36 -15
  259. package/src/components/shared/storage/components/storage-feature-label.tsx +2 -1
  260. package/src/components/shared/storage/hooks/use-plan-storage.ts +3 -0
  261. package/src/components/test/actions-button.tsx +5 -0
  262. package/src/components/test/billing-timeframe.tsx +1 -1
  263. package/src/components/test/header-price.tsx +342 -4
  264. package/src/fixtures/sites-purchases.ts +2 -4
  265. package/src/grid-context.tsx +9 -0
  266. package/src/hooks/data-store/get-renewal-pricing-text.ts +73 -0
  267. package/src/hooks/data-store/types.ts +21 -0
  268. package/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts +10 -0
  269. package/src/hooks/data-store/use-grid-plans-for-features-grid.ts +10 -0
  270. package/src/hooks/data-store/use-grid-plans.tsx +189 -23
  271. package/src/hooks/data-store/use-highlight-labels.ts +12 -3
  272. package/src/hooks/data-store/use-plan-billing-description.tsx +80 -15
  273. package/src/hooks/data-store/use-plan-features-for-grid-plans.ts +135 -1
  274. package/src/hooks/data-store/use-restructured-plan-features-for-comparison-grid.ts +93 -20
  275. package/src/hooks/data-store/use-title-badges.ts +31 -0
  276. package/src/hooks/test/use-visible-grid-plans.tsx +116 -0
  277. package/src/hooks/use-is-large-currency.ts +1 -1
  278. package/src/hooks/use-visible-grid-plans.tsx +102 -0
  279. package/src/index.tsx +18 -0
  280. package/src/lib/get-plan-features-object.ts +23 -2
  281. package/src/lib/plan-pricing-utils.ts +211 -0
  282. package/src/lib/test/plan-pricing-utils.ts +594 -0
  283. package/src/style-imports.d.ts +3 -0
  284. package/src/types.ts +41 -0
  285. package/dist/cjs/components/features-grid/mobile-free-domain.js +0 -25
  286. package/dist/cjs/components/features-grid/mobile-free-domain.js.map +0 -1
  287. package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js +0 -15
  288. package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
  289. package/dist/esm/components/features-grid/mobile-free-domain.js +0 -23
  290. package/dist/esm/components/features-grid/mobile-free-domain.js.map +0 -1
  291. package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js +0 -12
  292. package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
  293. package/dist/types/components/features-grid/mobile-free-domain.d.ts +0 -8
  294. package/dist/types/components/features-grid/mobile-free-domain.d.ts.map +0 -1
  295. package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts +0 -9
  296. package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts.map +0 -1
  297. package/src/components/features-grid/mobile-free-domain.tsx +0 -51
  298. package/src/lib/get-plan-pricing-info-from-grid-plans.ts +0 -31
@@ -0,0 +1,594 @@
1
+ import {
2
+ getPlanPriceForDuration,
3
+ calculateDiscountPercentage,
4
+ fromPricingMetaForGridPlan,
5
+ fromVariantPriceData,
6
+ } from '../plan-pricing-utils';
7
+ import type { PlanPriceInfo, VariantPriceData } from '../plan-pricing-utils';
8
+ import type { Plans } from '@automattic/data-stores';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Helpers
12
+ // ---------------------------------------------------------------------------
13
+
14
+ function makePlanPriceInfo( overrides: Partial< PlanPriceInfo > = {} ): PlanPriceInfo {
15
+ return {
16
+ termMonths: 12,
17
+ regularPricePerMonth: 2000,
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ function makePricingMeta(
23
+ overrides: Partial< Plans.PricingMetaForGridPlan > = {}
24
+ ): Plans.PricingMetaForGridPlan {
25
+ return {
26
+ originalPrice: { monthly: 2000, full: 24000 },
27
+ discountedPrice: { monthly: null, full: null },
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ function makeVariant( overrides: Partial< VariantPriceData > = {} ): VariantPriceData {
33
+ return {
34
+ termIntervalInMonths: 12,
35
+ priceInteger: 24000,
36
+ priceBeforeDiscounts: 24000,
37
+ introductoryInterval: 0,
38
+ introductoryTerm: 'year',
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // getPlanPriceForDuration
45
+ // ---------------------------------------------------------------------------
46
+
47
+ describe( 'getPlanPriceForDuration', () => {
48
+ describe( 'no intro offer', () => {
49
+ it( 'returns regularPricePerMonth × durationMonths for a monthly plan', () => {
50
+ const info = makePlanPriceInfo( { termMonths: 1, regularPricePerMonth: 2000 } );
51
+ expect( getPlanPriceForDuration( info, 6 ) ).toBe( 12000 );
52
+ } );
53
+
54
+ it( 'returns pro-rated price for a yearly plan over 6 months', () => {
55
+ const info = makePlanPriceInfo( { termMonths: 12, regularPricePerMonth: 1500 } );
56
+ expect( getPlanPriceForDuration( info, 6 ) ).toBe( 9000 );
57
+ } );
58
+
59
+ it( 'uses discountedPricePerMonth instead of regularPricePerMonth when provided', () => {
60
+ const info = makePlanPriceInfo( {
61
+ termMonths: 12,
62
+ regularPricePerMonth: 2000,
63
+ discountedPricePerMonth: 1800,
64
+ } );
65
+ expect( getPlanPriceForDuration( info, 6 ) ).toBe( 10800 );
66
+ } );
67
+
68
+ it( 'uses discountedPricePerMonth for post-intro months when both are present', () => {
69
+ const info = makePlanPriceInfo( {
70
+ termMonths: 12,
71
+ regularPricePerMonth: 2000,
72
+ discountedPricePerMonth: 1800,
73
+ introOffer: { pricePerMonth: 1000, durationMonths: 6, isActive: true },
74
+ } );
75
+ // 6 intro months × 1000 + 6 regular months × 1800
76
+ expect( getPlanPriceForDuration( info, 12 ) ).toBe( 6000 + 10800 );
77
+ } );
78
+ } );
79
+
80
+ describe( 'with active intro offer', () => {
81
+ const monthlyWithIntro = makePlanPriceInfo( {
82
+ termMonths: 1,
83
+ regularPricePerMonth: 2000,
84
+ introOffer: { pricePerMonth: 1000, durationMonths: 1, isActive: true },
85
+ } );
86
+
87
+ const yearlyWithIntro = makePlanPriceInfo( {
88
+ termMonths: 12,
89
+ regularPricePerMonth: 2000,
90
+ introOffer: { pricePerMonth: 1000, durationMonths: 12, isActive: true },
91
+ } );
92
+
93
+ it( 'applies intro price for the first N months and regular price thereafter (monthly plan, 6 months)', () => {
94
+ // 1 intro month × 1000 + 5 regular months × 2000
95
+ expect( getPlanPriceForDuration( monthlyWithIntro, 6 ) ).toBe( 11000 );
96
+ } );
97
+
98
+ it( 'ignores intro price when useIntroOffer is false (monthly plan)', () => {
99
+ expect( getPlanPriceForDuration( monthlyWithIntro, 6, { useIntroOffer: false } ) ).toBe(
100
+ 12000
101
+ );
102
+ } );
103
+
104
+ it( 'applies intro price for a yearly plan over 6 months (duration shorter than intro)', () => {
105
+ // intro covers 12 months; for 6 months only intro applies
106
+ // 6 × 1000 = 6000
107
+ expect( getPlanPriceForDuration( yearlyWithIntro, 6 ) ).toBe( 6000 );
108
+ } );
109
+
110
+ it( 'ignores intro price when useIntroOffer is false (yearly plan)', () => {
111
+ expect( getPlanPriceForDuration( yearlyWithIntro, 6, { useIntroOffer: false } ) ).toBe(
112
+ 12000
113
+ );
114
+ } );
115
+
116
+ it( 'handles duration longer than the intro period (3-month duration, 1-month intro)', () => {
117
+ // 1 intro month × 1000 + 2 regular months × 2000
118
+ expect( getPlanPriceForDuration( monthlyWithIntro, 3 ) ).toBe( 5000 );
119
+ } );
120
+
121
+ it( 'handles duration exactly equal to the intro period', () => {
122
+ expect( getPlanPriceForDuration( monthlyWithIntro, 1 ) ).toBe( 1000 );
123
+ } );
124
+ } );
125
+
126
+ describe( 'with inactive intro offer', () => {
127
+ it( 'falls through to regular price when isActive is false, even if useIntroOffer is true', () => {
128
+ const info = makePlanPriceInfo( {
129
+ termMonths: 12,
130
+ regularPricePerMonth: 2000,
131
+ introOffer: { pricePerMonth: 1000, durationMonths: 12, isActive: false },
132
+ } );
133
+ expect( getPlanPriceForDuration( info, 6 ) ).toBe( 12000 );
134
+ } );
135
+ } );
136
+
137
+ describe( 'edge cases', () => {
138
+ it( 'returns 0 for 0 duration months', () => {
139
+ const info = makePlanPriceInfo( { termMonths: 12, regularPricePerMonth: 2000 } );
140
+ expect( getPlanPriceForDuration( info, 0 ) ).toBe( 0 );
141
+ } );
142
+ } );
143
+ } );
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // calculateDiscountPercentage
147
+ // ---------------------------------------------------------------------------
148
+
149
+ describe( 'calculateDiscountPercentage', () => {
150
+ it( 'returns the floored percentage discount for a standard case', () => {
151
+ // (2000 - 1400) / 2000 × 100 = 30
152
+ expect( calculateDiscountPercentage( 2000, 1400 ) ).toBe( 30 );
153
+ } );
154
+
155
+ it( 'floors fractional percentages (does not round up)', () => {
156
+ // (3000 - 2050) / 3000 × 100 = 31.666... → floor = 31
157
+ expect( calculateDiscountPercentage( 3000, 2050 ) ).toBe( 31 );
158
+ } );
159
+
160
+ it( 'returns undefined when cheaperPrice equals referencePrice (no saving)', () => {
161
+ expect( calculateDiscountPercentage( 2000, 2000 ) ).toBeUndefined();
162
+ } );
163
+
164
+ it( 'returns undefined when cheaperPrice is higher than referencePrice', () => {
165
+ expect( calculateDiscountPercentage( 2000, 2500 ) ).toBeUndefined();
166
+ } );
167
+
168
+ it( 'returns undefined when referencePrice is zero', () => {
169
+ expect( calculateDiscountPercentage( 0, 0 ) ).toBeUndefined();
170
+ } );
171
+
172
+ it( 'returns undefined when referencePrice is negative', () => {
173
+ expect( calculateDiscountPercentage( -100, -200 ) ).toBeUndefined();
174
+ } );
175
+
176
+ it( 'returns 99 for a near-complete discount (not 100)', () => {
177
+ // (1000 - 1) / 1000 × 100 = 99.9 → floor = 99
178
+ expect( calculateDiscountPercentage( 1000, 1 ) ).toBe( 99 );
179
+ } );
180
+ } );
181
+
182
+ // ---------------------------------------------------------------------------
183
+ // fromPricingMetaForGridPlan
184
+ // ---------------------------------------------------------------------------
185
+
186
+ describe( 'fromPricingMetaForGridPlan', () => {
187
+ it( 'converts a monthly plan with no intro offer', () => {
188
+ const meta = makePricingMeta( { billingPeriod: 31 } );
189
+ const result = fromPricingMetaForGridPlan( meta );
190
+ expect( result ).toEqual( {
191
+ termMonths: 1,
192
+ regularPricePerMonth: 2000,
193
+ discountedPricePerMonth: undefined,
194
+ introOffer: undefined,
195
+ } );
196
+ } );
197
+
198
+ it( 'converts a yearly plan with no intro offer', () => {
199
+ const meta = makePricingMeta( { billingPeriod: 365 } );
200
+ const result = fromPricingMetaForGridPlan( meta );
201
+ expect( result ).toEqual( {
202
+ termMonths: 12,
203
+ regularPricePerMonth: 2000,
204
+ discountedPricePerMonth: undefined,
205
+ introOffer: undefined,
206
+ } );
207
+ } );
208
+
209
+ it( 'converts a biennial plan (730 days → 24 months)', () => {
210
+ const meta = makePricingMeta( {
211
+ billingPeriod: 730,
212
+ originalPrice: { monthly: 1500, full: 36000 },
213
+ } );
214
+ const result = fromPricingMetaForGridPlan( meta );
215
+ expect( result?.termMonths ).toBe( 24 );
216
+ expect( result?.regularPricePerMonth ).toBe( 1500 );
217
+ } );
218
+
219
+ it( 'converts a triennial plan (1095 days → 36 months)', () => {
220
+ const meta = makePricingMeta( {
221
+ billingPeriod: 1095,
222
+ originalPrice: { monthly: 1200, full: 43200 },
223
+ } );
224
+ const result = fromPricingMetaForGridPlan( meta );
225
+ expect( result?.termMonths ).toBe( 36 );
226
+ } );
227
+
228
+ it( 'sets discountedPricePerMonth when discountedPrice.monthly is provided', () => {
229
+ const meta = makePricingMeta( {
230
+ billingPeriod: 365,
231
+ discountedPrice: { monthly: 1800, full: 21600 },
232
+ } );
233
+ const result = fromPricingMetaForGridPlan( meta );
234
+ expect( result?.discountedPricePerMonth ).toBe( 1800 );
235
+ } );
236
+
237
+ it( 'leaves discountedPricePerMonth undefined when discountedPrice.monthly is null', () => {
238
+ const meta = makePricingMeta( { billingPeriod: 365 } );
239
+ const result = fromPricingMetaForGridPlan( meta );
240
+ expect( result?.discountedPricePerMonth ).toBeUndefined();
241
+ } );
242
+
243
+ it( 'maps an active yearly intro offer (intervalUnit: year, intervalCount: 1)', () => {
244
+ const meta = makePricingMeta( {
245
+ billingPeriod: 365,
246
+ introOffer: {
247
+ rawPrice: { monthly: 1000, full: 12000 },
248
+ intervalUnit: 'year',
249
+ intervalCount: 1,
250
+ isOfferComplete: false,
251
+ formattedPrice: '$120',
252
+ },
253
+ } );
254
+ const result = fromPricingMetaForGridPlan( meta );
255
+ expect( result?.introOffer ).toEqual( {
256
+ pricePerMonth: 1000,
257
+ durationMonths: 12,
258
+ isActive: true,
259
+ } );
260
+ } );
261
+
262
+ it( 'maps a completed intro offer with isActive: false', () => {
263
+ const meta = makePricingMeta( {
264
+ billingPeriod: 365,
265
+ introOffer: {
266
+ rawPrice: { monthly: 1000, full: 12000 },
267
+ intervalUnit: 'year',
268
+ intervalCount: 1,
269
+ isOfferComplete: true,
270
+ formattedPrice: '$120',
271
+ },
272
+ } );
273
+ const result = fromPricingMetaForGridPlan( meta );
274
+ expect( result?.introOffer?.isActive ).toBe( false );
275
+ } );
276
+
277
+ it( 'maps an intro offer spanning 2 years (intervalUnit: year, intervalCount: 2 → 24 months)', () => {
278
+ const meta = makePricingMeta( {
279
+ billingPeriod: 730,
280
+ originalPrice: { monthly: 1500, full: 36000 },
281
+ introOffer: {
282
+ rawPrice: { monthly: 900, full: 21600 },
283
+ intervalUnit: 'year',
284
+ intervalCount: 2,
285
+ isOfferComplete: false,
286
+ formattedPrice: '$216',
287
+ },
288
+ } );
289
+ const result = fromPricingMetaForGridPlan( meta );
290
+ expect( result?.introOffer?.durationMonths ).toBe( 24 );
291
+ } );
292
+
293
+ it( 'maps an intro offer in months (intervalUnit: month, intervalCount: 3 → 3 months)', () => {
294
+ const meta = makePricingMeta( {
295
+ billingPeriod: 365,
296
+ introOffer: {
297
+ rawPrice: { monthly: 500, full: 1500 },
298
+ intervalUnit: 'month',
299
+ intervalCount: 3,
300
+ isOfferComplete: false,
301
+ formattedPrice: '$15',
302
+ },
303
+ } );
304
+ const result = fromPricingMetaForGridPlan( meta );
305
+ expect( result?.introOffer?.durationMonths ).toBe( 3 );
306
+ } );
307
+
308
+ it( 'returns null when originalPrice.monthly is null', () => {
309
+ const meta = makePricingMeta( {
310
+ billingPeriod: 365,
311
+ originalPrice: { monthly: null, full: null },
312
+ } );
313
+ expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
314
+ } );
315
+
316
+ it( 'returns null when billingPeriod is undefined', () => {
317
+ const meta = makePricingMeta( { billingPeriod: undefined } );
318
+ expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
319
+ } );
320
+
321
+ it( 'returns null for billingPeriod -1 (lifetime/unknown)', () => {
322
+ const meta = makePricingMeta( { billingPeriod: -1 } );
323
+ expect( fromPricingMetaForGridPlan( meta ) ).toBeNull();
324
+ } );
325
+ } );
326
+
327
+ // ---------------------------------------------------------------------------
328
+ // fromVariantPriceData
329
+ // ---------------------------------------------------------------------------
330
+
331
+ describe( 'fromVariantPriceData', () => {
332
+ it( 'converts a monthly plan with no intro offer', () => {
333
+ const variant = makeVariant( {
334
+ termIntervalInMonths: 1,
335
+ priceInteger: 2000,
336
+ priceBeforeDiscounts: 2000,
337
+ introductoryInterval: 0,
338
+ } );
339
+ expect( fromVariantPriceData( variant ) ).toEqual( {
340
+ termMonths: 1,
341
+ regularPricePerMonth: 2000,
342
+ introOffer: undefined,
343
+ } );
344
+ } );
345
+
346
+ it( 'converts an annual plan with no intro offer', () => {
347
+ const variant = makeVariant( {
348
+ termIntervalInMonths: 12,
349
+ priceInteger: 24000,
350
+ priceBeforeDiscounts: 24000,
351
+ introductoryInterval: 0,
352
+ } );
353
+ const result = fromVariantPriceData( variant );
354
+ expect( result.termMonths ).toBe( 12 );
355
+ expect( result.regularPricePerMonth ).toBe( 2000 );
356
+ expect( result.introOffer ).toBeUndefined();
357
+ } );
358
+
359
+ it( 'converts an annual plan where the entire term is the intro (1-year intro on annual plan)', () => {
360
+ // priceInteger = 10000 (intro year), priceBeforeDiscounts = 24000 (regular year)
361
+ // introDuration = 1 × 12 = 12 months = full term
362
+ // nonIntroMonths = 12 - 12 = 0
363
+ // introPriceTotal = 10000 - 0 × 2000 = 10000
364
+ // introPricePerMonth = Math.round(10000 / 12) = Math.round(833.33) = 833
365
+ const variant = makeVariant( {
366
+ termIntervalInMonths: 12,
367
+ priceInteger: 10000,
368
+ priceBeforeDiscounts: 24000,
369
+ introductoryInterval: 1,
370
+ introductoryTerm: 'year',
371
+ } );
372
+ const result = fromVariantPriceData( variant );
373
+ expect( result.termMonths ).toBe( 12 );
374
+ expect( result.regularPricePerMonth ).toBe( 2000 );
375
+ expect( result.introOffer ).toEqual( {
376
+ pricePerMonth: 833,
377
+ durationMonths: 12,
378
+ isActive: true,
379
+ } );
380
+ } );
381
+
382
+ it( 'derives intro price for a biennial plan with a 1-year intro', () => {
383
+ // priceBeforeDiscounts = 48000 (2 × 24000 regular year)
384
+ // priceInteger = 34000 (intro year 10000 + regular year 24000)
385
+ // regularPricePerMonth = 48000 / 24 = 2000
386
+ // introDuration = 1 × 12 = 12 months
387
+ // nonIntroMonths = 24 - 12 = 12
388
+ // introPriceTotal = 34000 - 12 × 2000 = 34000 - 24000 = 10000
389
+ // introPricePerMonth = Math.round(10000 / 12) = Math.round(833.33) = 833
390
+ const variant = makeVariant( {
391
+ termIntervalInMonths: 24,
392
+ priceInteger: 34000,
393
+ priceBeforeDiscounts: 48000,
394
+ introductoryInterval: 1,
395
+ introductoryTerm: 'year',
396
+ } );
397
+ const result = fromVariantPriceData( variant );
398
+ expect( result.termMonths ).toBe( 24 );
399
+ expect( result.regularPricePerMonth ).toBe( 2000 );
400
+ expect( result.introOffer ).toEqual( {
401
+ pricePerMonth: 833,
402
+ durationMonths: 12,
403
+ isActive: true,
404
+ } );
405
+ } );
406
+
407
+ it( 'rounds regularPricePerMonth to the nearest integer for non-evenly-divisible prices', () => {
408
+ // 10000 / 12 = 833.333... → rounds to 833 (not 834, and not a fraction)
409
+ const variant = makeVariant( {
410
+ termIntervalInMonths: 12,
411
+ priceBeforeDiscounts: 10000,
412
+ priceInteger: 10000,
413
+ introductoryInterval: 0,
414
+ } );
415
+ const result = fromVariantPriceData( variant );
416
+ expect( result.regularPricePerMonth ).toBe( 833 );
417
+ expect( Number.isInteger( result.regularPricePerMonth ) ).toBe( true );
418
+ } );
419
+
420
+ it( 'rounds introOffer.pricePerMonth to the nearest integer for non-evenly-divisible prices', () => {
421
+ // Annual plan, full term is the intro period
422
+ // priceBeforeDiscounts = 24000 → regularPricePerMonth = 2000 (exact)
423
+ // priceInteger = 5000 → introPriceTotal = 5000 - 0*2000 = 5000
424
+ // 5000 / 12 = 416.666... → rounds to 417
425
+ const variant = makeVariant( {
426
+ termIntervalInMonths: 12,
427
+ priceBeforeDiscounts: 24000,
428
+ priceInteger: 5000,
429
+ introductoryInterval: 1,
430
+ introductoryTerm: 'year',
431
+ } );
432
+ const result = fromVariantPriceData( variant );
433
+ expect( result.introOffer?.pricePerMonth ).toBe( 417 );
434
+ expect( Number.isInteger( result.introOffer?.pricePerMonth ) ).toBe( true );
435
+ } );
436
+
437
+ it( 'handles a monthly intro offer (introductoryTerm: month)', () => {
438
+ // Monthly plan with a 1-month intro: the whole term is intro
439
+ // termIntervalInMonths: 12, introDuration: 1 month
440
+ // priceBeforeDiscounts: 24000, priceInteger: 22000
441
+ // regularPricePerMonth = 24000 / 12 = 2000
442
+ // nonIntroMonths = 12 - 1 = 11
443
+ // introPriceTotal = 22000 - 11 × 2000 = 22000 - 22000 = 0 — not useful
444
+ // Use a simpler case: termIntervalInMonths: 1, 1-month intro
445
+ const variant = makeVariant( {
446
+ termIntervalInMonths: 1,
447
+ priceInteger: 1000,
448
+ priceBeforeDiscounts: 2000,
449
+ introductoryInterval: 1,
450
+ introductoryTerm: 'month',
451
+ } );
452
+ const result = fromVariantPriceData( variant );
453
+ expect( result.introOffer?.durationMonths ).toBe( 1 );
454
+ // nonIntroMonths = 1 - 1 = 0; introPriceTotal = 1000 - 0 = 1000
455
+ expect( result.introOffer?.pricePerMonth ).toBe( 1000 );
456
+ } );
457
+
458
+ it( 'produces no introOffer when introductoryInterval is 0', () => {
459
+ const variant = makeVariant( {
460
+ priceInteger: 24000,
461
+ priceBeforeDiscounts: 24000,
462
+ introductoryInterval: 0,
463
+ } );
464
+ expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
465
+ } );
466
+
467
+ it( 'produces no introOffer when priceInteger equals priceBeforeDiscounts (no actual discount)', () => {
468
+ // introductoryInterval is set but prices are equal — indicates intro not applied
469
+ const variant = makeVariant( {
470
+ termIntervalInMonths: 12,
471
+ priceInteger: 24000,
472
+ priceBeforeDiscounts: 24000,
473
+ introductoryInterval: 1,
474
+ introductoryTerm: 'year',
475
+ } );
476
+ expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
477
+ } );
478
+
479
+ it( 'handles a multi-month intro on a monthly plan (introDurationMonths > termMonths)', () => {
480
+ // Monthly plan ($20/month), 3-month intro at $5/month.
481
+ // termMonths=1, introDurationMonths=3 → whole term is within the intro period.
482
+ // nonIntroMonths = max(0, 1 - 3) = 0
483
+ // introPriceTotal = 500 - 0 = 500
484
+ // introPricePerMonth = round(500 / min(3, 1)) = round(500 / 1) = 500
485
+ const variant = makeVariant( {
486
+ termIntervalInMonths: 1,
487
+ priceBeforeDiscounts: 2000,
488
+ priceInteger: 500,
489
+ introductoryInterval: 3,
490
+ introductoryTerm: 'month',
491
+ } );
492
+ const result = fromVariantPriceData( variant );
493
+ expect( result.introOffer?.pricePerMonth ).toBe( 500 );
494
+ expect( result.introOffer?.durationMonths ).toBe( 3 );
495
+ } );
496
+
497
+ it( 'handles a multi-year intro on an annual plan (introDurationMonths > termMonths)', () => {
498
+ // Annual plan ($200/year), 3-year intro at $100/year.
499
+ // termMonths=12, introDurationMonths=36 → whole term is within the intro period.
500
+ // regularPricePerMonth = round(20000 / 12) = 1667
501
+ // nonIntroMonths = max(0, 12 - 36) = 0
502
+ // introPriceTotal = 10000 - 0 = 10000
503
+ // introPricePerMonth = round(10000 / min(36, 12)) = round(10000 / 12) = 833
504
+ const variant = makeVariant( {
505
+ termIntervalInMonths: 12,
506
+ priceBeforeDiscounts: 20000,
507
+ priceInteger: 10000,
508
+ introductoryInterval: 3,
509
+ introductoryTerm: 'year',
510
+ } );
511
+ const result = fromVariantPriceData( variant );
512
+ expect( result.introOffer?.pricePerMonth ).toBe( 833 );
513
+ expect( result.introOffer?.durationMonths ).toBe( 36 );
514
+ } );
515
+
516
+ it( 'produces no introOffer when introPriceTotal is non-positive (inconsistent data)', () => {
517
+ // Annual plan with a 3-month intro, but priceInteger is too low to be consistent:
518
+ // regularPricePerMonth = round(24000 / 12) = 2000
519
+ // nonIntroMonths = 12 - 3 = 9
520
+ // introPriceTotal = 1000 - 9 × 2000 = 1000 - 18000 = -17000 → bail out
521
+ const variant = makeVariant( {
522
+ termIntervalInMonths: 12,
523
+ priceBeforeDiscounts: 24000,
524
+ priceInteger: 1000,
525
+ introductoryInterval: 3,
526
+ introductoryTerm: 'month',
527
+ } );
528
+ expect( fromVariantPriceData( variant ).introOffer ).toBeUndefined();
529
+ } );
530
+
531
+ it( 'does not set discountedPricePerMonth', () => {
532
+ const variant = makeVariant();
533
+ const result = fromVariantPriceData( variant );
534
+ expect( result.discountedPricePerMonth ).toBeUndefined();
535
+ } );
536
+ } );
537
+
538
+ // ---------------------------------------------------------------------------
539
+ // Integration: adapters + getPlanPriceForDuration + calculateDiscountPercentage
540
+ // ---------------------------------------------------------------------------
541
+
542
+ describe( 'integration — fromPricingMetaForGridPlan → getPlanPriceForDuration → calculateDiscountPercentage', () => {
543
+ it( 'calculates the annual vs monthly discount (no intro offers)', () => {
544
+ const monthlyMeta = makePricingMeta( {
545
+ billingPeriod: 31,
546
+ originalPrice: { monthly: 2000, full: 2000 },
547
+ } );
548
+ const yearlyMeta = makePricingMeta( {
549
+ billingPeriod: 365,
550
+ originalPrice: { monthly: 1500, full: 18000 },
551
+ } );
552
+
553
+ const monthlyInfo = fromPricingMetaForGridPlan( monthlyMeta )!;
554
+ const yearlyInfo = fromPricingMetaForGridPlan( yearlyMeta )!;
555
+
556
+ // Compare monthly-equivalent prices for 12 months
557
+ const monthlyEquivPerMonth =
558
+ getPlanPriceForDuration( monthlyInfo, 12, { useIntroOffer: false } ) / 12;
559
+ const yearlyEquivPerMonth =
560
+ getPlanPriceForDuration( yearlyInfo, 12, { useIntroOffer: false } ) / 12;
561
+
562
+ // (2000 - 1500) / 2000 × 100 = 25
563
+ expect( calculateDiscountPercentage( monthlyEquivPerMonth, yearlyEquivPerMonth ) ).toBe( 25 );
564
+ } );
565
+ } );
566
+
567
+ describe( 'integration — fromVariantPriceData → getPlanPriceForDuration → calculateDiscountPercentage', () => {
568
+ it( 'calculates the upsell discount from annual to biennial (no intros)', () => {
569
+ const annualVariant = makeVariant( {
570
+ termIntervalInMonths: 12,
571
+ priceInteger: 24000,
572
+ priceBeforeDiscounts: 24000,
573
+ introductoryInterval: 0,
574
+ } );
575
+ const biennialVariant = makeVariant( {
576
+ termIntervalInMonths: 24,
577
+ priceInteger: 40000,
578
+ priceBeforeDiscounts: 40000,
579
+ introductoryInterval: 0,
580
+ } );
581
+
582
+ const annualInfo = fromVariantPriceData( annualVariant );
583
+ const biennialInfo = fromVariantPriceData( biennialVariant );
584
+
585
+ // Normalise both to per-month cost over the biennial period (24 months)
586
+ const refPerMonth = getPlanPriceForDuration( annualInfo, 24 ) / 24;
587
+ const cheaperPerMonth = getPlanPriceForDuration( biennialInfo, 24 ) / 24;
588
+
589
+ // annualInfo per month: 24000/12 = 2000; over 24 months: 48000 → 2000/mo
590
+ // biennialInfo per month: 40000/24 ≈ 1666.67/mo
591
+ // discount = floor((2000 - 1666.67) / 2000 × 100) = floor(16.67) = 16
592
+ expect( calculateDiscountPercentage( refPerMonth, cheaperPerMonth ) ).toBe( 16 );
593
+ } );
594
+ } );
@@ -0,0 +1,3 @@
1
+ // Ambient declarations for side-effect style imports (TS6 requires these; see TS2882).
2
+ declare module '*.scss';
3
+ declare module '*.css';
package/src/types.ts CHANGED
@@ -18,6 +18,14 @@ export type TransformedFeatureObject = FeatureObject & {
18
18
  availableForCurrentPlan: boolean;
19
19
  availableOnlyForAnnualPlans: boolean;
20
20
  isHighlighted?: boolean;
21
+ /**
22
+ * When true, extra bottom margin on the last feature row for pricing experiment variants.
23
+ */
24
+ isExperimentLastFeature?: boolean;
25
+ /**
26
+ * Badge text to display after the feature title (e.g. pricing differentiators pills).
27
+ */
28
+ badgeText?: TranslateResult;
21
29
  };
22
30
 
23
31
  export interface PlanFeaturesForGridPlan {
@@ -45,6 +53,7 @@ export interface GridPlan {
45
53
  product_slug: string;
46
54
  } | null;
47
55
  highlightLabel?: React.ReactNode | null;
56
+ titleBadge?: React.ReactNode | null;
48
57
  }
49
58
 
50
59
  /***********************
@@ -55,6 +64,7 @@ export type GridSize = 'small' | 'smedium' | 'medium' | 'large' | 'xlarge';
55
64
 
56
65
  export type PlansIntent =
57
66
  | 'plans-affiliate'
67
+ | 'plans-ai-assembler-free-trial'
58
68
  | 'plans-blog-onboarding'
59
69
  | 'plans-newsletter'
60
70
  | 'plans-new-hosted-site'
@@ -74,6 +84,15 @@ export type PlansIntent =
74
84
  | 'plans-guided-segment-nonprofit'
75
85
  | 'plans-guided-segment-consumer-or-business'
76
86
  | 'plans-site-selected-legacy'
87
+ | 'plans-playground'
88
+ | 'plans-playground-premium' // This plan intent is currently not utilized but will be soon
89
+ | 'plans-upgrade'
90
+ | 'plans-upgrade-or-downgrade'
91
+ | 'plans-wordpress-hosting'
92
+ | 'plans-website-builder'
93
+ | 'plans-woo-hosted'
94
+ | 'plans-woo-hosting-solutions'
95
+ | 'plans-migration'
77
96
  | 'default';
78
97
 
79
98
  export interface PlanActionOverrides {
@@ -135,6 +154,8 @@ export interface ComparisonGridProps extends CommonGridProps {
135
154
  // Value of the `?plan=` query param, so we can highlight a given plan.
136
155
  selectedPlan?: string;
137
156
  intervalType: SupportedUrlFriendlyTermType;
157
+ /** Called when the number of visible plans in the grid changes (e.g. for narrowing the container). */
158
+ onVisiblePlansCountChange?: ( count: number ) => void;
138
159
  }
139
160
 
140
161
  export type UseActionCallback = ( {
@@ -152,6 +173,7 @@ export type UseActionCallback = ( {
152
173
  export interface GridAction {
153
174
  primary: {
154
175
  text: TranslateResult;
176
+ ariaLabel?: TranslateResult;
155
177
  callback: () => Promise< void > | void;
156
178
  // TODO: It's not clear if status is ever actually set to 'blocked'. Investigate and remove if not.
157
179
  status?: 'disabled' | 'blocked' | 'enabled';
@@ -172,6 +194,8 @@ export type UseAction = ( {
172
194
  planTitle,
173
195
  priceString,
174
196
  selectedStorageAddOn,
197
+ pricing,
198
+ isMonthlyPlan,
175
199
  }: {
176
200
  availableForPurchase?: boolean;
177
201
  billingPeriod?: PlanPricing[ 'billPeriod' ];
@@ -184,6 +208,8 @@ export type UseAction = ( {
184
208
  planTitle?: TranslateResult;
185
209
  priceString?: string;
186
210
  selectedStorageAddOn?: AddOns.AddOnMeta | null;
211
+ pricing?: Plans.PricingMetaForGridPlan | null;
212
+ isMonthlyPlan?: boolean;
187
213
  } ) => GridAction;
188
214
 
189
215
  export type GridContextProps = {
@@ -239,6 +265,21 @@ export type GridContextProps = {
239
265
  * calculating prices.
240
266
  */
241
267
  reflectStorageSelectionInPlanPrices?: boolean;
268
+
269
+ /**
270
+ * Enable simplified billing description
271
+ */
272
+ showSimplifiedBillingDescription?: boolean;
273
+ /**
274
+ * If, and how to present increased renewal pricing (null or the assigned variant name)
275
+ */
276
+ showBillingDescriptionForIncreasedRenewalPrice?: string | null;
277
+
278
+ /**
279
+ * When true, the user is in the rolled-out pricing differentiation cohort.
280
+ * Used to display cohort-specific feature titles in the comparison grid.
281
+ */
282
+ isExperimentVariant?: boolean;
242
283
  };
243
284
 
244
285
  export type ComparisonGridExternalProps = Omit<