@automattic/plans-grid-next 1.0.1 → 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 (339) 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 +101 -71
  4. package/dist/cjs/components/comparison-grid/index.js.map +1 -1
  5. package/dist/cjs/components/comparison-grid/index.stories.js.map +1 -1
  6. package/dist/cjs/components/comparison-grid/style.scss +10 -2
  7. package/dist/cjs/components/features-grid/client-logo-list/client-list.js +0 -12
  8. package/dist/cjs/components/features-grid/client-logo-list/client-list.js.map +1 -1
  9. package/dist/cjs/components/features-grid/index.js +9 -6
  10. package/dist/cjs/components/features-grid/index.js.map +1 -1
  11. package/dist/cjs/components/features-grid/plan-features-list.js +10 -3
  12. package/dist/cjs/components/features-grid/plan-features-list.js.map +1 -1
  13. package/dist/cjs/components/features-grid/plan-headers.js +2 -2
  14. package/dist/cjs/components/features-grid/plan-headers.js.map +1 -1
  15. package/dist/cjs/components/features-grid/plan-tagline.js +1 -1
  16. package/dist/cjs/components/features-grid/plan-tagline.js.map +1 -1
  17. package/dist/cjs/components/features-grid/style.scss +111 -21
  18. package/dist/cjs/components/features-grid/table.js +1 -1
  19. package/dist/cjs/components/features-grid/table.js.map +1 -1
  20. package/dist/cjs/components/features.js +43 -4
  21. package/dist/cjs/components/features.js.map +1 -1
  22. package/dist/cjs/components/item.js +1 -1
  23. package/dist/cjs/components/item.js.map +1 -1
  24. package/dist/cjs/components/plan-button/index.js +5 -3
  25. package/dist/cjs/components/plan-button/index.js.map +1 -1
  26. package/dist/cjs/components/plan-button/style.scss +71 -47
  27. package/dist/cjs/components/plan-div-td-container.js +4 -1
  28. package/dist/cjs/components/plan-div-td-container.js.map +1 -1
  29. package/dist/cjs/components/plan-logo.js +6 -3
  30. package/dist/cjs/components/plan-logo.js.map +1 -1
  31. package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
  32. package/dist/cjs/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
  33. package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js +4 -33
  34. package/dist/cjs/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
  35. package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
  36. package/dist/cjs/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
  37. package/dist/cjs/components/plans-2023-tooltip.js +16 -5
  38. package/dist/cjs/components/plans-2023-tooltip.js.map +1 -1
  39. package/dist/cjs/components/shared/action-button/index.js +22 -7
  40. package/dist/cjs/components/shared/action-button/index.js.map +1 -1
  41. package/dist/cjs/components/shared/action-button/style.scss +4 -0
  42. package/dist/cjs/components/shared/billing-timeframe/index.js +8 -4
  43. package/dist/cjs/components/shared/billing-timeframe/index.js.map +1 -1
  44. package/dist/cjs/components/shared/header-price/index.js +60 -15
  45. package/dist/cjs/components/shared/header-price/index.js.map +1 -1
  46. package/dist/cjs/components/shared/header-price/style.scss +10 -2
  47. package/dist/cjs/components/shared/storage/components/plan-storage.js +2 -2
  48. package/dist/cjs/components/shared/storage/components/plan-storage.js.map +1 -1
  49. package/dist/cjs/components/shared/storage/components/storage-dropdown.js +29 -6
  50. package/dist/cjs/components/shared/storage/components/storage-dropdown.js.map +1 -1
  51. package/dist/cjs/components/shared/storage/components/storage-feature-label.js +2 -1
  52. package/dist/cjs/components/shared/storage/components/storage-feature-label.js.map +1 -1
  53. package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js +2 -0
  54. package/dist/cjs/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
  55. package/dist/cjs/fixtures/sites-purchases.js +2 -4
  56. package/dist/cjs/fixtures/sites-purchases.js.map +1 -1
  57. package/dist/cjs/grid-context.js +4 -1
  58. package/dist/cjs/grid-context.js.map +1 -1
  59. package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js +50 -0
  60. package/dist/cjs/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
  61. package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
  62. package/dist/cjs/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
  63. package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
  64. package/dist/cjs/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
  65. package/dist/cjs/hooks/data-store/use-grid-plans.js +175 -21
  66. package/dist/cjs/hooks/data-store/use-grid-plans.js.map +1 -1
  67. package/dist/cjs/hooks/data-store/use-highlight-labels.js +13 -4
  68. package/dist/cjs/hooks/data-store/use-highlight-labels.js.map +1 -1
  69. package/dist/cjs/hooks/data-store/use-plan-billing-description.js +68 -13
  70. package/dist/cjs/hooks/data-store/use-plan-billing-description.js.map +1 -1
  71. package/dist/cjs/hooks/data-store/use-plan-billing-period.js +14 -0
  72. package/dist/cjs/hooks/data-store/use-plan-billing-period.js.map +1 -0
  73. package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js +76 -2
  74. package/dist/cjs/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
  75. package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +60 -12
  76. package/dist/cjs/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
  77. package/dist/cjs/hooks/data-store/use-title-badges.js +19 -0
  78. package/dist/cjs/hooks/data-store/use-title-badges.js.map +1 -0
  79. package/dist/cjs/hooks/use-grid-size.js.map +1 -1
  80. package/dist/cjs/hooks/use-is-large-currency.js +2 -2
  81. package/dist/cjs/hooks/use-is-large-currency.js.map +1 -1
  82. package/dist/cjs/hooks/use-visible-grid-plans.js +70 -0
  83. package/dist/cjs/hooks/use-visible-grid-plans.js.map +1 -0
  84. package/dist/cjs/index.js +8 -1
  85. package/dist/cjs/index.js.map +1 -1
  86. package/dist/cjs/lib/get-plan-features-object.js +15 -2
  87. package/dist/cjs/lib/get-plan-features-object.js.map +1 -1
  88. package/dist/cjs/lib/plan-pricing-utils.js +135 -0
  89. package/dist/cjs/lib/plan-pricing-utils.js.map +1 -0
  90. package/dist/esm/_shared.scss +4 -3
  91. package/dist/esm/components/comparison-grid/index.js +102 -72
  92. package/dist/esm/components/comparison-grid/index.js.map +1 -1
  93. package/dist/esm/components/comparison-grid/index.stories.js.map +1 -1
  94. package/dist/esm/components/comparison-grid/style.scss +10 -2
  95. package/dist/esm/components/features-grid/client-logo-list/client-list.js +0 -12
  96. package/dist/esm/components/features-grid/client-logo-list/client-list.js.map +1 -1
  97. package/dist/esm/components/features-grid/index.js +9 -6
  98. package/dist/esm/components/features-grid/index.js.map +1 -1
  99. package/dist/esm/components/features-grid/plan-features-list.js +10 -3
  100. package/dist/esm/components/features-grid/plan-features-list.js.map +1 -1
  101. package/dist/esm/components/features-grid/plan-headers.js +3 -3
  102. package/dist/esm/components/features-grid/plan-headers.js.map +1 -1
  103. package/dist/esm/components/features-grid/plan-tagline.js +1 -1
  104. package/dist/esm/components/features-grid/plan-tagline.js.map +1 -1
  105. package/dist/esm/components/features-grid/style.scss +111 -21
  106. package/dist/esm/components/features-grid/table.js +1 -1
  107. package/dist/esm/components/features-grid/table.js.map +1 -1
  108. package/dist/esm/components/features.js +44 -5
  109. package/dist/esm/components/features.js.map +1 -1
  110. package/dist/esm/components/item.js +1 -1
  111. package/dist/esm/components/item.js.map +1 -1
  112. package/dist/esm/components/plan-button/index.js +5 -3
  113. package/dist/esm/components/plan-button/index.js.map +1 -1
  114. package/dist/esm/components/plan-button/style.scss +71 -47
  115. package/dist/esm/components/plan-div-td-container.js +4 -1
  116. package/dist/esm/components/plan-div-td-container.js.map +1 -1
  117. package/dist/esm/components/plan-logo.js +7 -4
  118. package/dist/esm/components/plan-logo.js.map +1 -1
  119. package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js +12 -1
  120. package/dist/esm/components/plan-type-selector/components/interval-type-dropdown.js.map +1 -1
  121. package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js +3 -33
  122. package/dist/esm/components/plan-type-selector/hooks/use-max-discount.js.map +1 -1
  123. package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js +11 -13
  124. package/dist/esm/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.js.map +1 -1
  125. package/dist/esm/components/plans-2023-tooltip.js +16 -5
  126. package/dist/esm/components/plans-2023-tooltip.js.map +1 -1
  127. package/dist/esm/components/shared/action-button/index.js +22 -7
  128. package/dist/esm/components/shared/action-button/index.js.map +1 -1
  129. package/dist/esm/components/shared/action-button/style.scss +4 -0
  130. package/dist/esm/components/shared/billing-timeframe/index.js +8 -4
  131. package/dist/esm/components/shared/billing-timeframe/index.js.map +1 -1
  132. package/dist/esm/components/shared/header-price/index.js +60 -15
  133. package/dist/esm/components/shared/header-price/index.js.map +1 -1
  134. package/dist/esm/components/shared/header-price/style.scss +10 -2
  135. package/dist/esm/components/shared/storage/components/plan-storage.js +2 -2
  136. package/dist/esm/components/shared/storage/components/plan-storage.js.map +1 -1
  137. package/dist/esm/components/shared/storage/components/storage-dropdown.js +30 -7
  138. package/dist/esm/components/shared/storage/components/storage-dropdown.js.map +1 -1
  139. package/dist/esm/components/shared/storage/components/storage-feature-label.js +2 -1
  140. package/dist/esm/components/shared/storage/components/storage-feature-label.js.map +1 -1
  141. package/dist/esm/components/shared/storage/hooks/use-plan-storage.js +3 -1
  142. package/dist/esm/components/shared/storage/hooks/use-plan-storage.js.map +1 -1
  143. package/dist/esm/fixtures/sites-purchases.js +2 -4
  144. package/dist/esm/fixtures/sites-purchases.js.map +1 -1
  145. package/dist/esm/grid-context.js +4 -1
  146. package/dist/esm/grid-context.js.map +1 -1
  147. package/dist/esm/hooks/data-store/get-renewal-pricing-text.js +47 -0
  148. package/dist/esm/hooks/data-store/get-renewal-pricing-text.js.map +1 -0
  149. package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js +6 -1
  150. package/dist/esm/hooks/data-store/use-grid-plans-for-comparison-grid.js.map +1 -1
  151. package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js +6 -1
  152. package/dist/esm/hooks/data-store/use-grid-plans-for-features-grid.js.map +1 -1
  153. package/dist/esm/hooks/data-store/use-grid-plans.js +176 -22
  154. package/dist/esm/hooks/data-store/use-grid-plans.js.map +1 -1
  155. package/dist/esm/hooks/data-store/use-highlight-labels.js +14 -5
  156. package/dist/esm/hooks/data-store/use-highlight-labels.js.map +1 -1
  157. package/dist/esm/hooks/data-store/use-plan-billing-description.js +66 -11
  158. package/dist/esm/hooks/data-store/use-plan-billing-description.js.map +1 -1
  159. package/dist/esm/hooks/data-store/use-plan-billing-period.js +12 -0
  160. package/dist/esm/hooks/data-store/use-plan-billing-period.js.map +1 -0
  161. package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js +77 -3
  162. package/dist/esm/hooks/data-store/use-plan-features-for-grid-plans.js.map +1 -1
  163. package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js +59 -11
  164. package/dist/esm/hooks/data-store/use-restructured-plan-features-for-comparison-grid.js.map +1 -1
  165. package/dist/esm/hooks/data-store/use-title-badges.js +17 -0
  166. package/dist/esm/hooks/data-store/use-title-badges.js.map +1 -0
  167. package/dist/esm/hooks/use-grid-size.js.map +1 -1
  168. package/dist/esm/hooks/use-is-large-currency.js +1 -1
  169. package/dist/esm/hooks/use-is-large-currency.js.map +1 -1
  170. package/dist/esm/hooks/use-visible-grid-plans.js +66 -0
  171. package/dist/esm/hooks/use-visible-grid-plans.js.map +1 -0
  172. package/dist/esm/index.js +4 -1
  173. package/dist/esm/index.js.map +1 -1
  174. package/dist/esm/lib/get-plan-features-object.js +15 -2
  175. package/dist/esm/lib/get-plan-features-object.js.map +1 -1
  176. package/dist/esm/lib/plan-pricing-utils.js +129 -0
  177. package/dist/esm/lib/plan-pricing-utils.js.map +1 -0
  178. package/dist/tsconfig-cjs.tsbuildinfo +1 -1
  179. package/dist/tsconfig.tsbuildinfo +1 -1
  180. package/dist/types/components/comparison-grid/index.d.ts +1 -1
  181. package/dist/types/components/comparison-grid/index.d.ts.map +1 -1
  182. package/dist/types/components/comparison-grid/index.stories.d.ts +2 -2
  183. package/dist/types/components/dropdown-option.d.ts.map +1 -1
  184. package/dist/types/components/features-grid/billing-timeframes.d.ts.map +1 -1
  185. package/dist/types/components/features-grid/client-logo-list/client-list.d.ts.map +1 -1
  186. package/dist/types/components/features-grid/client-logo-list/index.d.ts.map +1 -1
  187. package/dist/types/components/features-grid/enterprise-features.d.ts.map +1 -1
  188. package/dist/types/components/features-grid/index.d.ts.map +1 -1
  189. package/dist/types/components/features-grid/plan-features-list.d.ts.map +1 -1
  190. package/dist/types/components/features-grid/plan-headers.d.ts +2 -0
  191. package/dist/types/components/features-grid/plan-headers.d.ts.map +1 -1
  192. package/dist/types/components/features-grid/plan-logos.d.ts.map +1 -1
  193. package/dist/types/components/features-grid/plan-prices.d.ts.map +1 -1
  194. package/dist/types/components/features-grid/plan-tagline.d.ts.map +1 -1
  195. package/dist/types/components/features-grid/previous-features-included-title.d.ts.map +1 -1
  196. package/dist/types/components/features-grid/spotlight-plan.d.ts.map +1 -1
  197. package/dist/types/components/features-grid/table.d.ts.map +1 -1
  198. package/dist/types/components/features-grid/top-buttons.d.ts.map +1 -1
  199. package/dist/types/components/features.d.ts.map +1 -1
  200. package/dist/types/components/item.d.ts +2 -1
  201. package/dist/types/components/item.d.ts.map +1 -1
  202. package/dist/types/components/plan-button/index.d.ts +2 -1
  203. package/dist/types/components/plan-button/index.d.ts.map +1 -1
  204. package/dist/types/components/plan-div-td-container.d.ts +2 -0
  205. package/dist/types/components/plan-div-td-container.d.ts.map +1 -1
  206. package/dist/types/components/plan-logo.d.ts.map +1 -1
  207. package/dist/types/components/plan-type-selector/components/interval-type-dropdown.d.ts.map +1 -1
  208. package/dist/types/components/plan-type-selector/hooks/use-max-discount.d.ts.map +1 -1
  209. package/dist/types/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.d.ts.map +1 -1
  210. package/dist/types/components/plans-2023-tooltip.d.ts.map +1 -1
  211. package/dist/types/components/shared/action-button/index.d.ts +2 -1
  212. package/dist/types/components/shared/action-button/index.d.ts.map +1 -1
  213. package/dist/types/components/shared/billing-timeframe/index.d.ts.map +1 -1
  214. package/dist/types/components/shared/header-price/header-price-context.d.ts.map +1 -1
  215. package/dist/types/components/shared/header-price/index.d.ts.map +1 -1
  216. package/dist/types/components/shared/storage/components/plan-storage.d.ts.map +1 -1
  217. package/dist/types/components/shared/storage/components/storage-dropdown.d.ts.map +1 -1
  218. package/dist/types/components/shared/storage/components/storage-feature-label.d.ts.map +1 -1
  219. package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts +1 -1
  220. package/dist/types/components/shared/storage/hooks/use-plan-storage.d.ts.map +1 -1
  221. package/dist/types/css-mixins.d.ts.map +1 -1
  222. package/dist/types/fixtures/sites-purchases.d.ts +2 -4
  223. package/dist/types/fixtures/sites-purchases.d.ts.map +1 -1
  224. package/dist/types/grid-context.d.ts +4 -1
  225. package/dist/types/grid-context.d.ts.map +1 -1
  226. package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts +14 -0
  227. package/dist/types/hooks/data-store/get-renewal-pricing-text.d.ts.map +1 -0
  228. package/dist/types/hooks/data-store/types.d.ts +21 -0
  229. package/dist/types/hooks/data-store/types.d.ts.map +1 -1
  230. package/dist/types/hooks/data-store/use-grid-plan-for-spotlight.d.ts.map +1 -1
  231. package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts +1 -1
  232. package/dist/types/hooks/data-store/use-grid-plans-for-comparison-grid.d.ts.map +1 -1
  233. package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts +1 -1
  234. package/dist/types/hooks/data-store/use-grid-plans-for-features-grid.d.ts.map +1 -1
  235. package/dist/types/hooks/data-store/use-grid-plans.d.ts.map +1 -1
  236. package/dist/types/hooks/data-store/use-highlight-labels.d.ts.map +1 -1
  237. package/dist/types/hooks/data-store/use-plan-billing-description.d.ts.map +1 -1
  238. package/dist/types/hooks/data-store/use-plan-billing-period.d.ts +8 -0
  239. package/dist/types/hooks/data-store/use-plan-billing-period.d.ts.map +1 -0
  240. package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts +4 -1
  241. package/dist/types/hooks/data-store/use-plan-features-for-grid-plans.d.ts.map +1 -1
  242. package/dist/types/hooks/data-store/use-plans-from-types.d.ts.map +1 -1
  243. package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts +4 -1
  244. package/dist/types/hooks/data-store/use-restructured-plan-features-for-comparison-grid.d.ts.map +1 -1
  245. package/dist/types/hooks/data-store/use-title-badges.d.ts +9 -0
  246. package/dist/types/hooks/data-store/use-title-badges.d.ts.map +1 -0
  247. package/dist/types/hooks/use-grid-size.d.ts +3 -2
  248. package/dist/types/hooks/use-grid-size.d.ts.map +1 -1
  249. package/dist/types/hooks/use-highlight-adjacency-matrix.d.ts.map +1 -1
  250. package/dist/types/hooks/use-visible-grid-plans.d.ts +14 -0
  251. package/dist/types/hooks/use-visible-grid-plans.d.ts.map +1 -0
  252. package/dist/types/index.d.ts +9 -1
  253. package/dist/types/index.d.ts.map +1 -1
  254. package/dist/types/lib/filter-unused-features-object.d.ts.map +1 -1
  255. package/dist/types/lib/get-plan-features-object.d.ts +1 -1
  256. package/dist/types/lib/get-plan-features-object.d.ts.map +1 -1
  257. package/dist/types/lib/plan-pricing-utils.d.ts +105 -0
  258. package/dist/types/lib/plan-pricing-utils.d.ts.map +1 -0
  259. package/dist/types/types.d.ts +33 -6
  260. package/dist/types/types.d.ts.map +1 -1
  261. package/package.json +39 -28
  262. package/src/_shared.scss +4 -3
  263. package/src/components/comparison-grid/index.stories.tsx +1 -1
  264. package/src/components/comparison-grid/index.tsx +263 -158
  265. package/src/components/comparison-grid/style.scss +10 -2
  266. package/src/components/features-grid/client-logo-list/client-list.tsx +0 -25
  267. package/src/components/features-grid/index.tsx +37 -19
  268. package/src/components/features-grid/plan-features-list.tsx +15 -4
  269. package/src/components/features-grid/plan-headers.tsx +10 -3
  270. package/src/components/features-grid/plan-tagline.tsx +1 -1
  271. package/src/components/features-grid/style.scss +111 -21
  272. package/src/components/features-grid/table.tsx +4 -2
  273. package/src/components/features.tsx +66 -6
  274. package/src/components/item.tsx +6 -3
  275. package/src/components/plan-button/index.tsx +7 -1
  276. package/src/components/plan-button/style.scss +71 -47
  277. package/src/components/plan-div-td-container.tsx +6 -2
  278. package/src/components/plan-logo.tsx +16 -9
  279. package/src/components/plan-type-selector/components/interval-type-dropdown.tsx +14 -1
  280. package/src/components/plan-type-selector/hooks/use-max-discount.ts +8 -47
  281. package/src/components/plan-type-selector/hooks/use-max-discounts-for-plan-terms.ts +19 -17
  282. package/src/components/plans-2023-tooltip.tsx +17 -5
  283. package/src/components/shared/action-button/index.tsx +46 -5
  284. package/src/components/shared/action-button/style.scss +4 -0
  285. package/src/components/shared/billing-timeframe/index.tsx +12 -7
  286. package/src/components/shared/header-price/index.tsx +129 -27
  287. package/src/components/shared/header-price/style.scss +10 -2
  288. package/src/components/shared/storage/components/plan-storage.tsx +2 -2
  289. package/src/components/shared/storage/components/storage-dropdown.tsx +36 -15
  290. package/src/components/shared/storage/components/storage-feature-label.tsx +2 -1
  291. package/src/components/shared/storage/hooks/use-plan-storage.ts +3 -0
  292. package/src/components/test/actions-button.tsx +5 -0
  293. package/src/components/test/billing-timeframe.tsx +1 -1
  294. package/src/components/test/header-price.tsx +342 -4
  295. package/src/fixtures/sites-purchases.ts +2 -4
  296. package/src/grid-context.tsx +9 -0
  297. package/src/hooks/data-store/get-renewal-pricing-text.ts +73 -0
  298. package/src/hooks/data-store/types.ts +21 -0
  299. package/src/hooks/data-store/use-grid-plans-for-comparison-grid.ts +10 -0
  300. package/src/hooks/data-store/use-grid-plans-for-features-grid.ts +10 -0
  301. package/src/hooks/data-store/use-grid-plans.tsx +189 -23
  302. package/src/hooks/data-store/use-highlight-labels.ts +12 -3
  303. package/src/hooks/data-store/use-plan-billing-description.tsx +80 -15
  304. package/src/hooks/data-store/use-plan-billing-period.tsx +28 -0
  305. package/src/hooks/data-store/use-plan-features-for-grid-plans.ts +135 -1
  306. package/src/hooks/data-store/use-restructured-plan-features-for-comparison-grid.ts +93 -20
  307. package/src/hooks/data-store/use-title-badges.ts +31 -0
  308. package/src/hooks/test/use-visible-grid-plans.tsx +116 -0
  309. package/src/hooks/use-grid-size.ts +3 -2
  310. package/src/hooks/use-is-large-currency.ts +1 -1
  311. package/src/hooks/use-visible-grid-plans.tsx +102 -0
  312. package/src/index.tsx +20 -0
  313. package/src/lib/get-plan-features-object.ts +23 -2
  314. package/src/lib/plan-pricing-utils.ts +211 -0
  315. package/src/lib/test/plan-pricing-utils.ts +594 -0
  316. package/src/style-imports.d.ts +3 -0
  317. package/src/types.ts +45 -4
  318. package/dist/cjs/components/features-grid/mobile-free-domain.js +0 -25
  319. package/dist/cjs/components/features-grid/mobile-free-domain.js.map +0 -1
  320. package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js +0 -15
  321. package/dist/cjs/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
  322. package/dist/cjs/lib/sort-plan-properties.js +0 -26
  323. package/dist/cjs/lib/sort-plan-properties.js.map +0 -1
  324. package/dist/esm/components/features-grid/mobile-free-domain.js +0 -23
  325. package/dist/esm/components/features-grid/mobile-free-domain.js.map +0 -1
  326. package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js +0 -12
  327. package/dist/esm/lib/get-plan-pricing-info-from-grid-plans.js.map +0 -1
  328. package/dist/esm/lib/sort-plan-properties.js +0 -23
  329. package/dist/esm/lib/sort-plan-properties.js.map +0 -1
  330. package/dist/types/components/features-grid/mobile-free-domain.d.ts +0 -8
  331. package/dist/types/components/features-grid/mobile-free-domain.d.ts.map +0 -1
  332. package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts +0 -9
  333. package/dist/types/lib/get-plan-pricing-info-from-grid-plans.d.ts.map +0 -1
  334. package/dist/types/lib/sort-plan-properties.d.ts +0 -3
  335. package/dist/types/lib/sort-plan-properties.d.ts.map +0 -1
  336. package/src/components/features-grid/mobile-free-domain.tsx +0 -51
  337. package/src/lib/get-plan-pricing-info-from-grid-plans.ts +0 -31
  338. package/src/lib/sort-plan-properties.ts +0 -27
  339. package/src/lib/test/sort-plan-properties.ts +0 -122
@@ -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';