@owlmeans/web-payment 0.1.18-rc.20 → 0.1.18-rc.21

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 (136) hide show
  1. package/README.md +1 -1
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/web-payment/SKILL.md +171 -31
  4. package/build/@/components/ui/checkbox.d.ts +5 -0
  5. package/build/@/components/ui/checkbox.d.ts.map +1 -0
  6. package/build/@/components/ui/checkbox.js +6 -0
  7. package/build/@/components/ui/checkbox.js.map +1 -0
  8. package/build/checkout-limit.d.ts +11 -0
  9. package/build/checkout-limit.d.ts.map +1 -0
  10. package/build/checkout-limit.js +34 -0
  11. package/build/checkout-limit.js.map +1 -0
  12. package/build/consumer/cancellation.d.ts +20 -0
  13. package/build/consumer/cancellation.d.ts.map +1 -0
  14. package/build/consumer/cancellation.js +115 -0
  15. package/build/consumer/cancellation.js.map +1 -0
  16. package/build/consumer/copy.d.ts +23 -0
  17. package/build/consumer/copy.d.ts.map +1 -0
  18. package/build/consumer/copy.js +75 -0
  19. package/build/consumer/copy.js.map +1 -0
  20. package/build/consumer/ensure.d.ts +56 -0
  21. package/build/consumer/ensure.d.ts.map +1 -0
  22. package/build/consumer/ensure.js +71 -0
  23. package/build/consumer/ensure.js.map +1 -0
  24. package/build/consumer/fields.d.ts +43 -0
  25. package/build/consumer/fields.d.ts.map +1 -0
  26. package/build/consumer/fields.js +19 -0
  27. package/build/consumer/fields.js.map +1 -0
  28. package/build/consumer/hooks.d.ts +113 -0
  29. package/build/consumer/hooks.d.ts.map +1 -0
  30. package/build/consumer/hooks.js +225 -0
  31. package/build/consumer/hooks.js.map +1 -0
  32. package/build/consumer/index.d.ts +14 -0
  33. package/build/consumer/index.d.ts.map +1 -0
  34. package/build/consumer/index.js +12 -0
  35. package/build/consumer/index.js.map +1 -0
  36. package/build/consumer/legal.d.ts +53 -0
  37. package/build/consumer/legal.d.ts.map +1 -0
  38. package/build/consumer/legal.js +77 -0
  39. package/build/consumer/legal.js.map +1 -0
  40. package/build/consumer/performance-consent-dialog.d.ts +18 -0
  41. package/build/consumer/performance-consent-dialog.d.ts.map +1 -0
  42. package/build/consumer/performance-consent-dialog.js +71 -0
  43. package/build/consumer/performance-consent-dialog.js.map +1 -0
  44. package/build/consumer/provider.d.ts +33 -0
  45. package/build/consumer/provider.d.ts.map +1 -0
  46. package/build/consumer/provider.js +26 -0
  47. package/build/consumer/provider.js.map +1 -0
  48. package/build/consumer/refusal.d.ts +35 -0
  49. package/build/consumer/refusal.d.ts.map +1 -0
  50. package/build/consumer/refusal.js +98 -0
  51. package/build/consumer/refusal.js.map +1 -0
  52. package/build/consumer/subscription-start-dialog.d.ts +16 -0
  53. package/build/consumer/subscription-start-dialog.d.ts.map +1 -0
  54. package/build/consumer/subscription-start-dialog.js +43 -0
  55. package/build/consumer/subscription-start-dialog.js.map +1 -0
  56. package/build/consumer/types.d.ts +114 -0
  57. package/build/consumer/types.d.ts.map +1 -0
  58. package/build/consumer/types.js +2 -0
  59. package/build/consumer/types.js.map +1 -0
  60. package/build/consumer/withdrawal.d.ts +29 -0
  61. package/build/consumer/withdrawal.d.ts.map +1 -0
  62. package/build/consumer/withdrawal.js +140 -0
  63. package/build/consumer/withdrawal.js.map +1 -0
  64. package/build/dialog.d.ts +1 -1
  65. package/build/dialog.d.ts.map +1 -1
  66. package/build/dialog.js +42 -16
  67. package/build/dialog.js.map +1 -1
  68. package/build/estimate-summary.d.ts +9 -1
  69. package/build/estimate-summary.d.ts.map +1 -1
  70. package/build/estimate-summary.js +10 -7
  71. package/build/estimate-summary.js.map +1 -1
  72. package/build/estimate.d.ts +6 -0
  73. package/build/estimate.d.ts.map +1 -1
  74. package/build/estimate.js +26 -1
  75. package/build/estimate.js.map +1 -1
  76. package/build/format.d.ts +15 -0
  77. package/build/format.d.ts.map +1 -0
  78. package/build/format.js +68 -0
  79. package/build/format.js.map +1 -0
  80. package/build/i18n/be.json +1 -1
  81. package/build/i18n/de.json +1 -1
  82. package/build/i18n/en.json +1 -1
  83. package/build/i18n/es.json +1 -1
  84. package/build/i18n/fr.json +110 -15
  85. package/build/i18n/pl.json +1 -1
  86. package/build/i18n/ru.json +1 -1
  87. package/build/i18n/uk.json +1 -1
  88. package/build/i18n.d.ts +2 -1
  89. package/build/i18n.d.ts.map +1 -1
  90. package/build/i18n.js +10 -8
  91. package/build/i18n.js.map +1 -1
  92. package/build/index.d.ts +2 -0
  93. package/build/index.d.ts.map +1 -1
  94. package/build/index.js +2 -0
  95. package/build/index.js.map +1 -1
  96. package/build/types.d.ts +22 -1
  97. package/build/types.d.ts.map +1 -1
  98. package/package.json +18 -7
  99. package/src/@/components/ui/checkbox.tsx +23 -0
  100. package/src/checkout-limit.tsx +48 -0
  101. package/src/consumer/cancellation.tsx +214 -0
  102. package/src/consumer/copy.ts +93 -0
  103. package/src/consumer/ensure.ts +124 -0
  104. package/src/consumer/fields.tsx +89 -0
  105. package/src/consumer/hooks.ts +358 -0
  106. package/src/consumer/index.ts +13 -0
  107. package/src/consumer/legal.tsx +129 -0
  108. package/src/consumer/performance-consent-dialog.tsx +132 -0
  109. package/src/consumer/provider.tsx +49 -0
  110. package/src/consumer/refusal.ts +110 -0
  111. package/src/consumer/subscription-start-dialog.tsx +87 -0
  112. package/src/consumer/types.ts +121 -0
  113. package/src/consumer/withdrawal.tsx +279 -0
  114. package/src/dialog.tsx +53 -23
  115. package/src/estimate-summary.tsx +21 -11
  116. package/src/estimate.ts +27 -1
  117. package/src/format.ts +71 -0
  118. package/src/i18n/be.json +1 -1
  119. package/src/i18n/de.json +1 -1
  120. package/src/i18n/en.json +1 -1
  121. package/src/i18n/es.json +1 -1
  122. package/src/i18n/fr.json +110 -15
  123. package/src/i18n/pl.json +1 -1
  124. package/src/i18n/ru.json +1 -1
  125. package/src/i18n/uk.json +1 -1
  126. package/src/i18n.ts +11 -8
  127. package/src/index.ts +2 -0
  128. package/src/types.ts +23 -1
  129. package/tests/consent-refusal.spec.ts +83 -0
  130. package/tests/consumer-hooks.spec.ts +205 -0
  131. package/tests/consumer.spec.ts +343 -0
  132. package/tests/context.ts +1 -1
  133. package/tests/harness/consumer-fixtures.ts +105 -0
  134. package/tests/harness/consumer.tsx +197 -0
  135. package/tests/harness/mount.tsx +9 -2
  136. package/tests/i18n.spec.ts +41 -5
package/build/i18n.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAErD,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA;AACnC,UAAU,CAAC,IAAI,EAAE,aAAa,EAAE,EAAE,CAAC,CAAA"}
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AACrD,OAAO,EAAE,MAAM,gBAAgB,CAAC,OAAO,IAAI,EAAE,MAAM,EAAE,CAAA;AAErD,8FAA8F;AAC9F,MAAM,CAAC,MAAM,oBAAoB,GAAG,aAAa,CAAA;AAEjD,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA;AAC1C,UAAU,CAAC,IAAI,EAAE,oBAAoB,EAAE,EAAE,CAAC,CAAA"}
package/build/index.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import './i18n.js';
2
+ export { WEB_PAYMENT_RESOURCE } from './i18n.js';
2
3
  export type * from './types.js';
3
4
  export * from './service.js';
4
5
  export * from './hooks.js';
5
6
  export * from './amount.js';
6
7
  export * from './dialog.js';
8
+ export * from './checkout-limit.js';
7
9
  export * from './estimate.js';
8
10
  export * from './estimate-summary.js';
9
11
  export * from './selectors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,CAAA;AAClB,mBAAmB,YAAY,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,CAAA;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAChD,mBAAmB,YAAY,CAAA;AAC/B,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA"}
package/build/index.js CHANGED
@@ -1,8 +1,10 @@
1
1
  import './i18n.js';
2
+ export { WEB_PAYMENT_RESOURCE } from './i18n.js';
2
3
  export * from './service.js';
3
4
  export * from './hooks.js';
4
5
  export * from './amount.js';
5
6
  export * from './dialog.js';
7
+ export * from './checkout-limit.js';
6
8
  export * from './estimate.js';
7
9
  export * from './estimate-summary.js';
8
10
  export * from './selectors.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,CAAA;AAElB,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,CAAA;AAClB,OAAO,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAEhD,cAAc,cAAc,CAAA;AAC5B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,kBAAkB,CAAA;AAChC,cAAc,sBAAsB,CAAA"}
package/build/types.d.ts CHANGED
@@ -1,16 +1,25 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { CallOptions, RequestShape } from '@owlmeans/entrypoint';
3
- import type { AmountCheckoutPolicy, CapabilityView, CreateCheckoutResponse, EntitlementPlanView, LimitView, PortalLinkBody, PriceEstimate, PriceEstimateBody } from '@owlmeans/payment';
3
+ import type { AmountCheckoutPolicy, CapabilityView, CheckoutLimitView, CreateCheckoutResponse, EntitlementPlanView, LimitView, PortalLinkBody, PriceEstimate, PriceEstimateBody } from '@owlmeans/payment';
4
4
  export type CheckoutResult = CreateCheckoutResponse;
5
5
  export interface AmountCheckoutDialogProps {
6
6
  open: boolean;
7
7
  onOpenChange: (open: boolean) => void;
8
+ /** The plan's policy — or one already narrowed for the entity (`AmountPolicyView.policy`). */
8
9
  policy: AmountCheckoutPolicy;
9
10
  pending?: boolean;
10
11
  /** Chosen amount and, where tax estimation collected it, the billing country. */
11
12
  onConfirm: (amountMinor: number, country?: string) => Promise<void> | void;
12
13
  /** A live tax/currency estimate for the credit line — absent: the plain "tax at checkout" note. */
13
14
  estimate?: PriceEstimateControl;
15
+ /**
16
+ * What the entity may buy now (`AmountPolicyView.limit`). The dialog narrows `policy` with it
17
+ * through `narrowAmountPolicy` — presets above the maximum disappear, the default is clamped —
18
+ * shows a `CheckoutLimitNote`, and on `blocked` disables the input and the confirm button.
19
+ */
20
+ limit?: CheckoutLimitView | null;
21
+ /** Legal text the application places right above the buttons (a withdrawal note, terms links). */
22
+ legalNote?: ReactNode;
14
23
  }
15
24
  /** What `usePriceEstimate` returns: the latest answer, the chosen country, and its lifecycle. */
16
25
  export interface PriceEstimateControl {
@@ -21,8 +30,20 @@ export interface PriceEstimateControl {
21
30
  loading: boolean;
22
31
  /** The last request failed; `estimate` is the previous good answer, if any. */
23
32
  failed: boolean;
33
+ /**
34
+ * The country is the entity's locked billing country (an answer with `locked` or `source:
35
+ * 'profile'`): a picker shows it disabled, and `onCountryChange` does nothing.
36
+ */
37
+ locked?: boolean;
24
38
  onCountryChange: (country: string) => void;
25
39
  }
40
+ export interface CheckoutLimitNoteProps {
41
+ /** `null`, or a limit that narrowed nothing, renders nothing. */
42
+ limit: CheckoutLimitView | null | undefined;
43
+ /** Replaces the package's sentence for the limit's `reason` (`per-purchase`, `window`, `hold`). */
44
+ reasonLabel?: string | ((reason: string) => string | null | undefined);
45
+ className?: string;
46
+ }
26
47
  /** The request of a price-estimate protocol: its body is a `PriceEstimateBody`. */
27
48
  export type PriceEstimateRequest = RequestShape & {
28
49
  body: PriceEstimateBody;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,KAAK,EACV,oBAAoB,EAAE,cAAc,EAAE,sBAAsB,EAAE,mBAAmB,EAAE,SAAS,EAC5F,cAAc,EAAE,aAAa,EAAE,iBAAiB,EACjD,MAAM,mBAAmB,CAAA;AAE1B,MAAM,MAAM,cAAc,GAAG,sBAAsB,CAAA;AACnD,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,OAAO,CAAA;IACb,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,MAAM,EAAE,oBAAoB,CAAA;IAC5B,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,iFAAiF;IACjF,SAAS,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC1E,mGAAmG;IACnG,QAAQ,CAAC,EAAE,oBAAoB,CAAA;CAChC;AAED,iGAAiG;AACjG,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,aAAa,GAAG,IAAI,CAAA;IAC9B,6DAA6D;IAC7D,OAAO,EAAE,MAAM,CAAA;IACf,gEAAgE;IAChE,OAAO,EAAE,OAAO,CAAA;IAChB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,CAAA;IACf,eAAe,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;CAC3C;AAED,mFAAmF;AACnF,MAAM,MAAM,oBAAoB,GAAG,YAAY,GAAG;IAAE,IAAI,EAAE,iBAAiB,CAAA;CAAE,CAAA;AAE7E,KAAK,iBAAiB,CAAC,OAAO,SAAS,oBAAoB,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACrF,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC,CAAA;CACxC,CAAA;AAED,iGAAiG;AACjG,MAAM,MAAM,sBAAsB,CAAC,OAAO,SAAS,oBAAoB,IAAI,EAAE,SAAS,iBAAiB,CAAC,OAAO,CAAC,GAC5G,CAAC,OAAO,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,GACpD,CAAC,OAAO,EAAE,iBAAiB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,CAAA;AAEvD,kDAAkD;AAClD,MAAM,WAAW,WAAY,SAAQ,SAAS;IAC5C,uFAAuF;IACvF,SAAS,EAAE,OAAO,CAAA;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAC1B,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,SAAS,GACxD,QAAQ,GAAG,WAAW,GAAG,OAAO,GAAG,UAAU,GAAG,kBAAkB,GAClE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAA;AAEhC,kGAAkG;AAClG,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,CAAA;AAEvE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAA;IACxB,IAAI,EAAE,cAAc,CAAA;IACpB,iFAAiF;IACjF,IAAI,CAAC,EAAE,IAAI,CAAA;CACZ;AAED,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,eAAe,GAAG,OAAO,CAAA;AAEvE,wEAAwE;AACxE,MAAM,MAAM,aAAa,GAAG,YAAY,GAAG;IAAE,IAAI,EAAE,cAAc,CAAA;CAAE,CAAA;AAEnE,KAAK,UAAU,CAAC,OAAO,SAAS,aAAa,IAAI,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,GAAG;IACvE,IAAI,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,CAAA;CACrC,CAAA;AAED,+EAA+E;AAC/E,MAAM,MAAM,eAAe,CAAC,OAAO,SAAS,aAAa,IAAI,EAAE,SAAS,UAAU,CAAC,OAAO,CAAC,GACvF,CAAC,OAAO,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,GAC7C,CAAC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,CAAA;AAEhD,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,iGAAiG;IACjG,UAAU,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,mCAAmC;IACnC,IAAI,EAAE,mBAAmB,CAAA;IACzB,iGAAiG;IACjG,KAAK,CAAC,EAAE,SAAS,CAAA;IACjB,mEAAmE;IACnE,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAChD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,EAAE,SAAS,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,SAAS,CAAA;IAChB,KAAK,EAAE,MAAM,CAAA;IACb,uDAAuD;IACvD,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,cAAc,EAAE,CAAA;IAC9B,wFAAwF;IACxF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-payment",
3
- "version": "0.1.18-rc.20",
3
+ "version": "0.1.18-rc.21",
4
4
  "license": "MIT",
5
5
  "description": "Protocol-bound payment hooks and themed amount checkout UI for OwlMeans web applications.",
6
6
  "type": "module",
@@ -20,16 +20,26 @@
20
20
  "default": "./build/index.js",
21
21
  "module": "./build/index.js",
22
22
  "types": "./build/index.d.ts"
23
+ },
24
+ "./consumer": {
25
+ "import": "./build/consumer/index.js",
26
+ "require": "./build/consumer/index.js",
27
+ "default": "./build/consumer/index.js",
28
+ "module": "./build/consumer/index.js",
29
+ "types": "./build/consumer/index.d.ts"
23
30
  }
24
31
  },
25
32
  "dependencies": {
26
- "@owlmeans/client-i18n": "^0.1.18-rc.38",
27
- "@owlmeans/client-payment": "^0.1.18-rc.45",
28
- "@owlmeans/entrypoint": "^0.1.18-rc.35",
29
- "@owlmeans/i18n": "^0.1.18-rc.35",
30
- "@owlmeans/payment": "^0.1.18-rc.37"
33
+ "@owlmeans/api": "^0.1.18-rc.37",
34
+ "@owlmeans/client-i18n": "^0.1.18-rc.39",
35
+ "@owlmeans/client-payment": "^0.1.18-rc.46",
36
+ "@owlmeans/entrypoint": "^0.1.18-rc.36",
37
+ "@owlmeans/error": "^0.1.18-rc.35",
38
+ "@owlmeans/i18n": "^0.1.18-rc.36",
39
+ "@owlmeans/payment": "^0.1.18-rc.38"
31
40
  },
32
41
  "peerDependencies": {
42
+ "@radix-ui/react-checkbox": "*",
33
43
  "@radix-ui/react-dialog": "*",
34
44
  "@radix-ui/react-label": "*",
35
45
  "@radix-ui/react-progress": "*",
@@ -44,7 +54,8 @@
44
54
  },
45
55
  "devDependencies": {
46
56
  "@owlmeans/dep-config": "workspace:*",
47
- "@owlmeans/test-ui": "^0.1.18-rc.37",
57
+ "@owlmeans/test-ui": "^0.1.18-rc.38",
58
+ "@radix-ui/react-checkbox": "^1.3.3",
48
59
  "@radix-ui/react-dialog": "^1.1.15",
49
60
  "@radix-ui/react-label": "^2.1.0",
50
61
  "@radix-ui/react-progress": "^1.1.0",
@@ -0,0 +1,23 @@
1
+ import * as React from 'react'
2
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
3
+ import { cn } from '@/lib/utils'
4
+
5
+ /** The shadcn checkbox, with an inline check mark instead of an icon library. */
6
+ export const Checkbox = ({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) =>
7
+ <CheckboxPrimitive.Root
8
+ data-slot="checkbox"
9
+ className={cn(
10
+ 'peer border-input size-4 shrink-0 cursor-pointer rounded-[4px] border shadow-xs outline-none transition-shadow',
11
+ 'data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=checked]:border-primary',
12
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
13
+ className,
14
+ )}
15
+ {...props}
16
+ >
17
+ <CheckboxPrimitive.Indicator data-slot="checkbox-indicator" className="flex items-center justify-center text-current">
18
+ <svg viewBox="0 0 24 24" aria-hidden="true" className="size-3.5" fill="none" stroke="currentColor"
19
+ strokeWidth={3} strokeLinecap="round" strokeLinejoin="round">
20
+ <path d="M20 6 9 17l-5-5" />
21
+ </svg>
22
+ </CheckboxPrimitive.Indicator>
23
+ </CheckboxPrimitive.Root>
@@ -0,0 +1,48 @@
1
+ import { useI18nLib, useLanguage } from '@owlmeans/client-i18n'
2
+ import { cn } from '@/lib/utils'
3
+ import { money, shortMomentUtc } from './format.js'
4
+ import type { CheckoutLimitNoteProps } from './types.js'
5
+
6
+ /** The reasons the package phrases itself; any other reads the generic sentence. */
7
+ const KNOWN_REASONS = ['per-purchase', 'window', 'hold']
8
+
9
+ /**
10
+ * What an amount checkout is narrowed to for this entity now: the largest amount (or that nothing
11
+ * may be bought), why, and when it rises again — the same `CheckoutLimitView` the server refuses
12
+ * with, so the note and a refusal never disagree.
13
+ *
14
+ * `[data-checkout-limit]` carries `data-max-minor`, `data-blocked`, `data-reason` and, when known,
15
+ * `data-resets-at` (ISO).
16
+ */
17
+ export const CheckoutLimitNote = ({ limit, reasonLabel, className }: CheckoutLimitNoteProps) => {
18
+ const t = useI18nLib('web-payment', 'checkout-limit')
19
+ const [locale] = useLanguage()
20
+ if (limit == null || (!limit.narrowed && !limit.blocked)) {
21
+ return null
22
+ }
23
+
24
+ const reason = limit.reason ?? ''
25
+ const custom = typeof reasonLabel === 'function' ? reasonLabel(reason) : reasonLabel
26
+ const explained = custom != null && custom !== ''
27
+ ? custom
28
+ : t(`reason.${KNOWN_REASONS.includes(reason) ? reason : 'other'}`)
29
+ const resetsAt = limit.resetsAt != null ? new Date(limit.resetsAt) : null
30
+ const resets = resetsAt != null && !Number.isNaN(resetsAt.getTime())
31
+ ? t('resets', { date: shortMomentUtc(resetsAt, locale), interpolation: { escapeValue: false } })
32
+ : null
33
+
34
+ return <div
35
+ data-checkout-limit="" data-max-minor={limit.maximumMinor} data-blocked={limit.blocked ? 'true' : 'false'}
36
+ data-reason={reason} data-resets-at={resetsAt?.toISOString()}
37
+ role={limit.blocked ? 'alert' : 'note'}
38
+ className={cn('grid gap-1 rounded-lg border p-3 text-sm', limit.blocked ? 'border-destructive/50' : 'bg-muted/30', className)}
39
+ >
40
+ <p className={limit.blocked ? 'text-destructive font-medium' : 'font-medium'} data-checkout-limit-headline="">
41
+ {limit.blocked
42
+ ? t('blocked')
43
+ : t('note', { amount: money(limit.maximumMinor, limit.currency, locale), interpolation: { escapeValue: false } })}
44
+ </p>
45
+ <p className="text-muted-foreground text-xs" data-checkout-limit-reason="">{explained}</p>
46
+ {resets != null && <p className="text-muted-foreground text-xs" data-checkout-limit-resets="">{resets}</p>}
47
+ </div>
48
+ }
@@ -0,0 +1,214 @@
1
+ import { useEffect, useState, type ReactNode } from 'react'
2
+ import { CancellationKind } from '@owlmeans/payment'
3
+ import type { CancellationBody, CancellationReceipt } from '@owlmeans/payment'
4
+ import { cn } from '@/lib/utils'
5
+ import { Button } from '@/components/ui/button'
6
+ import { dayUtc, momentUtc } from '../format.js'
7
+ import { useLegalText, usePaymentText } from './copy.js'
8
+ import { Choice, Field, Honeypot, isEmail } from './fields.js'
9
+ import { ErrorLine, LanguageToggle, useShownLanguage } from './legal.js'
10
+ import type { CancellationFormProps, DeclarationStep } from './types.js'
11
+
12
+ const isCancellationReceipt = (receipt: object): receipt is CancellationReceipt => 'status' in receipt
13
+
14
+ /** Today as a UTC calendar day, `YYYY-MM-DD` — the earliest date a termination may name. */
15
+ const todayUtc = (): string => new Date().toISOString().slice(0, 10)
16
+
17
+ /**
18
+ * The cancellation function (§ 312k BGB "Verträge hier kündigen" → "Jetzt kündigen"; L215-1-1 /
19
+ * D215-1 "Résilier votre contrat" → a summary → "Notification de la résiliation"), for a public
20
+ * page reachable without a login and for the same function in-app.
21
+ *
22
+ * Step 1 asks for the kind (ordinary, or extraordinary with its reason), the name, the contract or
23
+ * customer reference, the date (the earliest possible, or a specific one) and the e-mail address
24
+ * the acknowledgement goes to; a honeypot field no person reaches rides along. Step 2 is the
25
+ * summary with the statutory confirm button; step 3 the receipt — date and time of receipt in UTC,
26
+ * the effective date when known — with a print button, so it can be kept.
27
+ *
28
+ * Selectors: `[data-cancellation-form]` / `[data-cancellation-step="form|review|receipt"]` (also
29
+ * `data-step`, `data-language`, `data-mode`), `[data-cancellation-field="kind|reason|name|contract|
30
+ * effective|date|email"]`, `[data-cancellation-continue]`, `[data-cancellation-confirm]`,
31
+ * `[data-cancellation-receipt]` (`data-status`, `data-declaration-id`, `data-received-at`,
32
+ * `data-effective-at`), `[data-cancellation-print]`.
33
+ */
34
+ export const CancellationForm = ({
35
+ mode, language: contractLanguage, uiLanguage, defaults, pending = false, error, receipt, onSubmit, onPrint, onClose,
36
+ className,
37
+ }: CancellationFormProps) => {
38
+ const language = useShownLanguage(contractLanguage, uiLanguage)
39
+ const legal = useLegalText(language.shown)
40
+ const text = usePaymentText(language.shown)
41
+
42
+ const [step, setStep] = useState<'form' | 'review'>('form')
43
+ const [kind, setKind] = useState<CancellationKind>(CancellationKind.Ordinary)
44
+ const [reason, setReason] = useState('')
45
+ const [name, setName] = useState(defaults?.name ?? '')
46
+ const [contractRef, setContractRef] = useState(defaults?.contractRef ?? '')
47
+ const [effective, setEffective] = useState<'earliest' | 'date'>('earliest')
48
+ const [date, setDate] = useState('')
49
+ const [email, setEmail] = useState(defaults?.email ?? '')
50
+ const [honeypot, setHoneypot] = useState('')
51
+ const [checked, setChecked] = useState(false)
52
+ const [submitted, setSubmitted] = useState<CancellationBody | null>(null)
53
+ useEffect(() => {
54
+ if (receipt == null && submitted != null) setStep('form')
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [receipt])
57
+
58
+ const current: DeclarationStep = receipt != null ? 'receipt' : step
59
+ const extraordinary = kind === CancellationKind.Extraordinary
60
+ const earliestDate = todayUtc()
61
+ const errors = {
62
+ reason: extraordinary && reason.trim() === '' ? text('consumer.required') : null,
63
+ name: name.trim() === '' ? text('consumer.required') : null,
64
+ date: effective === 'date' && (!/^\d{4}-\d{2}-\d{2}$/.test(date) || date < earliestDate) ? text('consumer.date-invalid') : null,
65
+ email: email.trim() === '' ? text('consumer.required') : !isEmail(email.trim()) ? text('consumer.email-invalid') : null,
66
+ }
67
+ const shown = (key: keyof typeof errors) => checked ? errors[key] : null
68
+ const valid = Object.values(errors).every(value => value == null)
69
+ const body = (): CancellationBody => ({
70
+ kind,
71
+ ...(extraordinary ? { reason: reason.trim() } : {}),
72
+ name: name.trim(),
73
+ ...(contractRef.trim() !== '' ? { contractRef: contractRef.trim() } : {}),
74
+ ...(defaults?.subscriptionId != null ? { subscriptionId: defaults.subscriptionId } : {}),
75
+ effective,
76
+ ...(effective === 'date' ? { date } : {}),
77
+ email: email.trim(),
78
+ language: language.shown,
79
+ ...(honeypot !== '' ? { honeypot } : {}),
80
+ })
81
+ const toReview = () => {
82
+ setChecked(true)
83
+ if (valid) setStep('review')
84
+ }
85
+ const confirm = async () => {
86
+ if (!valid || pending) return
87
+ const declared = body()
88
+ setSubmitted(declared)
89
+ await onSubmit(declared)
90
+ }
91
+ const print = () => {
92
+ if (onPrint != null) onPrint()
93
+ else if (typeof window !== 'undefined') window.print()
94
+ }
95
+
96
+ const kindLabel = (value: CancellationKind) => legal(value === CancellationKind.Extraordinary
97
+ ? 'cancellation.kind-extraordinary' : 'cancellation.kind-ordinary')
98
+ const effectiveLabel = (declared: CancellationBody) => declared.effective === 'date' && declared.date != null
99
+ ? dayUtc(`${declared.date}T00:00:00Z`, language.shown) : legal('cancellation.effective-earliest')
100
+ const summary = (declared: CancellationBody, hook?: string) =>
101
+ <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 rounded-lg border bg-muted/30 p-4 text-sm"
102
+ {...(hook != null ? { [hook]: '' } : {})}>
103
+ <dt className="text-muted-foreground">{legal('cancellation.kind')}</dt><dd data-summary="kind">{kindLabel(declared.kind)}</dd>
104
+ {declared.reason != null && <>
105
+ <dt className="text-muted-foreground">{legal('cancellation.reason')}</dt>
106
+ <dd data-summary="reason" className="whitespace-pre-wrap">{declared.reason}</dd>
107
+ </>}
108
+ <dt className="text-muted-foreground">{legal('cancellation.name')}</dt><dd data-summary="name">{declared.name}</dd>
109
+ <dt className="text-muted-foreground">{legal('cancellation.contract')}</dt><dd data-summary="contract">{declared.contractRef ?? '—'}</dd>
110
+ <dt className="text-muted-foreground">{legal('cancellation.effective')}</dt><dd data-summary="effective">{effectiveLabel(declared)}</dd>
111
+ <dt className="text-muted-foreground">{legal('cancellation.email')}</dt><dd data-summary="email">{declared.email}</dd>
112
+ </dl>
113
+
114
+ let content: ReactNode
115
+ if (current === 'receipt' && receipt != null) {
116
+ const receivedAt = new Date(receipt.receivedAt)
117
+ const declared = submitted ?? body()
118
+ const status = isCancellationReceipt(receipt) ? receipt.status : undefined
119
+ const effectiveAt = isCancellationReceipt(receipt) && receipt.effectiveAt != null ? new Date(receipt.effectiveAt) : null
120
+ const effectiveKnown = effectiveAt != null && !Number.isNaN(effectiveAt.getTime())
121
+ content = <div className="grid gap-4" data-cancellation-receipt="" data-status={status ?? 'received'}
122
+ data-declaration-id={receipt.declarationId}
123
+ data-received-at={Number.isNaN(receivedAt.getTime()) ? undefined : receivedAt.toISOString()}
124
+ data-effective-at={effectiveKnown ? effectiveAt.toISOString() : undefined}>
125
+ <h3 className="text-sm font-medium">{text('consumer.receipt')}</h3>
126
+ <p className="text-sm">{receipt.mailed
127
+ ? legal('cancellation.received', { date: momentUtc(receivedAt, language.shown), email: declared.email })
128
+ : `${text('consumer.received-at')}: ${momentUtc(receivedAt, language.shown)} (UTC). ${text('consumer.not-mailed')}`}</p>
129
+ {effectiveKnown && <p className="text-sm font-medium" data-cancellation-effective="">
130
+ {text('cancellation.effective-at', { date: dayUtc(effectiveAt, language.shown) })}
131
+ </p>}
132
+ <dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 rounded-lg border p-4 text-sm">
133
+ <dt className="text-muted-foreground">{text('consumer.reference')}</dt>
134
+ <dd data-cancellation-reference="">{receipt.declarationId}</dd>
135
+ <dt className="text-muted-foreground">{text('consumer.received-at')}</dt>
136
+ <dd data-cancellation-received-at="">{momentUtc(receivedAt, language.shown)} (UTC)</dd>
137
+ {status != null && <>
138
+ <dt className="text-muted-foreground">{text('consumer.status')}</dt>
139
+ <dd data-cancellation-status="">{text(`cancellation.status.${status}`)}</dd>
140
+ </>}
141
+ </dl>
142
+ {summary(declared)}
143
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end print:hidden">
144
+ {onClose != null && <Button type="button" variant="outline" onClick={onClose}>{text('consumer.close')}</Button>}
145
+ <Button type="button" variant="outline" onClick={print} data-cancellation-print="">{text('consumer.print')}</Button>
146
+ </div>
147
+ </div>
148
+ } else if (current === 'review') {
149
+ content = <div className="grid gap-4">
150
+ <h3 className="text-sm font-medium">{text('consumer.review')}</h3>
151
+ <p className="text-sm">{legal('cancellation.summary')}</p>
152
+ {summary(body(), 'data-cancellation-summary')}
153
+ <ErrorLine error={error} text={text} />
154
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
155
+ <Button type="button" variant="outline" disabled={pending} onClick={() => setStep('form')}
156
+ data-cancellation-back="">{text('consumer.back')}</Button>
157
+ <Button type="button" disabled={pending || !valid} onClick={() => void confirm()} data-cancellation-confirm="">
158
+ {pending ? text('consumer.pending') : legal('cancellation.confirm')}
159
+ </Button>
160
+ </div>
161
+ </div>
162
+ } else {
163
+ content = <form className="relative grid gap-4" noValidate onSubmit={event => { event.preventDefault(); toReview() }}>
164
+ <fieldset className="grid gap-2">
165
+ <legend className="mb-1 text-sm font-medium">{legal('cancellation.kind')}</legend>
166
+ {[CancellationKind.Ordinary, CancellationKind.Extraordinary].map(value => <Choice
167
+ key={value} id={`cancellation-kind-${value}`} name="cancellation-kind" checked={kind === value}
168
+ onSelect={() => setKind(value)} disabled={pending} hook={{ 'data-cancellation-field': 'kind', 'data-kind': value }}
169
+ >{kindLabel(value)}</Choice>)}
170
+ </fieldset>
171
+ {extraordinary && <Field id="cancellation-reason" label={legal('cancellation.reason')} value={reason} onChange={setReason}
172
+ error={shown('reason')} disabled={pending} multiline hook={{ 'data-cancellation-field': 'reason' }} />}
173
+ <Field id="cancellation-name" label={legal('cancellation.name')} value={name} onChange={setName} error={shown('name')}
174
+ autoComplete="name" disabled={pending} hook={{ 'data-cancellation-field': 'name' }} />
175
+ <Field id="cancellation-contract" label={legal('cancellation.contract')} value={contractRef} onChange={setContractRef}
176
+ disabled={pending} hook={{ 'data-cancellation-field': 'contract' }} />
177
+ <fieldset className="grid gap-2">
178
+ <legend className="mb-1 text-sm font-medium">{legal('cancellation.effective')}</legend>
179
+ <Choice id="cancellation-effective-earliest" name="cancellation-effective" checked={effective === 'earliest'}
180
+ onSelect={() => setEffective('earliest')} disabled={pending}
181
+ hook={{ 'data-cancellation-field': 'effective', 'data-effective': 'earliest' }}>
182
+ {legal('cancellation.effective-earliest')}
183
+ </Choice>
184
+ <Choice id="cancellation-effective-date" name="cancellation-effective" checked={effective === 'date'}
185
+ onSelect={() => setEffective('date')} disabled={pending}
186
+ hook={{ 'data-cancellation-field': 'effective', 'data-effective': 'date' }}>
187
+ {legal('cancellation.effective-date')}
188
+ </Choice>
189
+ {effective === 'date' && <Field id="cancellation-date" type="date" label={legal('cancellation.effective-date')}
190
+ value={date} onChange={setDate} min={earliestDate} error={shown('date')} disabled={pending}
191
+ hook={{ 'data-cancellation-field': 'date' }} />}
192
+ </fieldset>
193
+ <Field id="cancellation-email" type="email" label={legal('cancellation.email')} value={email} onChange={setEmail}
194
+ error={shown('email')} autoComplete="email" disabled={pending} hook={{ 'data-cancellation-field': 'email' }} />
195
+ <Honeypot id="cancellation-website" label={text('consumer.website')} value={honeypot} onChange={setHoneypot} />
196
+ <ErrorLine error={error} text={text} />
197
+ <div className="flex justify-end">
198
+ <Button type="submit" disabled={pending} data-cancellation-continue="">{text('consumer.continue')}</Button>
199
+ </div>
200
+ </form>
201
+ }
202
+
203
+ return <section className={cn('grid gap-4', className)} data-cancellation-section="" data-language={language.shown}>
204
+ <header className="grid gap-1.5" lang={language.shown}>
205
+ <h2 className="text-lg font-semibold leading-none">{legal('cancellation.title')}</h2>
206
+ {current === 'form' && <p className="text-muted-foreground text-sm">{legal('cancellation.intro')}</p>}
207
+ </header>
208
+ <LanguageToggle language={language} hook="data-cancellation-language-toggle" />
209
+ <div data-cancellation-form="" data-cancellation-step={current} data-step={current} data-mode={mode}
210
+ data-language={language.shown} lang={language.shown} className="grid gap-4">
211
+ {content}
212
+ </div>
213
+ </section>
214
+ }
@@ -0,0 +1,93 @@
1
+ import { useMemo } from 'react'
2
+ import { LIB_NAMESPACE, resolveI18nResource } from '@owlmeans/i18n'
3
+ import { baseLanguageOf, consumerText } from '@owlmeans/payment'
4
+ import type { CopyValues } from '@owlmeans/payment'
5
+ import { WEB_PAYMENT_RESOURCE } from '../i18n.js'
6
+
7
+ interface Tree {
8
+ [key: string]: string | Tree
9
+ }
10
+
11
+ const isTree = (value: unknown): value is Tree =>
12
+ value != null && typeof value === 'object' && !Array.isArray(value)
13
+
14
+ const merge = (base: Tree, over: Record<string, unknown> | null): Tree => {
15
+ const result: Tree = { ...base }
16
+ for (const [key, value] of Object.entries(over ?? {})) {
17
+ if (isTree(value)) {
18
+ const current = result[key]
19
+ result[key] = merge(isTree(current) ? current : {}, value)
20
+ } else if (typeof value === 'string') {
21
+ result[key] = value
22
+ }
23
+ }
24
+
25
+ return result
26
+ }
27
+
28
+ const bundleOf = (lng: string): Tree => {
29
+ const base = baseLanguageOf(lng)
30
+ let tree = merge({}, resolveI18nResource('en', WEB_PAYMENT_RESOURCE, LIB_NAMESPACE))
31
+ if (base !== '' && base !== 'en') {
32
+ tree = merge(tree, resolveI18nResource(base, WEB_PAYMENT_RESOURCE, LIB_NAMESPACE))
33
+ }
34
+ if (lng !== base && lng.trim() !== '') {
35
+ tree = merge(tree, resolveI18nResource(lng, WEB_PAYMENT_RESOURCE, LIB_NAMESPACE))
36
+ }
37
+
38
+ return tree
39
+ }
40
+
41
+ const fill = (text: string, values: CopyValues = {}): string =>
42
+ text.replace(/\{\{\s*([\w-]+)\s*\}\}/g, (_, name: string) => values[name] != null ? String(values[name]) : '')
43
+
44
+ /** A translator bound to ONE language, whatever the active i18n language is. */
45
+ export type FixedText = (key: string, values?: CopyValues) => string
46
+
47
+ /**
48
+ * The package's own interface strings (`web-payment`, library tier, any application override
49
+ * included) in a FIXED language, merged key by key over English — no i18next instance and no
50
+ * draining (`resolveI18nResource`). What lets a consumer-rights dialog render wholly in the
51
+ * contract language while the rest of the page stays in the interface language.
52
+ */
53
+ export const paymentTextOf = (lng: string): FixedText => {
54
+ const tree = bundleOf(lng)
55
+
56
+ return (key, values) => {
57
+ const value = key.split('.').reduce<unknown>((node, part) => isTree(node) ? node[part] : undefined, tree)
58
+
59
+ return typeof value === 'string' ? fill(value, values) : key
60
+ }
61
+ }
62
+
63
+ /**
64
+ * The legal copy of `@owlmeans/payment` (`payment-consumer-rights`) in a fixed language. A text
65
+ * that cannot be rendered in the language (a broken application override) falls back to English
66
+ * rather than blanking the page; one that cannot be rendered at all is `''`.
67
+ */
68
+ export const legalTextOf = (lng: string): FixedText => (path, values) => {
69
+ try {
70
+ return consumerText(lng, path, values)
71
+ } catch {
72
+ try {
73
+ return consumerText('en', path, values)
74
+ } catch {
75
+ return ''
76
+ }
77
+ }
78
+ }
79
+
80
+ /** `paymentTextOf(lng)`, memoised for the language. */
81
+ export const usePaymentText = (lng: string): FixedText => useMemo(() => paymentTextOf(lng), [lng])
82
+
83
+ /** `legalTextOf(lng)`, memoised for the language. */
84
+ export const useLegalText = (lng: string): FixedText => useMemo(() => legalTextOf(lng), [lng])
85
+
86
+ /** A language's own name in another language (`de` in `en` → "German"); the code when unknown. */
87
+ export const languageNameOf = (lng: string, inLanguage: string): string => {
88
+ try {
89
+ return new Intl.DisplayNames([inLanguage], { type: 'language' }).of(lng) ?? lng
90
+ } catch {
91
+ return lng
92
+ }
93
+ }