@eifi1/ui-kit 0.38.1 → 0.39.0
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 +6 -5
- package/dist/auth/auth-errors.d.ts +8 -3
- package/dist/auth/auth-errors.js +3 -1
- package/dist/auth/auth-errors.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +25 -2
- package/dist/billing/billing-labels.js +11 -2
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +50 -1
- package/dist/billing/checkout-processing.d.ts +15 -3
- package/dist/billing/checkout-processing.js +3 -1
- package/dist/billing/checkout-processing.js.map +1 -1
- package/dist/billing/pay-page.d.ts +55 -18
- package/dist/billing/pay-page.js +44 -10
- package/dist/billing/pay-page.js.map +1 -1
- package/dist/billing/plan-switch.d.ts +79 -0
- package/dist/billing/plan-switch.js +93 -0
- package/dist/billing/plan-switch.js.map +1 -0
- package/dist/bin/eifi1-pay-page.mjs +109 -21
- package/dist/eslint/index.js +1 -1
- package/dist/i18n/german.js +13 -2
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/locales/en.js +14 -3
- package/dist/i18n/locales/en.js.map +2 -2
- package/dist/i18n/locales/es.js +13 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +13 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +13 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +13 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +13 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +3 -0
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/translation-review.d.ts +11 -0
- package/dist/lib/translation-review.js.map +1 -1
- package/dist/pay/pay.js +102 -40
- package/package.json +1 -1
- package/src/auth/auth-errors.ts +12 -3
- package/src/billing/billing-labels.ts +38 -3
- package/src/billing/billing-overview.ts +53 -1
- package/src/billing/checkout-processing.ts +17 -3
- package/src/billing/pay-page.ts +111 -27
- package/src/billing/plan-switch.tsx +201 -0
- package/src/i18n/german.ts +13 -1
- package/src/i18n/locales/es.ts +12 -1
- package/src/i18n/locales/fr.ts +12 -2
- package/src/i18n/locales/hu.ts +12 -1
- package/src/i18n/locales/it.ts +12 -1
- package/src/i18n/locales/zh.ts +12 -1
- package/src/i18n/review.ts +3 -0
- package/src/index.ts +8 -0
- package/src/lib/translation-review.ts +12 -0
|
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useEffectEvent, useLayoutEffect, useRef, useSyn
|
|
|
3
3
|
import { toDate } from "../lib/format";
|
|
4
4
|
import type { DateInput } from "../lib/format";
|
|
5
5
|
import { readStored, removeStored, writeStored } from "../lib/safe-storage";
|
|
6
|
+
import type { PaidPrice } from "./billing-overview";
|
|
6
7
|
import type { BillingStanding } from "./billing-standing";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -43,6 +44,9 @@ export type CheckoutOverview = Pick<BillingStanding, "status" | "in_good_standin
|
|
|
43
44
|
plan: string;
|
|
44
45
|
source: string;
|
|
45
46
|
current_period_end?: DateInput;
|
|
47
|
+
/** 0.39 (§15.2.10): what the payer pays — a plan switch moves only this under a grant,
|
|
48
|
+
* and keeps `active` without one. Left out or null: nothing paid. */
|
|
49
|
+
paid?: Pick<PaidPrice, "plan" | "interval"> | null;
|
|
46
50
|
};
|
|
47
51
|
|
|
48
52
|
/** How long a return keeps the page in "processing" before it trusts the overview. */
|
|
@@ -53,9 +57,12 @@ const POLL_EVERY = 4_000;
|
|
|
53
57
|
const STORAGE_KEY = "eifi1-billing-checkout";
|
|
54
58
|
|
|
55
59
|
/**
|
|
56
|
-
* What of the overview a completed checkout changes, as one string:
|
|
57
|
-
* without case, §12.16), the status, the source, the standing
|
|
58
|
-
* as an instant, so `…Z` and `…+00:00` agree).
|
|
60
|
+
* What of the overview a completed checkout — or plan switch — changes, as one string:
|
|
61
|
+
* the plan (read without case, §12.16), the status, the source, the standing, the
|
|
62
|
+
* period's end (read as an instant, so `…Z` and `…+00:00` agree) and, since 0.39, the
|
|
63
|
+
* paid plan and period (`paid`, §15.2.10). A switch keeps `active`, and under a grant
|
|
64
|
+
* the plan too, so only `paid` shows it landed: one storage key serves the checkout and
|
|
65
|
+
* the switch.
|
|
59
66
|
*/
|
|
60
67
|
export function checkoutFingerprint(o: CheckoutOverview): string {
|
|
61
68
|
const end = toDate(o.current_period_end ?? null);
|
|
@@ -65,6 +72,8 @@ export function checkoutFingerprint(o: CheckoutOverview): string {
|
|
|
65
72
|
o.source,
|
|
66
73
|
String(o.in_good_standing),
|
|
67
74
|
end ? end.toISOString() : "",
|
|
75
|
+
String(o.paid?.plan ?? "").toLowerCase(),
|
|
76
|
+
o.paid?.interval ?? "",
|
|
68
77
|
].join("|");
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -247,6 +256,11 @@ function claimReturn(store: CheckoutStore): boolean {
|
|
|
247
256
|
* `payer` is the user's or the company's id: a mark never speaks for another payer.
|
|
248
257
|
* `overview` is what the page shows now; left out, the return waits for `active`. Pass
|
|
249
258
|
* the same `storageKey` as the hook's.
|
|
259
|
+
*
|
|
260
|
+
* A plan switch (0.39, §15.2.10) is waited for the same way, under the same key: note
|
|
261
|
+
* the departure before `POST /billing/plan`, and once it answers put the return marker
|
|
262
|
+
* on the page's URL yourself (`checkoutReturnUrl`'s `?checkout=done`) — the row follows
|
|
263
|
+
* by webhook, and the fingerprint's `paid` shows it landed.
|
|
250
264
|
*/
|
|
251
265
|
export function noteCheckoutStarted(
|
|
252
266
|
payer: string | number,
|
package/src/billing/pay-page.ts
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
import type { UiKitLabels } from "../i18n/kit-labels";
|
|
2
|
-
import { checkoutReturnUrl } from "./checkout-return";
|
|
3
2
|
|
|
4
3
|
/**
|
|
5
|
-
* The kit's pay page (docs/billing-harmonization.md §14.4, decision 19 as amended
|
|
6
|
-
* standalone static page that ui-kit ships as `dist/pay/`, and that each app
|
|
7
|
-
* its own pay host, `pay.<app domain>` — a separate origin, approved in Paddle,
|
|
8
|
-
* Paddle.js shares the origin with nothing: no sign-in, no app code, no storage.
|
|
4
|
+
* The kit's pay page (docs/billing-harmonization.md §14.4, decision 19 as amended by
|
|
5
|
+
* §15.2): a standalone static page that ui-kit ships as `dist/pay/`, and that each app
|
|
6
|
+
* serves on its own pay host, `pay.<app domain>` — a separate origin, approved in Paddle,
|
|
7
|
+
* where Paddle.js shares the origin with nothing: no sign-in, no app code, no storage.
|
|
8
|
+
* Since 0.39 every pay page serves ANY app's transaction: it names the app from the
|
|
9
|
+
* transaction's custom data, from the kit's map {@link PAY_PAGE_APPS}, not from its
|
|
10
|
+
* domain — Paddle's single default payment link reaches one host for all three apps.
|
|
9
11
|
*
|
|
10
12
|
* Paddle's create-transaction answers `checkout.url`: the pay page's URL plus
|
|
11
13
|
* `?_ptxn=<txn>`, and Paddle.js on that page opens the transaction by itself. The app
|
|
12
14
|
* sends the buyer there with {@link payPageUrl}, which adds the buyer's language; the
|
|
13
15
|
* page maps it to Paddle's checkout locale ({@link paddleLocale}). Everything else the
|
|
14
|
-
* page needs — the client-side token, sandbox or live, the
|
|
15
|
-
* `pay-config.json`, written at its web build
|
|
16
|
+
* page needs — the client-side token, sandbox or live, the host's app and the map of
|
|
17
|
+
* apps with their ways back — is the host's `pay-config.json`, written at its web build
|
|
18
|
+
* by the `eifi1-pay-page` script.
|
|
16
19
|
*
|
|
17
20
|
* Pure, and React-free: the page's own script (`pay-page/pay.ts`) and that build script
|
|
18
|
-
* bundle this module. The exports
|
|
21
|
+
* bundle this module. The barrel exports the link's parts and the app map ({@link PAY_PAGE_APPS},
|
|
22
|
+
* {@link PayPageApp}); the exports marked @internal are theirs.
|
|
19
23
|
*/
|
|
20
24
|
|
|
21
25
|
/** Paddle's two environments: the sandbox's client-side tokens start `test_`, live
|
|
@@ -138,6 +142,9 @@ export interface PayPageStrings {
|
|
|
138
142
|
failed: string;
|
|
139
143
|
/** `billing.payBack`: the buyer closed the checkout. */
|
|
140
144
|
closed: string;
|
|
145
|
+
/** `billing.payDone` (0.39): paid, for a transaction that names no app the page
|
|
146
|
+
* knows — there is no way back to offer. */
|
|
147
|
+
done: string;
|
|
141
148
|
/** `common.back`: the link to the app's subscription page. */
|
|
142
149
|
back: string;
|
|
143
150
|
/** `legal.links.terms` and `legal.links.privacy`: the footer's links. */
|
|
@@ -154,6 +161,7 @@ export function payPageStrings(labels: UiKitLabels): PayPageStrings {
|
|
|
154
161
|
nothing: labels.billing.payNothing,
|
|
155
162
|
failed: labels.billing.payFailed,
|
|
156
163
|
closed: labels.billing.payBack,
|
|
164
|
+
done: labels.billing.payDone,
|
|
157
165
|
back: labels.common.back,
|
|
158
166
|
terms: labels.legal.links.terms,
|
|
159
167
|
privacy: labels.legal.links.privacy,
|
|
@@ -167,7 +175,45 @@ export function payPageStrings(labels: UiKitLabels): PayPageStrings {
|
|
|
167
175
|
export const PAY_CONFIG_FILE = "pay-config.json";
|
|
168
176
|
|
|
169
177
|
/**
|
|
170
|
-
*
|
|
178
|
+
* One app a pay page serves (docs/billing-harmonization.md §15.2.6): its name, the way
|
|
179
|
+
* back and its Terms and Privacy. Every pay page knows every app, because Paddle's links
|
|
180
|
+
* — its single default payment link, a payment-method update from its e-mails — reach
|
|
181
|
+
* whichever page the account names, whatever app the transaction is for.
|
|
182
|
+
*/
|
|
183
|
+
export interface PayPageApp {
|
|
184
|
+
/** The app's display name: the footer's line and the page's title. */
|
|
185
|
+
name: string;
|
|
186
|
+
/** The app's subscription page. Once paid, the page goes to
|
|
187
|
+
* `checkoutReturnUrl(returnUrl)`; "Back" goes here. */
|
|
188
|
+
returnUrl: string;
|
|
189
|
+
termsUrl: string;
|
|
190
|
+
privacyUrl: string;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** An app's tag: server-kit's `billing_app` pattern, the key of {@link PAY_PAGE_APPS}
|
|
194
|
+
* and the `app` of a transaction's custom data (§14.3). */
|
|
195
|
+
const APP_TAG = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
196
|
+
|
|
197
|
+
/** An app on its site: the name, the Terms and Privacy at `/terms` and `/privacy`, and
|
|
198
|
+
* its subscription page. */
|
|
199
|
+
function appAt(name: string, site: string, returnPath: string): PayPageApp {
|
|
200
|
+
return { name, returnUrl: `${site}${returnPath}`, termsUrl: `${site}/terms`, privacyUrl: `${site}/privacy` };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The apps the kit's pay pages serve, by their `billing_app` tag (§15.2.6) — one source,
|
|
205
|
+
* so the three pay hosts can't drift. A new app, or a page that moves, is a kit release.
|
|
206
|
+
* The bin writes it into `pay-config.json`; `--app-url` moves the host's own app to
|
|
207
|
+
* another site (a local stack, a dev deploy) and `--apps <file>` merges a map over it.
|
|
208
|
+
*/
|
|
209
|
+
export const PAY_PAGE_APPS: Readonly<Record<string, Readonly<PayPageApp>>> = Object.freeze({
|
|
210
|
+
keksdose: Object.freeze(appAt("Keksdose", "https://keksdose.app", "/settings/subscription")),
|
|
211
|
+
kastlan: Object.freeze(appAt("kastlan", "https://kastlan.app", "/admin/billing")),
|
|
212
|
+
kurvenschmiede: Object.freeze(appAt("Kurvenschmiede", "https://kurvenschmiede.app", "/settings/subscription")),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* `pay-config.json`: what the page needs that differs per deployment (§15.2.6). Read by
|
|
171
217
|
* the page from its own origin — never from the query, where Paddle's own links (it
|
|
172
218
|
* puts only `_ptxn` there) would lack it and a return URL would be an open redirect on
|
|
173
219
|
* an approved payment domain. @internal
|
|
@@ -177,19 +223,19 @@ export interface PayPageConfig {
|
|
|
177
223
|
* while billing is off, and the page says `payNothing`. */
|
|
178
224
|
token: string | null;
|
|
179
225
|
environment: PaddleEnvironment | null;
|
|
180
|
-
/** The
|
|
181
|
-
*
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
226
|
+
/** The host's own app, a key of `apps`: the page shows it until the checkout loads,
|
|
227
|
+
* and its Terms and Privacy stay for a transaction that names no app. */
|
|
228
|
+
app: string;
|
|
229
|
+
/** Every app the page serves, by tag. The transaction's `custom_data.app` picks one;
|
|
230
|
+
* a return URL only ever comes from here. */
|
|
231
|
+
apps: Record<string, PayPageApp>;
|
|
186
232
|
}
|
|
187
233
|
|
|
188
234
|
/** Paddle's client-side token prefixes, per environment. */
|
|
189
235
|
const TOKEN_PREFIX: Readonly<Record<PaddleEnvironment, string>> = { sandbox: "test_", live: "live_" };
|
|
190
236
|
|
|
191
|
-
/** `https://`, or `http://` on this machine only (`localhost`, `127.0.0.1`, `[::1]`). */
|
|
192
|
-
function
|
|
237
|
+
/** `https://`, or `http://` on this machine only (`localhost`, `127.0.0.1`, `[::1]`). @internal */
|
|
238
|
+
export function isPayPageUrl(value: unknown): boolean {
|
|
193
239
|
if (typeof value !== "string") return false;
|
|
194
240
|
let url: URL;
|
|
195
241
|
try {
|
|
@@ -201,6 +247,37 @@ function isPageUrl(value: unknown): boolean {
|
|
|
201
247
|
return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
202
248
|
}
|
|
203
249
|
|
|
250
|
+
/** Whether `tag` is a tag of `apps`, its own key — never `__proto__` or `constructor`. */
|
|
251
|
+
function hasApp(apps: unknown, tag: unknown): tag is string {
|
|
252
|
+
return (
|
|
253
|
+
typeof tag === "string" &&
|
|
254
|
+
APP_TAG.test(tag) &&
|
|
255
|
+
typeof apps === "object" &&
|
|
256
|
+
apps !== null &&
|
|
257
|
+
Object.prototype.hasOwnProperty.call(apps, tag)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The app a transaction's custom data names (`custom_data.app`, §14.3), from the page's
|
|
263
|
+
* map — or undefined for none, or a tag the map doesn't know. @internal
|
|
264
|
+
*/
|
|
265
|
+
export function payPageAppFor(config: Pick<PayPageConfig, "apps">, tag: unknown): PayPageApp | undefined {
|
|
266
|
+
return hasApp(config.apps, tag) ? config.apps[tag] : undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** The problems of one app of the map. */
|
|
270
|
+
function appProblems(tag: string, app: unknown): string[] {
|
|
271
|
+
if (typeof app !== "object" || app === null) return [`apps.${tag} is not an object`];
|
|
272
|
+
const a = app as Record<string, unknown>;
|
|
273
|
+
const problems: string[] = [];
|
|
274
|
+
if (typeof a.name !== "string" || a.name.trim() === "") problems.push(`apps.${tag}.name is missing`);
|
|
275
|
+
for (const key of ["returnUrl", "termsUrl", "privacyUrl"] as const) {
|
|
276
|
+
if (!isPayPageUrl(a[key])) problems.push(`apps.${tag}.${key} must be an https:// URL (http:// for localhost only)`);
|
|
277
|
+
}
|
|
278
|
+
return problems;
|
|
279
|
+
}
|
|
280
|
+
|
|
204
281
|
/**
|
|
205
282
|
* What is wrong with a `pay-config.json`, one line each — `[]` when nothing is. The
|
|
206
283
|
* build script refuses to write a file with any (so a wrong pairing fails the build,
|
|
@@ -208,8 +285,10 @@ function isPageUrl(value: unknown): boolean {
|
|
|
208
285
|
*
|
|
209
286
|
* - a token whose prefix disagrees with the environment (`test_` ↔ sandbox, `live_` ↔
|
|
210
287
|
* live), or a token without an environment;
|
|
211
|
-
* -
|
|
212
|
-
*
|
|
288
|
+
* - an app tag that isn't a `billing_app` tag (`^[a-z][a-z0-9_-]{0,63}$`), an app
|
|
289
|
+
* without a name, or a return, terms or privacy URL that isn't `https://` (`http://`
|
|
290
|
+
* for localhost only);
|
|
291
|
+
* - no `app`, or one that isn't in `apps`. @internal
|
|
213
292
|
*/
|
|
214
293
|
export function payPageConfigProblems(config: unknown): string[] {
|
|
215
294
|
if (typeof config !== "object" || config === null) return ["the configuration is not an object"];
|
|
@@ -233,14 +312,19 @@ export function payPageConfigProblems(config: unknown): string[] {
|
|
|
233
312
|
);
|
|
234
313
|
}
|
|
235
314
|
}
|
|
236
|
-
|
|
237
|
-
|
|
315
|
+
const { apps, app } = c;
|
|
316
|
+
if (typeof apps !== "object" || apps === null || Array.isArray(apps) || Object.keys(apps).length === 0) {
|
|
317
|
+
problems.push("apps must map each app's tag to its name and URLs");
|
|
318
|
+
} else {
|
|
319
|
+
for (const [tag, entry] of Object.entries(apps)) {
|
|
320
|
+
if (!APP_TAG.test(tag)) problems.push(`apps: ${JSON.stringify(tag)} is not an app tag (a-z, 0-9, _ and -)`);
|
|
321
|
+
else problems.push(...appProblems(tag, entry));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (typeof app !== "string" || app === "") problems.push("app is missing: the host's own app tag");
|
|
325
|
+
else if (!hasApp(apps, app)) {
|
|
326
|
+
const known = typeof apps === "object" && apps !== null ? Object.keys(apps).join(", ") : "";
|
|
327
|
+
problems.push(`app ${JSON.stringify(app)} is not in apps${known ? ` (${known})` : ""}`);
|
|
238
328
|
}
|
|
239
|
-
if (typeof c.appName !== "string" || c.appName.trim() === "") problems.push("appName is missing");
|
|
240
329
|
return problems;
|
|
241
330
|
}
|
|
242
|
-
|
|
243
|
-
/** Paddle.js's `successUrl` for a configuration: the way back, `?checkout=done`. @internal */
|
|
244
|
-
export function paySuccessUrl(config: Pick<PayPageConfig, "returnUrl">): string {
|
|
245
|
-
return checkoutReturnUrl(config.returnUrl);
|
|
246
|
-
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { KeyboardEvent } from "react";
|
|
2
|
+
|
|
3
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels";
|
|
4
|
+
import type { LabelOverride } from "../i18n/kit-labels";
|
|
5
|
+
import { DEFAULT_COMMON_LABELS } from "../i18n/shared-labels";
|
|
6
|
+
import { pluralForm } from "../i18n/plurals";
|
|
7
|
+
import type { PluralForms } from "../i18n/plurals";
|
|
8
|
+
import { formatDate, formatMoney, formatNumber } from "../lib/format";
|
|
9
|
+
import { Button } from "../components/button";
|
|
10
|
+
import { DialogFrame } from "../components/dialog-frame";
|
|
11
|
+
import { Spinner } from "../components/spinner";
|
|
12
|
+
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock";
|
|
13
|
+
import { DEFAULT_PLAN_CHANGE_LABELS } from "../admin/plan-change-labels";
|
|
14
|
+
import { useBillingLabels } from "./billing-labels";
|
|
15
|
+
import type { BillingLabels } from "./billing-labels";
|
|
16
|
+
import type { PlanSwitchPreview } from "./billing-overview";
|
|
17
|
+
import { minorToMajor } from "./plan-price";
|
|
18
|
+
import type { BillingInterval } from "./plan-price";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The payer's plan switch (docs/billing-harmonization.md §15.2.10, ui-kit 0.39): built
|
|
22
|
+
* once in the kits and used by all three apps, instead of Paddle's portal. While a
|
|
23
|
+
* provider subscription runs, the app sends a `PlanPicker` pick to
|
|
24
|
+
* `POST /billing/plan/preview` and opens this dialog with the answer; Confirm is the
|
|
25
|
+
* app's `POST /billing/plan`. Otherwise the pick goes to the checkout, as before.
|
|
26
|
+
*
|
|
27
|
+
* The switch is immediate and prorated: an upgrade charges the difference now, a
|
|
28
|
+
* downgrade credits the unused part to the customer's balance, which Paddle takes off
|
|
29
|
+
* the next charges. So the payer agrees to a figure — Paddle's preview — and sees the
|
|
30
|
+
* dimensions over the new plan's limit before agreeing (§3.4: a limit gates creation
|
|
31
|
+
* only, so the switch is allowed).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** One side of a switch: a plan code and its billing period. */
|
|
35
|
+
export interface PlanSwitchSide {
|
|
36
|
+
plan: string;
|
|
37
|
+
interval: BillingInterval;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PlanSwitchConfirmProps {
|
|
41
|
+
open: boolean;
|
|
42
|
+
/** `false` when the payer dismisses it — Cancel, Escape, the backdrop. Confirm leaves
|
|
43
|
+
* it open: close it once the switch answers. */
|
|
44
|
+
onOpenChange: (open: boolean) => void;
|
|
45
|
+
/** What the payer pays now: the overview's `paid`. */
|
|
46
|
+
from: PlanSwitchSide;
|
|
47
|
+
/** The pick. */
|
|
48
|
+
to: PlanSwitchSide;
|
|
49
|
+
/** `POST /billing/plan/preview`'s answer. Undefined while it loads: a spinner, and
|
|
50
|
+
* Confirm is held. */
|
|
51
|
+
preview?: PlanSwitchPreview;
|
|
52
|
+
/** `POST /billing/plan` is under way: Confirm's spinner, no second press, and Escape
|
|
53
|
+
* doesn't close. */
|
|
54
|
+
pending?: boolean;
|
|
55
|
+
/** The plan's display name, the app's (§3.1). Default: the code. */
|
|
56
|
+
planName?: (code: string) => string;
|
|
57
|
+
/**
|
|
58
|
+
* The app's word per dimension, for the over-limit lines. A heading's word ("Budgets")
|
|
59
|
+
* reads "Budgets: 7 of 5" (`billing.limitUsageLine`); its plural forms
|
|
60
|
+
* (`{ one: "budget", other: "budgets" }`) read "7 of 5 budgets" (`planChange.usageOf`,
|
|
61
|
+
* the form the limit takes). A dimension without one is named by its key.
|
|
62
|
+
*/
|
|
63
|
+
dimensionLabels?: Readonly<Record<string, string | PluralForms>>;
|
|
64
|
+
/** How a usage or limit figure is written. Default: a number in the locale. */
|
|
65
|
+
formatLimit?: (dimension: string, value: number) => string;
|
|
66
|
+
/** Confirm: the app's `POST /billing/plan` with `{plan, interval}` of `to`. */
|
|
67
|
+
onConfirm: () => void;
|
|
68
|
+
/** The figures' and the date's locale. Default: the provider's. */
|
|
69
|
+
locale?: string;
|
|
70
|
+
/** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
|
|
71
|
+
labels?: LabelOverride<BillingLabels>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The confirm for a plan switch (§15.2.10): from → to, each with its billing period;
|
|
76
|
+
* Paddle's preview — "Charged now" (an upgrade) or "Credited towards your next charges"
|
|
77
|
+
* (a downgrade), and the next charge with its date — in the subscription's currency;
|
|
78
|
+
* the dimensions over the new plan's limit; and Confirm.
|
|
79
|
+
*
|
|
80
|
+
* const preview = useMutation({ mutationFn: api.previewPlanSwitch });
|
|
81
|
+
* <PlanSwitchConfirm
|
|
82
|
+
* open={pick !== null}
|
|
83
|
+
* onOpenChange={(open) => !open && setPick(null)}
|
|
84
|
+
* from={overview.paid}
|
|
85
|
+
* to={pick}
|
|
86
|
+
* preview={preview.data}
|
|
87
|
+
* pending={switching.isPending}
|
|
88
|
+
* planName={planName}
|
|
89
|
+
* onConfirm={() => switching.mutate(pick)}
|
|
90
|
+
* />
|
|
91
|
+
*
|
|
92
|
+
* It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in): a read-only payer may
|
|
93
|
+
* change plan, as they may pay. A demo lock still holds it, as the server's demo guard
|
|
94
|
+
* does. The kit sends no request.
|
|
95
|
+
*/
|
|
96
|
+
export function PlanSwitchConfirm({
|
|
97
|
+
open,
|
|
98
|
+
onOpenChange,
|
|
99
|
+
from,
|
|
100
|
+
to,
|
|
101
|
+
preview,
|
|
102
|
+
pending = false,
|
|
103
|
+
planName = (code) => code,
|
|
104
|
+
dimensionLabels,
|
|
105
|
+
formatLimit,
|
|
106
|
+
onConfirm,
|
|
107
|
+
locale: localeProp,
|
|
108
|
+
labels: labelsProp,
|
|
109
|
+
}: PlanSwitchConfirmProps) {
|
|
110
|
+
const labels = useBillingLabels(labelsProp);
|
|
111
|
+
const planChange = useKitLabels("planChange", DEFAULT_PLAN_CHANGE_LABELS);
|
|
112
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
113
|
+
const locale = useKitLocale(localeProp);
|
|
114
|
+
|
|
115
|
+
const period = (interval: BillingInterval) => (interval === "year" ? labels.yearly : labels.monthly);
|
|
116
|
+
const side = (s: PlanSwitchSide) => `${planName(s.plan.toLowerCase())} (${period(s.interval)})`;
|
|
117
|
+
const money = (minor: number, currency: string) =>
|
|
118
|
+
formatMoney(minorToMajor(minor, currency), currency, { locale });
|
|
119
|
+
const figure = (dimension: string, value: number) =>
|
|
120
|
+
formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });
|
|
121
|
+
|
|
122
|
+
/** One line per dimension over the new limit, in the word's own sentence. */
|
|
123
|
+
const overLimit = (p: PlanSwitchPreview): string[] =>
|
|
124
|
+
(p.over_limit ?? []).flatMap((dimension) => {
|
|
125
|
+
const limit = p.limits?.[dimension];
|
|
126
|
+
const used = p.usage?.[dimension];
|
|
127
|
+
if (limit === null || limit === undefined || used === undefined) return [];
|
|
128
|
+
const word = dimensionLabels?.[dimension] ?? dimension;
|
|
129
|
+
return [
|
|
130
|
+
typeof word === "string"
|
|
131
|
+
? labels.limitUsageLine(word, figure(dimension, used), figure(dimension, limit))
|
|
132
|
+
: planChange.usageOf(figure(dimension, used), figure(dimension, limit), pluralForm(limit, word, locale)),
|
|
133
|
+
];
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const credit = preview !== undefined && preview.now_minor < 0;
|
|
137
|
+
const lines = preview ? overLimit(preview) : [];
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<DialogFrame
|
|
141
|
+
open={open}
|
|
142
|
+
title={labels.switchTitle}
|
|
143
|
+
description={labels.switchFromTo(side(from), side(to))}
|
|
144
|
+
onClose={() => onOpenChange(false)}
|
|
145
|
+
onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {
|
|
146
|
+
if (pending && e.key === "Escape") e.preventDefault();
|
|
147
|
+
}}
|
|
148
|
+
aria-busy={preview === undefined || pending || undefined}
|
|
149
|
+
actions={(close) => (
|
|
150
|
+
<>
|
|
151
|
+
<Button variant="secondary" onClick={close} disabled={pending}>
|
|
152
|
+
{common.cancel}
|
|
153
|
+
</Button>
|
|
154
|
+
<Button
|
|
155
|
+
commit={COMMIT_EXCEPT_BILLING}
|
|
156
|
+
pending={pending}
|
|
157
|
+
disabled={preview === undefined}
|
|
158
|
+
onClick={() => {
|
|
159
|
+
if (preview !== undefined && !pending) onConfirm();
|
|
160
|
+
}}
|
|
161
|
+
>
|
|
162
|
+
{labels.switchConfirm}
|
|
163
|
+
</Button>
|
|
164
|
+
</>
|
|
165
|
+
)}
|
|
166
|
+
>
|
|
167
|
+
{preview === undefined ? (
|
|
168
|
+
<div className="flex justify-center py-4">
|
|
169
|
+
<Spinner showLabel />
|
|
170
|
+
</div>
|
|
171
|
+
) : (
|
|
172
|
+
<>
|
|
173
|
+
<dl className="grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm" data-plan-switch-preview>
|
|
174
|
+
<dt className="text-secondary">{credit ? labels.switchCredited : labels.switchChargedNow}</dt>
|
|
175
|
+
<dd className="text-right font-medium tabular-nums" data-now={credit ? "credit" : "charge"}>
|
|
176
|
+
{money(Math.abs(preview.now_minor), preview.currency)}
|
|
177
|
+
</dd>
|
|
178
|
+
{preview.next_billed_at && preview.next_minor != null && (
|
|
179
|
+
<>
|
|
180
|
+
<dt className="text-secondary">
|
|
181
|
+
{labels.switchNextCharge(formatDate(preview.next_billed_at, "medium", { locale }))}
|
|
182
|
+
</dt>
|
|
183
|
+
<dd className="text-right tabular-nums">{money(preview.next_minor, preview.currency)}</dd>
|
|
184
|
+
</>
|
|
185
|
+
)}
|
|
186
|
+
</dl>
|
|
187
|
+
{lines.length > 0 && (
|
|
188
|
+
<div className="space-y-1 text-sm" data-plan-switch-over-limit>
|
|
189
|
+
<p>{planChange.overLimit}</p>
|
|
190
|
+
<ul className="list-disc space-y-0.5 ps-5 text-secondary">
|
|
191
|
+
{lines.map((line) => (
|
|
192
|
+
<li key={line}>{line}</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</>
|
|
198
|
+
)}
|
|
199
|
+
</DialogFrame>
|
|
200
|
+
);
|
|
201
|
+
}
|
package/src/i18n/german.ts
CHANGED
|
@@ -1749,6 +1749,14 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1749
1749
|
"Der Bezahlvorgang konnte nicht geöffnet werden. Bitte kehren Sie zurück und versuchen Sie es gleich noch einmal.",
|
|
1750
1750
|
payBack:
|
|
1751
1751
|
"Der Bezahlvorgang wurde geschlossen. Sie können zurückkehren oder diese Seite neu laden, um ihn wieder zu öffnen.",
|
|
1752
|
+
payDone: "Zahlung abgeschlossen. Sie können diese Seite schließen.",
|
|
1753
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1754
|
+
switchTitle: "Tarif ändern",
|
|
1755
|
+
switchFromTo: (from, to) => `Von ${from} zu ${to}`,
|
|
1756
|
+
switchChargedNow: "Jetzt belastet",
|
|
1757
|
+
switchCredited: "Gutschrift auf die nächsten Belastungen",
|
|
1758
|
+
switchNextCharge: (date) => `Nächste Belastung am ${date}`,
|
|
1759
|
+
switchConfirm: "Tarif ändern",
|
|
1752
1760
|
waitingChanges: (count) =>
|
|
1753
1761
|
count === 1
|
|
1754
1762
|
? `${n(count)} Änderung wartet auf einen Tarif`
|
|
@@ -1760,9 +1768,13 @@ export function germanLabels(numberLocale: string): UiKitLabels {
|
|
|
1760
1768
|
// §14.2: server-kit 0.7's refusals. "Zahlung und Rechnungen", as `manage`.
|
|
1761
1769
|
providerUnavailable:
|
|
1762
1770
|
"Der Zahlungsanbieter ist gerade nicht erreichbar. Bitte versuchen Sie es gleich noch einmal.",
|
|
1763
|
-
alreadySubscribed: "Sie haben bereits ein Abonnement.
|
|
1771
|
+
alreadySubscribed: "Sie haben bereits ein Abonnement. Ändern Sie Ihren Tarif oben.",
|
|
1764
1772
|
planNotSold:
|
|
1765
1773
|
"Dieser Tarif ist für diesen Abrechnungszeitraum oder diese Währung nicht erhältlich. Bitte wählen Sie einen anderen.",
|
|
1774
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1775
|
+
notSubscribed:
|
|
1776
|
+
"Es läuft kein Abonnement, das sich ändern ließe. Wählen Sie einen Tarif, um eines abzuschließen.",
|
|
1777
|
+
planUnchanged: "Das ist bereits Ihr Tarif. Wählen Sie einen anderen Tarif oder Abrechnungszeitraum.",
|
|
1766
1778
|
},
|
|
1767
1779
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "Tarif" as
|
|
1768
1780
|
// in `billing`; keksdose's reviewed admin wording where it had one.
|
package/src/i18n/locales/es.ts
CHANGED
|
@@ -1699,6 +1699,14 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
1699
1699
|
payNothing: "Aquí no hay nada que pagar. Para elegir un plan, vuelva a la aplicación.",
|
|
1700
1700
|
payFailed: "No se ha podido abrir el pago. Vuelva atrás e inténtelo de nuevo en un momento.",
|
|
1701
1701
|
payBack: "El pago se ha cerrado. Puede volver atrás o recargar esta página para abrirlo de nuevo.",
|
|
1702
|
+
payDone: "Pago completado. Puede cerrar esta página.",
|
|
1703
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1704
|
+
switchTitle: "Cambiar de plan",
|
|
1705
|
+
switchFromTo: (from, to) => `De ${from} a ${to}`,
|
|
1706
|
+
switchChargedNow: "Cargo ahora",
|
|
1707
|
+
switchCredited: "Abonado a sus próximos cargos",
|
|
1708
|
+
switchNextCharge: (date) => `Próximo cargo el ${date}`,
|
|
1709
|
+
switchConfirm: "Cambiar de plan",
|
|
1702
1710
|
waitingChanges: (count) => `${n(count)} ${plural(count, "cambio", "cambios")} a la espera de un plan`,
|
|
1703
1711
|
// "Descartar", as `wizard.cancelConfirmLabel`.
|
|
1704
1712
|
discardWaiting: "Descartar los cambios en espera",
|
|
@@ -1706,8 +1714,11 @@ export function uiKitLabelsEs(numberLocale = "es-ES"): UiKitLabels {
|
|
|
1706
1714
|
disabled: "Las suscripciones no están disponibles aquí.",
|
|
1707
1715
|
// §14.2: server-kit 0.7's refusals. "Pagos y facturas", as `manage`. "Usted".
|
|
1708
1716
|
providerUnavailable: "El proveedor de pagos no está disponible en este momento. Inténtelo de nuevo en un momento.",
|
|
1709
|
-
alreadySubscribed: "Ya tiene una suscripción.
|
|
1717
|
+
alreadySubscribed: "Ya tiene una suscripción. Cambie de plan más arriba.",
|
|
1710
1718
|
planNotSold: "Este plan no está disponible para ese periodo de facturación o esa moneda. Elija otro.",
|
|
1719
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1720
|
+
notSubscribed: "No hay ninguna suscripción activa que cambiar. Elija un plan para suscribirse.",
|
|
1721
|
+
planUnchanged: "Ese ya es su plan. Elija otro plan u otro periodo de facturación.",
|
|
1711
1722
|
},
|
|
1712
1723
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "plan" as
|
|
1713
1724
|
// in `billing`. "Usted".
|
package/src/i18n/locales/fr.ts
CHANGED
|
@@ -1733,6 +1733,14 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
1733
1733
|
payNothing: "Il n’y a rien à payer ici. Pour choisir une formule, revenez à l’application.",
|
|
1734
1734
|
payFailed: "Le paiement n’a pas pu s’ouvrir. Veuillez revenir en arrière et réessayer dans un instant.",
|
|
1735
1735
|
payBack: "Le paiement a été fermé. Vous pouvez revenir en arrière, ou recharger cette page pour le rouvrir.",
|
|
1736
|
+
payDone: "Paiement effectué. Vous pouvez fermer cette page.",
|
|
1737
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1738
|
+
switchTitle: "Changer de formule",
|
|
1739
|
+
switchFromTo: (from, to) => `De ${from} à ${to}`,
|
|
1740
|
+
switchChargedNow: "Débité maintenant",
|
|
1741
|
+
switchCredited: "Crédité sur vos prochains paiements",
|
|
1742
|
+
switchNextCharge: (date) => `Prochain prélèvement le ${date}`,
|
|
1743
|
+
switchConfirm: "Changer de formule",
|
|
1736
1744
|
waitingChanges: (count) =>
|
|
1737
1745
|
`${n(count)} ${plural(count, "modification en attente", "modifications en attente")} d’une formule`,
|
|
1738
1746
|
// "Abandonner", as `wizard.cancelConfirmLabel`.
|
|
@@ -1742,10 +1750,12 @@ export function uiKitLabelsFr(numberLocale = "fr-FR"): UiKitLabels {
|
|
|
1742
1750
|
// §14.2: server-kit 0.7's refusals. "Paiement et factures", as `manage`.
|
|
1743
1751
|
providerUnavailable:
|
|
1744
1752
|
"Le prestataire de paiement est injoignable pour le moment. Veuillez réessayer dans un instant.",
|
|
1745
|
-
alreadySubscribed:
|
|
1746
|
-
"Vous avez déjà un abonnement. Pour changer de formule, allez dans «\u202fPaiement et factures\u202f».",
|
|
1753
|
+
alreadySubscribed: "Vous avez déjà un abonnement. Changez de formule ci-dessus.",
|
|
1747
1754
|
planNotSold:
|
|
1748
1755
|
"Cette formule n’est pas proposée pour cette période de facturation ou cette devise. Veuillez en choisir une autre.",
|
|
1756
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1757
|
+
notSubscribed: "Aucun abonnement en cours à modifier. Choisissez une formule pour vous abonner.",
|
|
1758
|
+
planUnchanged: "C’est déjà votre formule. Choisissez une autre formule ou une autre période de facturation.",
|
|
1749
1759
|
},
|
|
1750
1760
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "formule"
|
|
1751
1761
|
// as in `billing`. "Vous".
|
package/src/i18n/locales/hu.ts
CHANGED
|
@@ -1702,6 +1702,14 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
1702
1702
|
payNothing: "Itt nincs mit kifizetni. Csomag választásához térjen vissza az alkalmazásba.",
|
|
1703
1703
|
payFailed: "A fizetést nem sikerült megnyitni. Kérjük, lépjen vissza, és próbálja újra egy kis idő múlva.",
|
|
1704
1704
|
payBack: "A fizetés bezárult. Visszaléphet, vagy a megnyitásához töltse újra ezt az oldalt.",
|
|
1705
|
+
payDone: "A fizetés sikerült. Bezárhatja ezt az oldalt.",
|
|
1706
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1707
|
+
switchTitle: "Csomag módosítása",
|
|
1708
|
+
switchFromTo: (from, to) => `${from} helyett ${to}`,
|
|
1709
|
+
switchChargedNow: "Most terhelt összeg",
|
|
1710
|
+
switchCredited: "A következő terhelésekből levonva",
|
|
1711
|
+
switchNextCharge: (date) => `Következő terhelés: ${date}`,
|
|
1712
|
+
switchConfirm: "Csomag módosítása",
|
|
1705
1713
|
// No plural after a numeral (convention 2).
|
|
1706
1714
|
waitingChanges: (count) => `${n(count)} módosítás csomagra vár`,
|
|
1707
1715
|
// "Elvetés", as `wizard.cancelConfirmLabel`.
|
|
@@ -1710,9 +1718,12 @@ export function uiKitLabelsHu(numberLocale = "hu-HU"): UiKitLabels {
|
|
|
1710
1718
|
disabled: "Az előfizetés itt nem érhető el.",
|
|
1711
1719
|
// §14.2: server-kit 0.7's refusals. "Fizetés és számlák", as `manage`.
|
|
1712
1720
|
providerUnavailable: "A fizetési szolgáltató jelenleg nem érhető el. Kérjük, próbálja újra egy kis idő múlva.",
|
|
1713
|
-
alreadySubscribed: "Már van előfizetése. A
|
|
1721
|
+
alreadySubscribed: "Már van előfizetése. A csomagot fent módosíthatja.",
|
|
1714
1722
|
planNotSold:
|
|
1715
1723
|
"Ez a csomag ehhez a számlázási időszakhoz vagy pénznemhez nem érhető el. Kérjük, válasszon másikat.",
|
|
1724
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1725
|
+
notSubscribed: "Nincs futó előfizetés, amelyet módosítani lehetne. Előfizetéshez válasszon egy csomagot.",
|
|
1726
|
+
planUnchanged: "Ez már az Ön csomagja. Válasszon másik csomagot vagy számlázási időszakot.",
|
|
1716
1727
|
},
|
|
1717
1728
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "csomag"
|
|
1718
1729
|
// as in `billing`. A plan's name takes no case ending: it follows a colon or an arrow.
|
package/src/i18n/locales/it.ts
CHANGED
|
@@ -1715,6 +1715,14 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
1715
1715
|
payNothing: "Qui non c’è nulla da pagare. Per scegliere un piano, torni all’app.",
|
|
1716
1716
|
payFailed: "Non è stato possibile aprire il pagamento. Torni indietro e riprovi tra un momento.",
|
|
1717
1717
|
payBack: "Il pagamento è stato chiuso. Può tornare indietro, oppure ricaricare questa pagina per riaprirlo.",
|
|
1718
|
+
payDone: "Pagamento completato. Può chiudere questa pagina.",
|
|
1719
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1720
|
+
switchTitle: "Cambia piano",
|
|
1721
|
+
switchFromTo: (from, to) => `Da ${from} a ${to}`,
|
|
1722
|
+
switchChargedNow: "Addebitato ora",
|
|
1723
|
+
switchCredited: "Accreditato sui prossimi addebiti",
|
|
1724
|
+
switchNextCharge: (date) => `Prossimo addebito il ${date}`,
|
|
1725
|
+
switchConfirm: "Cambia piano",
|
|
1718
1726
|
waitingChanges: (count) =>
|
|
1719
1727
|
`${n(count)} ${plural(count, "modifica in attesa", "modifiche in attesa")} di un piano`,
|
|
1720
1728
|
// "Scarta", as `wizard.cancelConfirmLabel`.
|
|
@@ -1723,9 +1731,12 @@ export function uiKitLabelsIt(numberLocale = "it-IT"): UiKitLabels {
|
|
|
1723
1731
|
disabled: "Gli abbonamenti non sono disponibili qui.",
|
|
1724
1732
|
// §14.2: server-kit 0.7's refusals. "Pagamento e fatture", as `manage`. "Lei".
|
|
1725
1733
|
providerUnavailable: "Il fornitore dei pagamenti non è raggiungibile al momento. Riprovi tra un momento.",
|
|
1726
|
-
alreadySubscribed: "Ha già un abbonamento.
|
|
1734
|
+
alreadySubscribed: "Ha già un abbonamento. Cambi piano qui sopra.",
|
|
1727
1735
|
planNotSold:
|
|
1728
1736
|
"Questo piano non è disponibile per questo periodo di fatturazione o questa valuta. Ne scelga un altro.",
|
|
1737
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1738
|
+
notSubscribed: "Non c’è nessun abbonamento attivo da modificare. Scelga un piano per abbonarsi.",
|
|
1739
|
+
planUnchanged: "Questo è già il Suo piano. Scelga un altro piano o un altro periodo di fatturazione.",
|
|
1729
1740
|
},
|
|
1730
1741
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "piano" as
|
|
1731
1742
|
// in `billing`. "Lei" in sentences, the imperative on the title and the button.
|
package/src/i18n/locales/zh.ts
CHANGED
|
@@ -1629,6 +1629,14 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
1629
1629
|
payNothing: "这里没有需要支付的款项。如需选择套餐,请返回应用。",
|
|
1630
1630
|
payFailed: "无法打开支付页面。请返回后稍候再试。",
|
|
1631
1631
|
payBack: "支付页面已关闭。您可以返回,或重新加载此页面以再次打开。",
|
|
1632
|
+
payDone: "付款已完成。您可以关闭此页面。",
|
|
1633
|
+
// 0.39 (§15.2.10): PlanSwitchConfirm.
|
|
1634
|
+
switchTitle: "更改套餐",
|
|
1635
|
+
switchFromTo: (from, to) => `从 ${from} 更改为 ${to}`,
|
|
1636
|
+
switchChargedNow: "立即扣款",
|
|
1637
|
+
switchCredited: "抵扣后续扣款",
|
|
1638
|
+
switchNextCharge: (date) => `下次扣款日期:${date}`,
|
|
1639
|
+
switchConfirm: "更改套餐",
|
|
1632
1640
|
waitingChanges: (count) => `${n(count)} 项修改正在等待套餐`,
|
|
1633
1641
|
// "放弃", as `wizard.cancelConfirmLabel`.
|
|
1634
1642
|
discardWaiting: "放弃等待中的修改",
|
|
@@ -1636,8 +1644,11 @@ export function uiKitLabelsZh(numberLocale = "zh-CN"): UiKitLabels {
|
|
|
1636
1644
|
disabled: "此处不提供订阅。",
|
|
1637
1645
|
// §14.2: server-kit 0.7's refusals. "付款与发票", as `manage`.
|
|
1638
1646
|
providerUnavailable: "暂时无法连接支付服务商,请稍后再试。",
|
|
1639
|
-
alreadySubscribed: "
|
|
1647
|
+
alreadySubscribed: "您已有订阅。请在上方更改套餐。",
|
|
1640
1648
|
planNotSold: "该套餐不支持此计费周期或货币,请选择其他套餐。",
|
|
1649
|
+
// 0.39 (§15.2.10): the plan switch's refusals.
|
|
1650
|
+
notSubscribed: "没有可更改的有效订阅。请选择一个套餐进行订阅。",
|
|
1651
|
+
planUnchanged: "这已经是您当前的套餐。请选择其他套餐或计费周期。",
|
|
1641
1652
|
},
|
|
1642
1653
|
// 0.33.0 — docs/billing-harmonization.md §14.12: the operator's plan parts, "套餐" as
|
|
1643
1654
|
// in `billing`. "测试版", as `landing.beta`.
|
package/src/i18n/review.ts
CHANGED
|
@@ -390,6 +390,9 @@ export const KIT_LABEL_SAMPLES: Readonly<Record<string, readonly (readonly unkno
|
|
|
390
390
|
"billing.waitingChanges": one,
|
|
391
391
|
// 0.33.0: the limit's figure line (§14.11) and the operator's plan parts (§14.12).
|
|
392
392
|
"billing.limitUsageLine": [["{{dimension}}", "{{used}}", "{{limit}}"]],
|
|
393
|
+
// 0.39: PlanSwitchConfirm's two lines with figures (§15.2.10).
|
|
394
|
+
"billing.switchFromTo": [["{{from}}", "{{to}}"]],
|
|
395
|
+
"billing.switchNextCharge": [["{{date}}"]],
|
|
393
396
|
"planChange.current": [["{{plan}}"]],
|
|
394
397
|
"planChange.changed": [["{{from}}", "{{to}}"]],
|
|
395
398
|
"planChange.set": [["{{to}}"]],
|