@softure-ai/billing 0.0.0-stage → 0.1.5

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 (294) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +699 -2
  3. package/dist/calendar.d.ts +26 -0
  4. package/dist/calendar.d.ts.map +1 -0
  5. package/dist/calendar.js +81 -0
  6. package/dist/calendar.js.map +1 -0
  7. package/dist/contract.d.ts +187 -0
  8. package/dist/contract.d.ts.map +1 -0
  9. package/dist/contract.js +6 -0
  10. package/dist/contract.js.map +1 -0
  11. package/dist/currency-digits.d.ts +3 -0
  12. package/dist/currency-digits.d.ts.map +1 -0
  13. package/dist/currency-digits.js +32 -0
  14. package/dist/currency-digits.js.map +1 -0
  15. package/dist/entitlement.d.ts +25 -0
  16. package/dist/entitlement.d.ts.map +1 -0
  17. package/dist/entitlement.js +75 -0
  18. package/dist/entitlement.js.map +1 -0
  19. package/dist/fields.d.ts +25 -0
  20. package/dist/fields.d.ts.map +1 -0
  21. package/dist/fields.js +27 -0
  22. package/dist/fields.js.map +1 -0
  23. package/dist/index.d.ts +274 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +73 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/invoice.d.ts +29 -0
  28. package/dist/invoice.d.ts.map +1 -0
  29. package/dist/invoice.js +35 -0
  30. package/dist/invoice.js.map +1 -0
  31. package/dist/mailing/index.d.ts +2 -0
  32. package/dist/mailing/index.d.ts.map +1 -0
  33. package/dist/mailing/index.js +4 -0
  34. package/dist/mailing/index.js.map +1 -0
  35. package/dist/mailing/reminder-mail.d.ts +50 -0
  36. package/dist/mailing/reminder-mail.d.ts.map +1 -0
  37. package/dist/mailing/reminder-mail.js +71 -0
  38. package/dist/mailing/reminder-mail.js.map +1 -0
  39. package/dist/manual.d.ts +12 -0
  40. package/dist/manual.d.ts.map +1 -0
  41. package/dist/manual.js +22 -0
  42. package/dist/manual.js.map +1 -0
  43. package/dist/messages/en.d.ts +176 -0
  44. package/dist/messages/en.d.ts.map +1 -0
  45. package/dist/messages/en.js +151 -0
  46. package/dist/messages/en.js.map +1 -0
  47. package/dist/messages/index.d.ts +359 -0
  48. package/dist/messages/index.d.ts.map +1 -0
  49. package/dist/messages/index.js +14 -0
  50. package/dist/messages/index.js.map +1 -0
  51. package/dist/messages/pl.d.ts +3 -0
  52. package/dist/messages/pl.d.ts.map +1 -0
  53. package/dist/messages/pl.js +151 -0
  54. package/dist/messages/pl.js.map +1 -0
  55. package/dist/next/access.d.ts +16 -0
  56. package/dist/next/access.d.ts.map +1 -0
  57. package/dist/next/access.js +24 -0
  58. package/dist/next/access.js.map +1 -0
  59. package/dist/next/actions.d.ts +24 -0
  60. package/dist/next/actions.d.ts.map +1 -0
  61. package/dist/next/actions.js +158 -0
  62. package/dist/next/actions.js.map +1 -0
  63. package/dist/next/context.d.ts +4 -0
  64. package/dist/next/context.d.ts.map +1 -0
  65. package/dist/next/context.js +17 -0
  66. package/dist/next/context.js.map +1 -0
  67. package/dist/next/current-entitlement.d.ts +18 -0
  68. package/dist/next/current-entitlement.d.ts.map +1 -0
  69. package/dist/next/current-entitlement.js +26 -0
  70. package/dist/next/current-entitlement.js.map +1 -0
  71. package/dist/next/index.d.ts +8 -0
  72. package/dist/next/index.d.ts.map +1 -0
  73. package/dist/next/index.js +12 -0
  74. package/dist/next/index.js.map +1 -0
  75. package/dist/next/pages.d.ts +24 -0
  76. package/dist/next/pages.d.ts.map +1 -0
  77. package/dist/next/pages.js +166 -0
  78. package/dist/next/pages.js.map +1 -0
  79. package/dist/next/pricing.d.ts +12 -0
  80. package/dist/next/pricing.d.ts.map +1 -0
  81. package/dist/next/pricing.js +20 -0
  82. package/dist/next/pricing.js.map +1 -0
  83. package/dist/next/route.d.ts +11 -0
  84. package/dist/next/route.d.ts.map +1 -0
  85. package/dist/next/route.js +56 -0
  86. package/dist/next/route.js.map +1 -0
  87. package/dist/options.d.ts +85 -0
  88. package/dist/options.d.ts.map +1 -0
  89. package/dist/options.js +121 -0
  90. package/dist/options.js.map +1 -0
  91. package/dist/payment.d.ts +61 -0
  92. package/dist/payment.d.ts.map +1 -0
  93. package/dist/payment.js +12 -0
  94. package/dist/payment.js.map +1 -0
  95. package/dist/plans.d.ts +20 -0
  96. package/dist/plans.d.ts.map +1 -0
  97. package/dist/plans.js +53 -0
  98. package/dist/plans.js.map +1 -0
  99. package/dist/price.d.ts +12 -0
  100. package/dist/price.d.ts.map +1 -0
  101. package/dist/price.js +37 -0
  102. package/dist/price.js.map +1 -0
  103. package/dist/refund.d.ts +70 -0
  104. package/dist/refund.d.ts.map +1 -0
  105. package/dist/refund.js +109 -0
  106. package/dist/refund.js.map +1 -0
  107. package/dist/reminder.d.ts +30 -0
  108. package/dist/reminder.d.ts.map +1 -0
  109. package/dist/reminder.js +34 -0
  110. package/dist/reminder.js.map +1 -0
  111. package/dist/schema.d.ts +1023 -0
  112. package/dist/schema.d.ts.map +1 -0
  113. package/dist/schema.js +77 -0
  114. package/dist/schema.js.map +1 -0
  115. package/dist/scripts/entitlement-scripts.d.ts +17 -0
  116. package/dist/scripts/entitlement-scripts.d.ts.map +1 -0
  117. package/dist/scripts/entitlement-scripts.js +167 -0
  118. package/dist/scripts/entitlement-scripts.js.map +1 -0
  119. package/dist/scripts/index.d.ts +3 -0
  120. package/dist/scripts/index.d.ts.map +1 -0
  121. package/dist/scripts/index.js +5 -0
  122. package/dist/scripts/index.js.map +1 -0
  123. package/dist/scripts/plan-scripts.d.ts +19 -0
  124. package/dist/scripts/plan-scripts.d.ts.map +1 -0
  125. package/dist/scripts/plan-scripts.js +106 -0
  126. package/dist/scripts/plan-scripts.js.map +1 -0
  127. package/dist/server/entitlements.d.ts +69 -0
  128. package/dist/server/entitlements.d.ts.map +1 -0
  129. package/dist/server/entitlements.js +202 -0
  130. package/dist/server/entitlements.js.map +1 -0
  131. package/dist/server/grants.d.ts +74 -0
  132. package/dist/server/grants.d.ts.map +1 -0
  133. package/dist/server/grants.js +174 -0
  134. package/dist/server/grants.js.map +1 -0
  135. package/dist/server/health.d.ts +3 -0
  136. package/dist/server/health.d.ts.map +1 -0
  137. package/dist/server/health.js +17 -0
  138. package/dist/server/health.js.map +1 -0
  139. package/dist/server/index.d.ts +12 -0
  140. package/dist/server/index.d.ts.map +1 -0
  141. package/dist/server/index.js +14 -0
  142. package/dist/server/index.js.map +1 -0
  143. package/dist/server/options.d.ts +20 -0
  144. package/dist/server/options.d.ts.map +1 -0
  145. package/dist/server/options.js +38 -0
  146. package/dist/server/options.js.map +1 -0
  147. package/dist/server/payments.d.ts +140 -0
  148. package/dist/server/payments.d.ts.map +1 -0
  149. package/dist/server/payments.js +339 -0
  150. package/dist/server/payments.js.map +1 -0
  151. package/dist/server/plans.d.ts +50 -0
  152. package/dist/server/plans.d.ts.map +1 -0
  153. package/dist/server/plans.js +129 -0
  154. package/dist/server/plans.js.map +1 -0
  155. package/dist/server/privacy.d.ts +77 -0
  156. package/dist/server/privacy.d.ts.map +1 -0
  157. package/dist/server/privacy.js +110 -0
  158. package/dist/server/privacy.js.map +1 -0
  159. package/dist/server/reminders.d.ts +20 -0
  160. package/dist/server/reminders.d.ts.map +1 -0
  161. package/dist/server/reminders.js +85 -0
  162. package/dist/server/reminders.js.map +1 -0
  163. package/dist/server/requests.d.ts +80 -0
  164. package/dist/server/requests.d.ts.map +1 -0
  165. package/dist/server/requests.js +155 -0
  166. package/dist/server/requests.js.map +1 -0
  167. package/dist/server/setup.d.ts +9 -0
  168. package/dist/server/setup.d.ts.map +1 -0
  169. package/dist/server/setup.js +34 -0
  170. package/dist/server/setup.js.map +1 -0
  171. package/dist/server/take-back.d.ts +80 -0
  172. package/dist/server/take-back.d.ts.map +1 -0
  173. package/dist/server/take-back.js +138 -0
  174. package/dist/server/take-back.js.map +1 -0
  175. package/dist/server/user-id.d.ts +4 -0
  176. package/dist/server/user-id.d.ts.map +1 -0
  177. package/dist/server/user-id.js +11 -0
  178. package/dist/server/user-id.js.map +1 -0
  179. package/dist/stripe-currency.d.ts +27 -0
  180. package/dist/stripe-currency.d.ts.map +1 -0
  181. package/dist/stripe-currency.js +57 -0
  182. package/dist/stripe-currency.js.map +1 -0
  183. package/dist/stripe-webhook.d.ts +101 -0
  184. package/dist/stripe-webhook.d.ts.map +1 -0
  185. package/dist/stripe-webhook.js +209 -0
  186. package/dist/stripe-webhook.js.map +1 -0
  187. package/dist/stripe.d.ts +25 -0
  188. package/dist/stripe.d.ts.map +1 -0
  189. package/dist/stripe.js +116 -0
  190. package/dist/stripe.js.map +1 -0
  191. package/dist/ui/access-badge.d.ts +16 -0
  192. package/dist/ui/access-badge.d.ts.map +1 -0
  193. package/dist/ui/access-badge.js +43 -0
  194. package/dist/ui/access-badge.js.map +1 -0
  195. package/dist/ui/access-notice.d.ts +20 -0
  196. package/dist/ui/access-notice.d.ts.map +1 -0
  197. package/dist/ui/access-notice.js +41 -0
  198. package/dist/ui/access-notice.js.map +1 -0
  199. package/dist/ui/format.d.ts +11 -0
  200. package/dist/ui/format.d.ts.map +1 -0
  201. package/dist/ui/format.js +21 -0
  202. package/dist/ui/format.js.map +1 -0
  203. package/dist/ui/grant-form.d.ts +18 -0
  204. package/dist/ui/grant-form.d.ts.map +1 -0
  205. package/dist/ui/grant-form.js +23 -0
  206. package/dist/ui/grant-form.js.map +1 -0
  207. package/dist/ui/grant-history.d.ts +39 -0
  208. package/dist/ui/grant-history.d.ts.map +1 -0
  209. package/dist/ui/grant-history.js +36 -0
  210. package/dist/ui/grant-history.js.map +1 -0
  211. package/dist/ui/index.d.ts +9 -0
  212. package/dist/ui/index.d.ts.map +1 -0
  213. package/dist/ui/index.js +12 -0
  214. package/dist/ui/index.js.map +1 -0
  215. package/dist/ui/payment-form.d.ts +23 -0
  216. package/dist/ui/payment-form.d.ts.map +1 -0
  217. package/dist/ui/payment-form.js +34 -0
  218. package/dist/ui/payment-form.js.map +1 -0
  219. package/dist/ui/payment-requests.d.ts +30 -0
  220. package/dist/ui/payment-requests.d.ts.map +1 -0
  221. package/dist/ui/payment-requests.js +31 -0
  222. package/dist/ui/payment-requests.js.map +1 -0
  223. package/dist/ui/pricing-tiles.d.ts +22 -0
  224. package/dist/ui/pricing-tiles.d.ts.map +1 -0
  225. package/dist/ui/pricing-tiles.js +40 -0
  226. package/dist/ui/pricing-tiles.js.map +1 -0
  227. package/migrations/0001_create_entitlements.sql +16 -0
  228. package/migrations/0002_create_payments.sql +30 -0
  229. package/migrations/0003_record_payment_grants.sql +25 -0
  230. package/migrations/0004_create_requests_and_grants.sql +58 -0
  231. package/migrations/0005_record_refunded_amounts.sql +18 -0
  232. package/migrations/0006_record_request_handover_and_prices.sql +38 -0
  233. package/migrations/0007_record_failed_refunds.sql +27 -0
  234. package/migrations/0008_record_request_handover_claims.sql +9 -0
  235. package/migrations/0009_record_pending_charge_states.sql +16 -0
  236. package/module.json +23 -0
  237. package/package.json +81 -4
  238. package/src/calendar.ts +90 -0
  239. package/src/contract.ts +181 -0
  240. package/src/currency-digits.ts +37 -0
  241. package/src/entitlement.ts +84 -0
  242. package/src/fields.ts +37 -0
  243. package/src/index.ts +163 -0
  244. package/src/invoice.ts +58 -0
  245. package/src/mailing/index.ts +11 -0
  246. package/src/mailing/reminder-mail.ts +108 -0
  247. package/src/manual.ts +31 -0
  248. package/src/messages/en.ts +150 -0
  249. package/src/messages/index.ts +18 -0
  250. package/src/messages/pl.ts +152 -0
  251. package/src/next/access.tsx +55 -0
  252. package/src/next/actions.ts +176 -0
  253. package/src/next/context.ts +18 -0
  254. package/src/next/current-entitlement.ts +36 -0
  255. package/src/next/index.ts +11 -0
  256. package/src/next/next-modules.d.ts +21 -0
  257. package/src/next/pages.tsx +267 -0
  258. package/src/next/pricing.tsx +39 -0
  259. package/src/next/route.ts +57 -0
  260. package/src/options.ts +128 -0
  261. package/src/payment.ts +77 -0
  262. package/src/plans.ts +63 -0
  263. package/src/price.ts +50 -0
  264. package/src/refund.ts +135 -0
  265. package/src/reminder.ts +52 -0
  266. package/src/schema.ts +86 -0
  267. package/src/scripts/entitlement-scripts.ts +188 -0
  268. package/src/scripts/index.ts +11 -0
  269. package/src/scripts/plan-scripts.ts +143 -0
  270. package/src/server/entitlements.ts +227 -0
  271. package/src/server/grants.ts +227 -0
  272. package/src/server/health.ts +18 -0
  273. package/src/server/index.ts +83 -0
  274. package/src/server/options.ts +52 -0
  275. package/src/server/payments.ts +434 -0
  276. package/src/server/plans.ts +143 -0
  277. package/src/server/privacy.ts +189 -0
  278. package/src/server/reminders.ts +113 -0
  279. package/src/server/requests.ts +201 -0
  280. package/src/server/setup.ts +37 -0
  281. package/src/server/take-back.ts +190 -0
  282. package/src/server/user-id.ts +12 -0
  283. package/src/stripe-currency.ts +65 -0
  284. package/src/stripe-webhook.ts +278 -0
  285. package/src/stripe.ts +130 -0
  286. package/src/ui/access-badge.tsx +64 -0
  287. package/src/ui/access-notice.tsx +73 -0
  288. package/src/ui/format.ts +25 -0
  289. package/src/ui/grant-form.tsx +69 -0
  290. package/src/ui/grant-history.tsx +127 -0
  291. package/src/ui/index.ts +25 -0
  292. package/src/ui/payment-form.tsx +111 -0
  293. package/src/ui/payment-requests.tsx +126 -0
  294. package/src/ui/pricing-tiles.tsx +105 -0
@@ -0,0 +1,101 @@
1
+ import { type Err, type Ok } from "@softure-ai/core";
2
+ /** The header Stripe signs every delivery with (lower case, as `Headers` returns it). */
3
+ export declare const STRIPE_SIGNATURE_HEADER = "stripe-signature";
4
+ /** How far a delivery's timestamp may be from now, in seconds: Stripe's default replay window. */
5
+ export declare const STRIPE_SIGNATURE_TOLERANCE_SECONDS = 300;
6
+ /** The metadata keys a checkout session carries (`stripe()` sets them). */
7
+ export declare const STRIPE_METADATA: {
8
+ readonly userId: "softure_user_id";
9
+ readonly planId: "softure_plan_id";
10
+ };
11
+ export type StripeWebhookError = "billing.webhook_invalid";
12
+ export interface SignStripePayloadInput {
13
+ readonly payload: string;
14
+ readonly secret: string;
15
+ /** Unix seconds. */
16
+ readonly timestamp: number;
17
+ }
18
+ /** A `Stripe-Signature` value as Stripe sends it, e.g. for a test that plays Stripe. */
19
+ export declare function signStripePayload({ payload, secret, timestamp }: SignStripePayloadInput): string;
20
+ export interface VerifyStripeSignatureInput {
21
+ readonly payload: string;
22
+ /** The `Stripe-Signature` header, or null when the request had none. */
23
+ readonly header: string | null;
24
+ readonly secret: string;
25
+ readonly now: Date;
26
+ readonly toleranceSeconds?: number;
27
+ }
28
+ /**
29
+ * Whether Stripe signed `payload` with `secret` within the tolerance of `now`. Any `v1` entry may
30
+ * match: Stripe sends one per secret while an old secret is still rolling off.
31
+ */
32
+ export declare function verifyStripeSignature(input: VerifyStripeSignatureInput): Ok<undefined> | Err<StripeWebhookError>;
33
+ /** A checkout Stripe reports as paid, with what `stripe()` put in its metadata. */
34
+ export interface PaidCheckout {
35
+ /** The Checkout Session id (`cs_...`). */
36
+ readonly checkoutId: string;
37
+ /** The PaymentIntent id (`pi_...`) refunds name; null for a checkout without a charge. */
38
+ readonly paymentId: string | null;
39
+ readonly userId: string;
40
+ readonly planId: string;
41
+ /** What Stripe charged, in billing's unit (the pinned minor unit, converted from Stripe's). */
42
+ readonly amount: number;
43
+ /** ISO 4217, upper case. */
44
+ readonly currency: string;
45
+ }
46
+ /** A refund Stripe reports as failed or canceled: its money is back with the customer's payment. */
47
+ export interface FailedRefund {
48
+ /** The PaymentIntent id (`pi_...`) of the refunded payment. */
49
+ readonly paymentId: string;
50
+ /** The Refund id (`re_...`). */
51
+ readonly refundId: string;
52
+ /** What the refund was for, in billing's unit. */
53
+ readonly amount: number;
54
+ /** When Stripe created the refund. */
55
+ readonly refundCreatedAt: Date;
56
+ /** When Stripe reported the failure (the event's `created`). */
57
+ readonly failedAt: Date;
58
+ }
59
+ /** What one delivery asks of billing. */
60
+ export type StripeWebhookEvent =
61
+ /** A paid checkout: grant its plan, once. */
62
+ {
63
+ readonly type: "checkout_paid";
64
+ readonly eventId: string;
65
+ readonly checkout: PaidCheckout;
66
+ }
67
+ /** A charge refunded in full: take back the access its payment gave, once. `snapshotAt`: when Stripe took the charge's state (the event's `created`). */
68
+ | {
69
+ readonly type: "payment_refunded";
70
+ readonly eventId: string;
71
+ readonly paymentId: string;
72
+ readonly snapshotAt: Date;
73
+ }
74
+ /** A charge refunded in part: `amountRefunded` is the total refunded so far, in billing's unit, as of `snapshotAt`. */
75
+ | {
76
+ readonly type: "payment_partially_refunded";
77
+ readonly eventId: string;
78
+ readonly paymentId: string;
79
+ readonly amountRefunded: number;
80
+ readonly snapshotAt: Date;
81
+ }
82
+ /** A refund that failed (or was canceled): give back what it took, once per refund. */
83
+ | {
84
+ readonly type: "refund_failed";
85
+ readonly eventId: string;
86
+ readonly failure: FailedRefund;
87
+ }
88
+ /** Nothing to do: another event type, a checkout still waiting for its money, a charge with nothing refunded, a session billing did not create. */
89
+ | {
90
+ readonly type: "ignored";
91
+ readonly eventId: string;
92
+ readonly reason: string;
93
+ };
94
+ /**
95
+ * The event a verified payload carries. A body that is not a Stripe event, or an event billing acts
96
+ * on whose object lacks what billing needs, is `billing.webhook_invalid`.
97
+ */
98
+ export declare function parseStripeEvent(payload: string): Ok<StripeWebhookEvent> | Err<StripeWebhookError>;
99
+ /** `verifyStripeSignature`, then `parseStripeEvent`: nothing unsigned is parsed. */
100
+ export declare function readStripeWebhook(input: VerifyStripeSignatureInput): Ok<StripeWebhookEvent> | Err<StripeWebhookError>;
101
+ //# sourceMappingURL=stripe-webhook.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe-webhook.d.ts","sourceRoot":"","sources":["../src/stripe-webhook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,209 @@
1
+ // Stripe webhooks, pure: the `Stripe-Signature` check and the events billing acts on, narrowed with
2
+ // zod. Stripe signs `<timestamp>.<raw body>` with HMAC-SHA256 under the endpoint's secret
3
+ // (https://docs.stripe.com/webhooks#verify-manually); a timestamp older or newer than the tolerance
4
+ // is a replay and fails like a wrong signature. Nothing here touches the database.
5
+ import { createHmac, timingSafeEqual } from "node:crypto";
6
+ import { err, ok } from "@softure-ai/core";
7
+ import { z } from "zod";
8
+ import { fromStripeAmount } from "./stripe-currency.js";
9
+ /** The header Stripe signs every delivery with (lower case, as `Headers` returns it). */
10
+ export const STRIPE_SIGNATURE_HEADER = "stripe-signature";
11
+ /** How far a delivery's timestamp may be from now, in seconds: Stripe's default replay window. */
12
+ export const STRIPE_SIGNATURE_TOLERANCE_SECONDS = 300;
13
+ /** The metadata keys a checkout session carries (`stripe()` sets them). */
14
+ export const STRIPE_METADATA = { userId: "softure_user_id", planId: "softure_plan_id" };
15
+ /** The longest id billing stores; Stripe's ids are far shorter. */
16
+ const MAX_ID_LENGTH = 255;
17
+ const SIGNATURE_PATTERN = /^[0-9a-f]{64}$/;
18
+ function sign(secret, timestamp, payload) {
19
+ return createHmac("sha256", secret).update(`${String(timestamp)}.${payload}`, "utf8").digest("hex");
20
+ }
21
+ /** A `Stripe-Signature` value as Stripe sends it, e.g. for a test that plays Stripe. */
22
+ export function signStripePayload({ payload, secret, timestamp }) {
23
+ return `t=${String(timestamp)},v1=${sign(secret, timestamp, payload)}`;
24
+ }
25
+ /**
26
+ * Whether Stripe signed `payload` with `secret` within the tolerance of `now`. Any `v1` entry may
27
+ * match: Stripe sends one per secret while an old secret is still rolling off.
28
+ */
29
+ export function verifyStripeSignature(input) {
30
+ const { payload, header, secret, now } = input;
31
+ const tolerance = input.toleranceSeconds ?? STRIPE_SIGNATURE_TOLERANCE_SECONDS;
32
+ if (header === null || secret === "")
33
+ return err("billing.webhook_invalid");
34
+ let timestamp = null;
35
+ const signatures = [];
36
+ for (const part of header.split(",")) {
37
+ const separator = part.indexOf("=");
38
+ if (separator <= 0)
39
+ continue;
40
+ const key = part.slice(0, separator).trim();
41
+ const value = part.slice(separator + 1).trim();
42
+ if (key === "t" && /^\d{1,12}$/.test(value))
43
+ timestamp = Number(value);
44
+ if (key === "v1" && SIGNATURE_PATTERN.test(value))
45
+ signatures.push(value);
46
+ }
47
+ if (timestamp === null || signatures.length === 0)
48
+ return err("billing.webhook_invalid");
49
+ if (Math.abs(Math.floor(now.getTime() / 1000) - timestamp) > tolerance)
50
+ return err("billing.webhook_invalid");
51
+ const expected = Buffer.from(sign(secret, timestamp, payload), "hex");
52
+ const isSigned = signatures.some((signature) => timingSafeEqual(Buffer.from(signature, "hex"), expected));
53
+ return isSigned ? ok() : err("billing.webhook_invalid");
54
+ }
55
+ const idSchema = z.string().min(1).max(MAX_ID_LENGTH);
56
+ /** Unix seconds, as Stripe dates every event and object (up to the end of year 9999). */
57
+ const unixSecondsSchema = z.number().int().min(0).max(253_402_300_799);
58
+ const envelopeSchema = z.object({
59
+ id: idSchema,
60
+ type: z.string().min(1).max(MAX_ID_LENGTH),
61
+ /** When the event (and the snapshot of its object) was made; billing needs it for refunds only. */
62
+ created: unixSecondsSchema.optional(),
63
+ data: z.object({ object: z.unknown() }),
64
+ });
65
+ /** An expanded object or its id. */
66
+ const referenceSchema = z.union([idSchema, z.object({ id: idSchema })]).transform((value) => (typeof value === "string" ? value : value.id));
67
+ const sessionSchema = z.object({
68
+ id: idSchema,
69
+ mode: z.string(),
70
+ payment_status: z.string(),
71
+ payment_intent: referenceSchema.nullish(),
72
+ amount_total: z.number().int().min(0).nullish(),
73
+ currency: z.string().regex(/^[a-zA-Z]{3}$/).nullish(),
74
+ metadata: z.record(z.string(), z.string()).nullish(),
75
+ });
76
+ const chargeSchema = z.object({
77
+ payment_intent: referenceSchema.nullish(),
78
+ refunded: z.boolean(),
79
+ /** Stripe sends it on every charge; needed only for a partial refund, to convert the amount. */
80
+ currency: z.string().regex(/^[a-zA-Z]{3}$/).optional(),
81
+ /** The total refunded so far (Stripe sends it on every charge); needed only for a partial refund. */
82
+ amount_refunded: z.number().int().min(0).optional(),
83
+ });
84
+ const refundSchema = z.object({
85
+ id: idSchema,
86
+ payment_intent: referenceSchema.nullish(),
87
+ amount: z.number().int().min(0),
88
+ currency: z.string().regex(/^[a-zA-Z]{3}$/),
89
+ created: unixSecondsSchema,
90
+ status: z.string().nullish(),
91
+ });
92
+ /** Refund statuses whose money went back to the payment: the refund took nothing in the end. */
93
+ const FAILED_REFUND_STATUSES = new Set(["failed", "canceled"]);
94
+ /** Payment statuses of a checkout that has its money (`no_payment_required`: a 100% discount). */
95
+ const SETTLED_PAYMENT_STATUSES = new Set(["paid", "no_payment_required"]);
96
+ function readCheckout(eventId, object) {
97
+ const parsed = sessionSchema.safeParse(object);
98
+ if (!parsed.success)
99
+ return null;
100
+ const session = parsed.data;
101
+ const userId = session.metadata?.[STRIPE_METADATA.userId];
102
+ const planId = session.metadata?.[STRIPE_METADATA.planId];
103
+ if (session.mode !== "payment" || userId === undefined || planId === undefined) {
104
+ return { type: "ignored", eventId, reason: "a checkout billing did not create" };
105
+ }
106
+ if (!SETTLED_PAYMENT_STATUSES.has(session.payment_status)) {
107
+ // A delayed method (a bank transfer): `checkout.session.async_payment_succeeded` follows.
108
+ return { type: "ignored", eventId, reason: `a checkout whose payment is ${session.payment_status}` };
109
+ }
110
+ if (session.amount_total === null || session.amount_total === undefined || session.currency === null || session.currency === undefined)
111
+ return null;
112
+ const currency = session.currency.toUpperCase();
113
+ return {
114
+ type: "checkout_paid",
115
+ eventId,
116
+ checkout: {
117
+ checkoutId: session.id,
118
+ paymentId: session.payment_intent ?? null,
119
+ userId,
120
+ planId,
121
+ amount: fromStripeAmount(session.amount_total, currency),
122
+ currency,
123
+ },
124
+ };
125
+ }
126
+ function fromUnixSeconds(seconds) {
127
+ return new Date(seconds * 1000);
128
+ }
129
+ function readRefund(eventId, object, created) {
130
+ const parsed = chargeSchema.safeParse(object);
131
+ if (!parsed.success || created === undefined)
132
+ return null;
133
+ const paymentId = parsed.data.payment_intent;
134
+ if (paymentId === null || paymentId === undefined)
135
+ return { type: "ignored", eventId, reason: "a refunded charge without a payment" };
136
+ const snapshotAt = fromUnixSeconds(created);
137
+ if (parsed.data.refunded)
138
+ return { type: "payment_refunded", eventId, paymentId, snapshotAt };
139
+ const { amount_refunded: amountRefunded, currency } = parsed.data;
140
+ if (amountRefunded === undefined || currency === undefined)
141
+ return null;
142
+ if (amountRefunded === 0)
143
+ return { type: "ignored", eventId, reason: "a charge with nothing refunded" };
144
+ return { type: "payment_partially_refunded", eventId, paymentId, amountRefunded: fromStripeAmount(amountRefunded, currency.toUpperCase()), snapshotAt };
145
+ }
146
+ /** A Refund event: `refund.failed` always reports a failure, the update events only with a failed or canceled status. */
147
+ function readFailedRefund(eventId, object, created, isFailure) {
148
+ const parsed = refundSchema.safeParse(object);
149
+ if (!parsed.success || created === undefined)
150
+ return null;
151
+ const refund = parsed.data;
152
+ if (!isFailure && !FAILED_REFUND_STATUSES.has(refund.status ?? "")) {
153
+ return { type: "ignored", eventId, reason: `a refund whose status is ${refund.status ?? "unknown"}` };
154
+ }
155
+ const paymentId = refund.payment_intent;
156
+ if (paymentId === null || paymentId === undefined)
157
+ return { type: "ignored", eventId, reason: "a failed refund without a payment" };
158
+ const failure = {
159
+ paymentId,
160
+ refundId: refund.id,
161
+ amount: fromStripeAmount(refund.amount, refund.currency.toUpperCase()),
162
+ refundCreatedAt: fromUnixSeconds(refund.created),
163
+ failedAt: fromUnixSeconds(created),
164
+ };
165
+ return { type: "refund_failed", eventId, failure };
166
+ }
167
+ /**
168
+ * The event a verified payload carries. A body that is not a Stripe event, or an event billing acts
169
+ * on whose object lacks what billing needs, is `billing.webhook_invalid`.
170
+ */
171
+ export function parseStripeEvent(payload) {
172
+ let json;
173
+ try {
174
+ json = JSON.parse(payload);
175
+ }
176
+ catch {
177
+ return err("billing.webhook_invalid");
178
+ }
179
+ const envelope = envelopeSchema.safeParse(json);
180
+ if (!envelope.success)
181
+ return err("billing.webhook_invalid");
182
+ const { id, type, data, created } = envelope.data;
183
+ let event;
184
+ switch (type) {
185
+ case "checkout.session.completed":
186
+ case "checkout.session.async_payment_succeeded":
187
+ event = readCheckout(id, data.object);
188
+ break;
189
+ case "charge.refunded":
190
+ event = readRefund(id, data.object, created);
191
+ break;
192
+ case "refund.failed":
193
+ event = readFailedRefund(id, data.object, created, true);
194
+ break;
195
+ case "refund.updated":
196
+ case "charge.refund.updated":
197
+ event = readFailedRefund(id, data.object, created, false);
198
+ break;
199
+ default:
200
+ event = { type: "ignored", eventId: id, reason: `the event type ${type}` };
201
+ }
202
+ return event === null ? err("billing.webhook_invalid") : ok(event);
203
+ }
204
+ /** `verifyStripeSignature`, then `parseStripeEvent`: nothing unsigned is parsed. */
205
+ export function readStripeWebhook(input) {
206
+ const verified = verifyStripeSignature(input);
207
+ return verified.ok ? parseStripeEvent(input.payload) : verified;
208
+ }
209
+ //# sourceMappingURL=stripe-webhook.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe-webhook.js","sourceRoot":"","sources":["../src/stripe-webhook.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { PaymentContext, PaymentProvider, PaymentRequest } from "./payment.js";
2
+ export declare const STRIPE_API_BASE = "https://api.stripe.com";
3
+ export declare const STRIPE_SECRET_KEY_ENV = "STRIPE_SECRET_KEY";
4
+ /** How long the Checkout request may take before the buyer sees `billing.payment_failed`. */
5
+ export declare const STRIPE_TIMEOUT_MS = 10000;
6
+ export interface StripeOptions {
7
+ /**
8
+ * The secret API key (`sk_test_...` in the sandbox). Default: `process.env.STRIPE_SECRET_KEY`,
9
+ * read on every payment, so the configuration loads at build time without the secret.
10
+ */
11
+ readonly secretKey?: string;
12
+ /** Default: `https://api.stripe.com`. For a proxy or a test server. */
13
+ readonly apiBase?: string;
14
+ /** Injected in tests; default: the global `fetch`. */
15
+ readonly fetch?: typeof fetch;
16
+ }
17
+ /**
18
+ * The form body of `POST /v1/checkout/sessions` for one payment of the plan: its price as a one-off
19
+ * line item in Stripe's unit (`toStripeAmount`), the account in `client_reference_id` and the metadata the webhook reads (copied onto
20
+ * the PaymentIntent, so the charge in Stripe's dashboard names the account too).
21
+ */
22
+ export declare function getCheckoutSessionParams(ctx: Pick<PaymentContext, "config">, request: PaymentRequest): URLSearchParams;
23
+ /** `billing({ payment: stripe() })`: card, BLIK and transfer payments on Stripe Checkout. */
24
+ export declare function stripe(options?: StripeOptions): PaymentProvider;
25
+ //# sourceMappingURL=stripe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.d.ts","sourceRoot":"","sources":["../src/stripe.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAMpF,eAAO,MAAM,eAAe,2BAA2B,CAAC;AACxD,eAAO,MAAM,qBAAqB,sBAAsB,CAAC;AACzD,6FAA6F;AAC7F,eAAO,MAAM,iBAAiB,QAAS,CAAC;AAExC,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CAC/B;AASD;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC,EAAE,OAAO,EAAE,cAAc,GAAG,eAAe,CAwBtH;AAiBD,6FAA6F;AAC7F,wBAAgB,MAAM,CAAC,OAAO,GAAE,aAAkB,GAAG,eAAe,CA8CnE"}
package/dist/stripe.js ADDED
@@ -0,0 +1,116 @@
1
+ // The Stripe adapter (https://docs.stripe.com/api/checkout/sessions/create): a one-time Checkout
2
+ // session per payment, paid on Stripe's page; the verified webhook (`stripeWebhookRoute` in
3
+ // `/next`) grants the plan once Stripe reports the money. Stripe's HTTP API through `fetch`, like
4
+ // mailing's `resend()`: no SDK, and tests inject `fetch`.
5
+ import { err, ok } from "@softure-ai/core";
6
+ import { CHECKOUT_PARAM, PLAN_FIELD } from "./fields.js";
7
+ import { getLocalizedText } from "./plans.js";
8
+ import { STRIPE_PROVIDER } from "./server/payments.js";
9
+ import { describeStripePriceProblem, toStripeAmount } from "./stripe-currency.js";
10
+ import { STRIPE_METADATA } from "./stripe-webhook.js";
11
+ export const STRIPE_API_BASE = "https://api.stripe.com";
12
+ export const STRIPE_SECRET_KEY_ENV = "STRIPE_SECRET_KEY";
13
+ /** How long the Checkout request may take before the buyer sees `billing.payment_failed`. */
14
+ export const STRIPE_TIMEOUT_MS = 10_000;
15
+ function getReturnUrl(returnUrl, result, planId) {
16
+ const url = new URL(returnUrl);
17
+ if (planId !== null)
18
+ url.searchParams.set(PLAN_FIELD, planId);
19
+ url.searchParams.set(CHECKOUT_PARAM, result);
20
+ return url.toString();
21
+ }
22
+ /**
23
+ * The form body of `POST /v1/checkout/sessions` for one payment of the plan: its price as a one-off
24
+ * line item in Stripe's unit (`toStripeAmount`), the account in `client_reference_id` and the metadata the webhook reads (copied onto
25
+ * the PaymentIntent, so the charge in Stripe's dashboard names the account too).
26
+ */
27
+ export function getCheckoutSessionParams(ctx, request) {
28
+ const { plan, account } = request;
29
+ const { locale } = ctx.config;
30
+ const unitAmount = toStripeAmount(plan.price);
31
+ // The config refuses such a price (`checkPrice`) and `startPayment` checks it first.
32
+ if (unitAmount === null)
33
+ throw new Error(`@softure-ai/billing: stripe() cannot charge the price of plan "${plan.id}"`);
34
+ const params = new URLSearchParams({
35
+ mode: "payment",
36
+ locale,
37
+ client_reference_id: account.id,
38
+ customer_email: account.email,
39
+ success_url: getReturnUrl(request.returnUrl, "success", null),
40
+ cancel_url: getReturnUrl(request.returnUrl, "cancelled", plan.id),
41
+ "line_items[0][quantity]": "1",
42
+ "line_items[0][price_data][currency]": plan.price.currency.toLowerCase(),
43
+ "line_items[0][price_data][unit_amount]": String(unitAmount),
44
+ "line_items[0][price_data][product_data][name]": getLocalizedText(plan.name, locale),
45
+ [`metadata[${STRIPE_METADATA.userId}]`]: account.id,
46
+ [`metadata[${STRIPE_METADATA.planId}]`]: plan.id,
47
+ [`payment_intent_data[metadata][${STRIPE_METADATA.userId}]`]: account.id,
48
+ [`payment_intent_data[metadata][${STRIPE_METADATA.planId}]`]: plan.id,
49
+ });
50
+ if (plan.description !== undefined)
51
+ params.set("line_items[0][price_data][product_data][description]", getLocalizedText(plan.description, locale));
52
+ return params;
53
+ }
54
+ /** The hosted page's URL from Stripe's answer, or null when the answer has none. */
55
+ function readCheckoutUrl(payload) {
56
+ if (typeof payload !== "object" || payload === null || !Object.hasOwn(payload, "url"))
57
+ return null;
58
+ const url = payload.url;
59
+ return typeof url === "string" && url.startsWith("https://") ? url : null;
60
+ }
61
+ /** Stripe's error type and code for the log; its message can echo the request back. */
62
+ function describeStripeError(payload) {
63
+ const error = typeof payload === "object" && payload !== null ? payload.error : null;
64
+ if (typeof error !== "object" || error === null)
65
+ return "no error body";
66
+ const { type, code } = error;
67
+ return [type, code].filter((part) => typeof part === "string").join("/") || "no error type";
68
+ }
69
+ /** `billing({ payment: stripe() })`: card, BLIK and transfer payments on Stripe Checkout. */
70
+ export function stripe(options = {}) {
71
+ const apiBase = options.apiBase ?? STRIPE_API_BASE;
72
+ return {
73
+ name: STRIPE_PROVIDER,
74
+ collectsInvoiceDetails: false,
75
+ handsOverRequests: false,
76
+ checkPrice: describeStripePriceProblem,
77
+ async startPayment(ctx, request) {
78
+ const priceProblem = describeStripePriceProblem(request.plan.price);
79
+ if (priceProblem !== null) {
80
+ // Only a config built around billing({ ... }) gets here; the options schema refuses it.
81
+ console.error(`@softure-ai/billing: plan "${request.plan.id}": ${priceProblem}`);
82
+ return err("billing.payment_failed");
83
+ }
84
+ const secretKey = (options.secretKey ?? process.env[STRIPE_SECRET_KEY_ENV] ?? "").trim();
85
+ if (secretKey === "") {
86
+ // A deployment without the key cannot take payments until it is set; never the buyer's fault.
87
+ console.error(`@softure-ai/billing: stripe has no secret key; set ${STRIPE_SECRET_KEY_ENV} or pass stripe({ secretKey })`);
88
+ return err("billing.payment_failed");
89
+ }
90
+ const fetchImpl = options.fetch ?? fetch;
91
+ let response;
92
+ try {
93
+ response = await fetchImpl(new URL("/v1/checkout/sessions", apiBase), {
94
+ method: "POST",
95
+ headers: { Authorization: `Bearer ${secretKey}`, "Content-Type": "application/x-www-form-urlencoded" },
96
+ body: getCheckoutSessionParams(ctx, request),
97
+ signal: AbortSignal.timeout(STRIPE_TIMEOUT_MS),
98
+ cache: "no-store",
99
+ });
100
+ }
101
+ catch (error) {
102
+ // A timeout or a network failure: nothing was created that the buyer can pay.
103
+ console.error(`@softure-ai/billing: the Stripe checkout for plan "${request.plan.id}" could not be created: ${error instanceof Error ? error.name : typeof error}`);
104
+ return err("billing.payment_failed");
105
+ }
106
+ const payload = await response.json().catch(() => null);
107
+ const url = response.ok ? readCheckoutUrl(payload) : null;
108
+ if (url === null) {
109
+ console.error(`@softure-ai/billing: Stripe refused the checkout for plan "${request.plan.id}" (HTTP ${String(response.status)}, ${response.ok ? "no checkout url" : describeStripeError(payload)})`);
110
+ return err("billing.payment_failed");
111
+ }
112
+ return ok({ type: "redirect", url });
113
+ },
114
+ };
115
+ }
116
+ //# sourceMappingURL=stripe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stripe.js","sourceRoot":"","sources":["../src/stripe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type Locale } from "@softure-ai/core";
2
+ import { type ClassNames } from "@softure-ai/ui";
3
+ import type { Entitlement } from "../contract.js";
4
+ import type { BillingMessages } from "../messages/index.js";
5
+ export type AccessBadgeSlot = "root" | "status" | "detail";
6
+ export interface AccessBadgeProps {
7
+ readonly entitlement: Entitlement;
8
+ readonly messages: BillingMessages;
9
+ readonly locale: Locale;
10
+ /** The app's IANA time zone, for the dates shown. */
11
+ readonly timezone: string;
12
+ readonly classNames?: ClassNames<AccessBadgeSlot>;
13
+ readonly unstyled?: boolean;
14
+ }
15
+ export declare function AccessBadge({ entitlement, messages, locale, timezone, classNames, unstyled }: AccessBadgeProps): import("react").JSX.Element;
16
+ //# sourceMappingURL=access-badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-badge.d.ts","sourceRoot":"","sources":["../../src/ui/access-badge.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,KAAK,UAAU,EAAyB,MAAM,gBAAgB,CAAC;AACxE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,eAAe,CAAC,CAAC;IAClD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AA2BD,wBAAgB,WAAW,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,gBAAgB,+BAiB9G"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // A compact label of where an account stands: "Trial · 5 days left", "Paid · until November 30,
3
+ // 2026", "Lifetime access" or "Read-only". Server-safe: it renders from props, so a server
4
+ // component passes the entitlement it read (`CurrentAccessBadge` in `/next` does).
5
+ import { formatMessage } from "@softure-ai/core";
6
+ import { createSlotClassGetter } from "@softure-ai/ui";
7
+ import { formatDaysLeft, formatLastDay } from "./format.js";
8
+ const TONE = {
9
+ neutral: "sft:text-foreground",
10
+ success: "sft:text-success",
11
+ warning: "sft:text-warning",
12
+ danger: "sft:text-danger",
13
+ };
14
+ function describe(entitlement, messages, locale, timezone) {
15
+ switch (entitlement.status) {
16
+ case "trial":
17
+ return { status: messages.badge.trial, detail: formatDaysLeft(entitlement.daysLeft, locale, messages), tone: entitlement.isEnding ? "warning" : "neutral" };
18
+ case "paid":
19
+ if (entitlement.endsAt === null)
20
+ return { status: messages.badge.lifetime, tone: "success" };
21
+ return {
22
+ status: messages.badge.paid,
23
+ detail: formatMessage(messages.badge.until, { date: formatLastDay(entitlement.endsAt, locale, timezone) }),
24
+ tone: entitlement.isEnding ? "warning" : "success",
25
+ };
26
+ case "read_only":
27
+ return { status: messages.badge.readOnly, tone: "danger" };
28
+ }
29
+ }
30
+ export function AccessBadge({ entitlement, messages, locale, timezone, classNames, unstyled }) {
31
+ const { status, detail, tone } = describe(entitlement, messages, locale, timezone);
32
+ const slot = createSlotClassGetter({
33
+ defaults: {
34
+ root: "sft:inline-flex sft:items-center sft:gap-1.5 sft:rounded-full sft:border sft:border-border sft:bg-surface sft:px-2.5 sft:py-1 sft:font-sans sft:text-xs",
35
+ status: `sft:font-medium ${TONE[tone]}`,
36
+ detail: "sft:text-muted",
37
+ },
38
+ classNames,
39
+ unstyled,
40
+ });
41
+ return (_jsxs("span", { className: slot("root"), "data-status": entitlement.status, "data-ending": "isEnding" in entitlement && entitlement.isEnding ? "true" : undefined, children: [_jsx("span", { className: slot("status"), children: status }), detail === undefined ? null : _jsx("span", { className: slot("detail"), children: detail })] }));
42
+ }
43
+ //# sourceMappingURL=access-badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-badge.js","sourceRoot":"","sources":["../../src/ui/access-badge.tsx"],"names":[],"mappings":";AAAA,gGAAgG;AAChG,2FAA2F;AAC3F,mFAAmF;AACnF,OAAO,EAAE,aAAa,EAAe,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAmB,qBAAqB,EAAE,MAAM,gBAAgB,CAAC;AAGxE,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgB5D,MAAM,IAAI,GAAmC;IAC3C,OAAO,EAAE,qBAAqB;IAC9B,OAAO,EAAE,kBAAkB;IAC3B,OAAO,EAAE,kBAAkB;IAC3B,MAAM,EAAE,iBAAiB;CAC1B,CAAC;AAEF,SAAS,QAAQ,CAAC,WAAwB,EAAE,QAAyB,EAAE,MAAc,EAAE,QAAgB;IACrG,QAAQ,WAAW,CAAC,MAAM,EAAE,CAAC;QAC3B,KAAK,OAAO;YACV,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,CAAC,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;QAC9J,KAAK,MAAM;YACT,IAAI,WAAW,CAAC,MAAM,KAAK,IAAI;gBAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;YAC7F,OAAO;gBACL,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI;gBAC3B,MAAM,EAAE,aAAa,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,aAAa,CAAC,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,CAAC;gBAC1G,IAAI,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS;aACnD,CAAC;QACJ,KAAK,WAAW;YACd,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAoB;IAC7G,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,QAAQ,CAAC,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACnF,MAAM,IAAI,GAAG,qBAAqB,CAAkB;QAClD,QAAQ,EAAE;YACR,IAAI,EAAE,yJAAyJ;YAC/J,MAAM,EAAE,mBAAmB,IAAI,CAAC,IAAI,CAAC,EAAE;YACvC,MAAM,EAAE,gBAAgB;SACzB;QACD,UAAU;QACV,QAAQ;KACT,CAAC,CAAC;IACH,OAAO,CACL,gBAAM,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,iBAAe,WAAW,CAAC,MAAM,iBAAe,UAAU,IAAI,WAAW,IAAI,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,aACjJ,eAAM,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAG,MAAM,GAAQ,EAC/C,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAM,SAAS,EAAE,IAAI,CAAC,QAAQ,CAAC,YAAG,MAAM,GAAQ,IAC1E,CACR,CAAC;AACJ,CAAC"}
@@ -0,0 +1,20 @@
1
+ import { type Locale } from "@softure-ai/core";
2
+ import { type ClassNames, type LinkComponentType } from "@softure-ai/ui";
3
+ import type { Entitlement } from "../contract.js";
4
+ import type { BillingMessages } from "../messages/index.js";
5
+ export type AccessNoticeSlot = "root" | "message" | "actions";
6
+ export interface AccessNoticeProps {
7
+ readonly entitlement: Entitlement;
8
+ readonly messages: BillingMessages;
9
+ readonly locale: Locale;
10
+ /** The app's IANA time zone, for the dates shown. */
11
+ readonly timezone: string;
12
+ /** Where to pay or renew (`routes.payment`). */
13
+ readonly paymentHref: string;
14
+ /** Renders the link; a plain `<a>` when omitted. */
15
+ readonly LinkComponent?: LinkComponentType;
16
+ readonly classNames?: ClassNames<AccessNoticeSlot>;
17
+ readonly unstyled?: boolean;
18
+ }
19
+ export declare function AccessNotice({ entitlement, messages, locale, timezone, paymentHref, LinkComponent, classNames, unstyled }: AccessNoticeProps): import("react").JSX.Element | null;
20
+ //# sourceMappingURL=access-notice.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"access-notice.d.ts","sourceRoot":"","sources":["../../src/ui/access-notice.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAiB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAc,KAAK,UAAU,EAAyB,KAAK,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAC5G,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAG5D,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;IACnC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,qDAAqD;IACrD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,gDAAgD;IAChD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,oDAAoD;IACpD,QAAQ,CAAC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAC3C,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAC,gBAAgB,CAAC,CAAC;IACnD,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAyBD,wBAAgB,YAAY,CAAC,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,iBAAiB,sCAwB5I"}
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ // The notice an account sees when its access is about to end or has ended, with a link to pay.
3
+ // It renders nothing while access is not in its reminder window. Server-safe and framework-free:
4
+ // the link goes through an injected `LinkComponent` (Next's `Link` in `/next`).
5
+ import { formatMessage } from "@softure-ai/core";
6
+ import { ButtonLink, createSlotClassGetter } from "@softure-ai/ui";
7
+ import { formatLastDay } from "./format.js";
8
+ /** The notice for this entitlement, or null when there is nothing to say. */
9
+ function getNoticeCopy(entitlement, messages, locale, timezone) {
10
+ const copy = messages.notice;
11
+ switch (entitlement.status) {
12
+ case "trial":
13
+ if (!entitlement.isEnding)
14
+ return null;
15
+ return { text: formatMessage(copy.trialEnding, { date: formatLastDay(entitlement.endsAt, locale, timezone) }), action: copy.choosePlan, isEnded: false };
16
+ case "paid":
17
+ if (!entitlement.isEnding || entitlement.endsAt === null)
18
+ return null;
19
+ return { text: formatMessage(copy.paidEnding, { date: formatLastDay(entitlement.endsAt, locale, timezone) }), action: copy.renew, isEnded: false };
20
+ case "read_only":
21
+ return entitlement.reason === "trial_ended"
22
+ ? { text: copy.trialEnded, action: copy.choosePlan, isEnded: true }
23
+ : { text: copy.paidEnded, action: copy.renew, isEnded: true };
24
+ }
25
+ }
26
+ export function AccessNotice({ entitlement, messages, locale, timezone, paymentHref, LinkComponent, classNames, unstyled }) {
27
+ const notice = getNoticeCopy(entitlement, messages, locale, timezone);
28
+ if (notice === null)
29
+ return null;
30
+ const slot = createSlotClassGetter({
31
+ defaults: {
32
+ root: `sft:flex sft:flex-col sft:gap-3 sft:rounded-control sft:border sft:px-4 sft:py-2.5 sft:font-sans sft:text-sm sft:text-foreground ${notice.isEnded ? "sft:border-danger/50 sft:bg-danger/10" : "sft:border-border-strong sft:bg-surface-raised"}`,
33
+ message: "sft:m-0",
34
+ actions: "sft:flex sft:shrink-0",
35
+ },
36
+ classNames,
37
+ unstyled,
38
+ });
39
+ return (_jsxs("div", { role: "status", className: slot("root"), "data-status": entitlement.status, children: [_jsx("p", { className: slot("message"), children: notice.text }), _jsx("div", { className: slot("actions"), children: _jsx(ButtonLink, { href: paymentHref, LinkComponent: LinkComponent, variant: notice.isEnded ? "primary" : "secondary", size: "sm", unstyled: unstyled, children: notice.action }) })] }));
40
+ }
41
+ //# sourceMappingURL=access-notice.js.map