@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.
- package/README.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-payment/SKILL.md +171 -31
- package/build/@/components/ui/checkbox.d.ts +5 -0
- package/build/@/components/ui/checkbox.d.ts.map +1 -0
- package/build/@/components/ui/checkbox.js +6 -0
- package/build/@/components/ui/checkbox.js.map +1 -0
- package/build/checkout-limit.d.ts +11 -0
- package/build/checkout-limit.d.ts.map +1 -0
- package/build/checkout-limit.js +34 -0
- package/build/checkout-limit.js.map +1 -0
- package/build/consumer/cancellation.d.ts +20 -0
- package/build/consumer/cancellation.d.ts.map +1 -0
- package/build/consumer/cancellation.js +115 -0
- package/build/consumer/cancellation.js.map +1 -0
- package/build/consumer/copy.d.ts +23 -0
- package/build/consumer/copy.d.ts.map +1 -0
- package/build/consumer/copy.js +75 -0
- package/build/consumer/copy.js.map +1 -0
- package/build/consumer/ensure.d.ts +56 -0
- package/build/consumer/ensure.d.ts.map +1 -0
- package/build/consumer/ensure.js +71 -0
- package/build/consumer/ensure.js.map +1 -0
- package/build/consumer/fields.d.ts +43 -0
- package/build/consumer/fields.d.ts.map +1 -0
- package/build/consumer/fields.js +19 -0
- package/build/consumer/fields.js.map +1 -0
- package/build/consumer/hooks.d.ts +113 -0
- package/build/consumer/hooks.d.ts.map +1 -0
- package/build/consumer/hooks.js +225 -0
- package/build/consumer/hooks.js.map +1 -0
- package/build/consumer/index.d.ts +14 -0
- package/build/consumer/index.d.ts.map +1 -0
- package/build/consumer/index.js +12 -0
- package/build/consumer/index.js.map +1 -0
- package/build/consumer/legal.d.ts +53 -0
- package/build/consumer/legal.d.ts.map +1 -0
- package/build/consumer/legal.js +77 -0
- package/build/consumer/legal.js.map +1 -0
- package/build/consumer/performance-consent-dialog.d.ts +18 -0
- package/build/consumer/performance-consent-dialog.d.ts.map +1 -0
- package/build/consumer/performance-consent-dialog.js +71 -0
- package/build/consumer/performance-consent-dialog.js.map +1 -0
- package/build/consumer/provider.d.ts +33 -0
- package/build/consumer/provider.d.ts.map +1 -0
- package/build/consumer/provider.js +26 -0
- package/build/consumer/provider.js.map +1 -0
- package/build/consumer/refusal.d.ts +35 -0
- package/build/consumer/refusal.d.ts.map +1 -0
- package/build/consumer/refusal.js +98 -0
- package/build/consumer/refusal.js.map +1 -0
- package/build/consumer/subscription-start-dialog.d.ts +16 -0
- package/build/consumer/subscription-start-dialog.d.ts.map +1 -0
- package/build/consumer/subscription-start-dialog.js +43 -0
- package/build/consumer/subscription-start-dialog.js.map +1 -0
- package/build/consumer/types.d.ts +114 -0
- package/build/consumer/types.d.ts.map +1 -0
- package/build/consumer/types.js +2 -0
- package/build/consumer/types.js.map +1 -0
- package/build/consumer/withdrawal.d.ts +29 -0
- package/build/consumer/withdrawal.d.ts.map +1 -0
- package/build/consumer/withdrawal.js +140 -0
- package/build/consumer/withdrawal.js.map +1 -0
- package/build/dialog.d.ts +1 -1
- package/build/dialog.d.ts.map +1 -1
- package/build/dialog.js +42 -16
- package/build/dialog.js.map +1 -1
- package/build/estimate-summary.d.ts +9 -1
- package/build/estimate-summary.d.ts.map +1 -1
- package/build/estimate-summary.js +10 -7
- package/build/estimate-summary.js.map +1 -1
- package/build/estimate.d.ts +6 -0
- package/build/estimate.d.ts.map +1 -1
- package/build/estimate.js +26 -1
- package/build/estimate.js.map +1 -1
- package/build/format.d.ts +15 -0
- package/build/format.d.ts.map +1 -0
- package/build/format.js +68 -0
- package/build/format.js.map +1 -0
- package/build/i18n/be.json +1 -1
- package/build/i18n/de.json +1 -1
- package/build/i18n/en.json +1 -1
- package/build/i18n/es.json +1 -1
- package/build/i18n/fr.json +110 -15
- package/build/i18n/pl.json +1 -1
- package/build/i18n/ru.json +1 -1
- package/build/i18n/uk.json +1 -1
- package/build/i18n.d.ts +2 -1
- package/build/i18n.d.ts.map +1 -1
- package/build/i18n.js +10 -8
- package/build/i18n.js.map +1 -1
- package/build/index.d.ts +2 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/types.d.ts +22 -1
- package/build/types.d.ts.map +1 -1
- package/package.json +18 -7
- package/src/@/components/ui/checkbox.tsx +23 -0
- package/src/checkout-limit.tsx +48 -0
- package/src/consumer/cancellation.tsx +214 -0
- package/src/consumer/copy.ts +93 -0
- package/src/consumer/ensure.ts +124 -0
- package/src/consumer/fields.tsx +89 -0
- package/src/consumer/hooks.ts +358 -0
- package/src/consumer/index.ts +13 -0
- package/src/consumer/legal.tsx +129 -0
- package/src/consumer/performance-consent-dialog.tsx +132 -0
- package/src/consumer/provider.tsx +49 -0
- package/src/consumer/refusal.ts +110 -0
- package/src/consumer/subscription-start-dialog.tsx +87 -0
- package/src/consumer/types.ts +121 -0
- package/src/consumer/withdrawal.tsx +279 -0
- package/src/dialog.tsx +53 -23
- package/src/estimate-summary.tsx +21 -11
- package/src/estimate.ts +27 -1
- package/src/format.ts +71 -0
- package/src/i18n/be.json +1 -1
- package/src/i18n/de.json +1 -1
- package/src/i18n/en.json +1 -1
- package/src/i18n/es.json +1 -1
- package/src/i18n/fr.json +110 -15
- package/src/i18n/pl.json +1 -1
- package/src/i18n/ru.json +1 -1
- package/src/i18n/uk.json +1 -1
- package/src/i18n.ts +11 -8
- package/src/index.ts +2 -0
- package/src/types.ts +23 -1
- package/tests/consent-refusal.spec.ts +83 -0
- package/tests/consumer-hooks.spec.ts +205 -0
- package/tests/consumer.spec.ts +343 -0
- package/tests/context.ts +1 -1
- package/tests/harness/consumer-fixtures.ts +105 -0
- package/tests/harness/consumer.tsx +197 -0
- package/tests/harness/mount.tsx +9 -2
- 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,
|
|
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';
|
package/build/index.d.ts.map
CHANGED
|
@@ -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';
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,WAAW,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;
|
package/build/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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.
|
|
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/
|
|
27
|
-
"@owlmeans/client-
|
|
28
|
-
"@owlmeans/
|
|
29
|
-
"@owlmeans/
|
|
30
|
-
"@owlmeans/
|
|
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.
|
|
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
|
+
}
|