@jskit-ai/payments-web 0.1.61 → 0.2.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 - 2026-10-29
4
+
5
+ - Translation ready: every user-visible text is a message key with its English in `locales/en.json`, so an app translates it by supplying translation files only. With no translations, texts and behaviour are unchanged. See the guide "Translating a JSKIT app".
6
+
7
+ ### New message keys (43)
8
+
9
+ All keys in `locales/en.json` (exported as `./locales/en.json`) are new in this version.
10
+
11
+ <details><summary>Key list</summary>
12
+
13
+ - `jskit.payments.account.region`
14
+ - `jskit.payments.account.retryDetails`
15
+ - `jskit.payments.account.availableCredits`
16
+ - `jskit.payments.account.includedFeatures`
17
+ - `jskit.payments.account.noPaidFeatures`
18
+ - `jskit.payments.account.subscriptions`
19
+ - `jskit.payments.account.periodEnd`
20
+ - `jskit.payments.account.noSubscription`
21
+ - `jskit.payments.account.manageBilling`
22
+ - `jskit.payments.account.refreshDetails`
23
+ - `jskit.payments.account.adminManages`
24
+ - `jskit.payments.account.useManageBilling`
25
+ - `jskit.payments.account.confirmationNote`
26
+ - `jskit.payments.account.availablePlans`
27
+ - `jskit.payments.account.renewalCredits`
28
+ - `jskit.payments.account.choosePlan`
29
+ - `jskit.payments.account.choosePlanNamed`
30
+ - `jskit.payments.account.notAvailableForCheckout`
31
+ - `jskit.payments.account.noPlans`
32
+ - `jskit.payments.account.unavailable`
33
+ - `jskit.payments.account.status.active`
34
+ - `jskit.payments.account.status.trialing`
35
+ - `jskit.payments.account.status.pastDue`
36
+ - `jskit.payments.account.status.paused`
37
+ - `jskit.payments.account.status.canceled`
38
+ - `jskit.payments.account.status.unpaid`
39
+ - `jskit.payments.account.status.incomplete`
40
+ - `jskit.payments.account.status.incompleteExpired`
41
+ - `jskit.payments.account.history.region`
42
+ - `jskit.payments.account.history.viewTransactions`
43
+ - `jskit.payments.account.history.viewSubscriptions`
44
+ - `jskit.payments.account.history.retry`
45
+ - `jskit.payments.account.history.unpaidNote`
46
+ - `jskit.payments.account.history.records`
47
+ - `jskit.payments.account.history.invoice`
48
+ - `jskit.payments.account.history.transaction`
49
+ - `jskit.payments.account.history.subscription`
50
+ - `jskit.payments.account.history.statusAndDate`
51
+ - `jskit.payments.account.history.total`
52
+ - `jskit.payments.account.history.totalPending`
53
+ - `jskit.payments.account.history.totalAndPaid`
54
+ - `jskit.payments.account.history.empty`
55
+ - `jskit.payments.account.history.nextPage`
56
+
57
+ </details>
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  `@jskit-ai/payments-web` supplies a Vue/Vuetify account component for generated or
4
4
  hand-written JavaScript applications. It has no Vibe64, Online, Genesis, provider
5
- SDK, credential, file-discovery or HTTP dependency. The application mounts it in
5
+ SDK, credential, file-discovery or HTTP dependency; its only JSKIT peer is
6
+ `@jskit-ai/kernel`, for the client text helper. The application mounts it in
6
7
  its existing billing page or semantic placement. It does not register routes or
7
8
  change shell navigation automatically. This worktree package is not yet published.
8
9
 
@@ -23,7 +24,12 @@ Import `PaymentAccount` from
23
24
  | `history` | One authorized `{collection, items, nextCursor}` provider page, or null |
24
25
  | `historyLoading` | History request pending; displays skeletons and disables page/collection requests |
25
26
  | `historyError` | Safe history read failure; hides stale rows and offers Retry |
26
- | `locale` | Optional locale for integer credit and UTC date formatting |
27
+ | `locale` | Optional locale for integer credit and UTC date formatting; defaults to the app's active language |
28
+
29
+ Every text the component renders is a `jskit.payments.account.*` message in
30
+ `locales/en.json`. With the app's vue-i18n registered through JSKIT, it shows the
31
+ user's language; without it, the English is shown as before. Plan names, price
32
+ labels, feature names and the `loadError`/`historyError` texts are the app's own.
27
33
 
28
34
  Subscription records contain `id`, `planId`, `status`, and `periodEnd` in UTC
29
35
  milliseconds. `payments-core`'s authorized `checkout.account({actor, subjectId})`
@@ -126,8 +132,9 @@ insert a second error banner or duplicate that request state. Re-read on the
126
132
  return route; a success query parameter must never grant paid access.
127
133
 
128
134
  Laravel owns its native server and UI implementation. An Inertia/Vue application
129
- can choose this presentational component, but no JSKIT runtime is required for
130
- Laravel to implement the documented JSON account/plan display shape and actions.
135
+ can choose this presentational component (installing its `@jskit-ai/kernel` peer
136
+ for the text helper), but no JSKIT runtime is required for Laravel to implement
137
+ the documented JSON account/plan display shape and actions.
131
138
 
132
139
  ## Evidence
133
140
 
@@ -0,0 +1,55 @@
1
+ {
2
+ "jskit": {
3
+ "payments": {
4
+ "account": {
5
+ "region": "Subscription and billing",
6
+ "retryDetails": "Retry billing details",
7
+ "availableCredits": "Available credits",
8
+ "includedFeatures": "Included features",
9
+ "noPaidFeatures": "No paid features active",
10
+ "subscriptions": "Subscriptions",
11
+ "periodEnd": "Period end: {date} (UTC)",
12
+ "noSubscription": "No subscription yet.",
13
+ "manageBilling": "Manage billing",
14
+ "refreshDetails": "Refresh billing details",
15
+ "adminManages": "A billing administrator can manage this account.",
16
+ "useManageBilling": "Use Manage billing to update your subscription or payment details.",
17
+ "confirmationNote": "Access and credits update after payment confirmation. Returning from checkout alone does not activate a plan.",
18
+ "availablePlans": "Available plans",
19
+ "renewalCredits": "{credits} credits per paid renewal; unused renewal credits expire at period end.",
20
+ "choosePlan": "Choose plan",
21
+ "choosePlanNamed": "Choose {plan}",
22
+ "notAvailableForCheckout": "Not available for checkout yet.",
23
+ "noPlans": "No plans are available yet.",
24
+ "unavailable": "Billing details are not available yet.",
25
+ "status": {
26
+ "active": "Active",
27
+ "trialing": "Trial",
28
+ "pastDue": "Payment overdue",
29
+ "paused": "Paused",
30
+ "canceled": "Canceled",
31
+ "unpaid": "Unpaid",
32
+ "incomplete": "Payment incomplete",
33
+ "incompleteExpired": "Payment expired"
34
+ },
35
+ "history": {
36
+ "region": "Billing history",
37
+ "viewTransactions": "View invoices and transactions",
38
+ "viewSubscriptions": "View subscription history",
39
+ "retry": "Retry billing history",
40
+ "unpaidNote": "Invoices and transactions include unpaid records. Their totals do not confirm payment.",
41
+ "records": "Provider billing records",
42
+ "invoice": "Invoice {id}",
43
+ "transaction": "Transaction {id}",
44
+ "subscription": "Subscription {id}",
45
+ "statusAndDate": "{status} · {date} (UTC)",
46
+ "total": "Total: {total}",
47
+ "totalPending": "Not available yet",
48
+ "totalAndPaid": "Total: {total} · Paid: {paid}",
49
+ "empty": "No billing records found.",
50
+ "nextPage": "Next billing page"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@jskit-ai/payments-web",
3
- "version": "0.1.61",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Application-owned billing views for JSKIT Vue applications.",
6
6
  "exports": {
7
- "./client/components/PaymentAccount": "./src/client/components/PaymentAccount.vue"
7
+ "./client/components/PaymentAccount": "./src/client/components/PaymentAccount.vue",
8
+ "./locales/en.json": "./locales/en.json"
8
9
  },
9
10
  "peerDependencies": {
11
+ "@jskit-ai/kernel": "0.2.1",
10
12
  "vue": "^3.5.13",
11
13
  "vuetify": "^4.0.0"
12
14
  },
@@ -1,5 +1,6 @@
1
1
  <script setup>
2
2
  import { computed } from "vue";
3
+ import { useJskitText } from "../i18n.js";
3
4
 
4
5
  const props = defineProps({
5
6
  plans: { type: Array, default: () => [] },
@@ -19,13 +20,35 @@ const subscriptions = computed(() => props.account?.subscriptions ?? []);
19
20
  const hasSubscription = computed(() => subscriptions.value.some((item) =>
20
21
  !["canceled", "incomplete_expired"].includes(item.status)));
21
22
  const disabled = computed(() => props.pending || props.loading || Boolean(props.loadError) || !props.account || !props.canManage);
22
- const statuses = {
23
- active: "Active", trialing: "Trial", past_due: "Payment overdue", paused: "Paused",
24
- canceled: "Canceled", unpaid: "Unpaid", incomplete: "Payment incomplete",
25
- incomplete_expired: "Payment expired"
23
+ const jskitText = useJskitText();
24
+ const { t } = jskitText;
25
+ const statusKeys = {
26
+ active: "jskit.payments.account.status.active",
27
+ trialing: "jskit.payments.account.status.trialing",
28
+ past_due: "jskit.payments.account.status.pastDue",
29
+ paused: "jskit.payments.account.status.paused",
30
+ canceled: "jskit.payments.account.status.canceled",
31
+ unpaid: "jskit.payments.account.status.unpaid",
32
+ incomplete: "jskit.payments.account.status.incomplete",
33
+ incomplete_expired: "jskit.payments.account.status.incompleteExpired"
26
34
  };
27
- const number = (value) => new Intl.NumberFormat(props.locale).format(value);
28
- const date = (value) => new Intl.DateTimeFormat(props.locale, { dateStyle: "medium", timeZone: "UTC" }).format(value);
35
+ const historyKindKeys = {
36
+ invoice: "jskit.payments.account.history.invoice",
37
+ transaction: "jskit.payments.account.history.transaction"
38
+ };
39
+ const historyTitle = (item) =>
40
+ t(Object.hasOwn(historyKindKeys, item.kind) ? historyKindKeys[item.kind] : "jskit.payments.account.history.subscription", { id: item.id });
41
+ const historyTotal = (item) => {
42
+ const total = item.totalLabel ?? t("jskit.payments.account.history.totalPending");
43
+ return item.paidLabel
44
+ ? t("jskit.payments.account.history.totalAndPaid", { total, paid: item.paidLabel })
45
+ : t("jskit.payments.account.history.total", { total });
46
+ };
47
+ const statusLabel = (status) => Object.hasOwn(statusKeys, status) ? t(statusKeys[status]) : status;
48
+ // An explicit locale prop wins; otherwise numbers and dates follow the app's language.
49
+ const locale = () => props.locale ?? jskitText.locale;
50
+ const number = (value) => new Intl.NumberFormat(locale()).format(value);
51
+ const date = (value) => new Intl.DateTimeFormat(locale(), { dateStyle: "medium", timeZone: "UTC" }).format(value);
29
52
  const planName = (id) => props.plans.find((plan) => plan.id === id)?.name ?? id;
30
53
  function checkout(plan) {
31
54
  if (!disabled.value && !hasSubscription.value && plan.available === true) emit("checkout", plan.id);
@@ -36,68 +59,68 @@ function loadHistory(collection, after = null) {
36
59
  </script>
37
60
 
38
61
  <template>
39
- <section class="payment-account" aria-label="Subscription and billing" :aria-busy="loading || pending">
62
+ <section class="payment-account" :aria-label="t('jskit.payments.account.region')" :aria-busy="loading || pending">
40
63
  <v-skeleton-loader v-if="loading && !account" type="article, list-item-two-line, actions" />
41
64
  <div v-else-if="loadError" role="alert">
42
65
  <p>{{ loadError }}</p>
43
- <v-btn variant="tonal" :disabled="loading" @click="emit('refresh')">Retry billing details</v-btn>
66
+ <v-btn variant="tonal" :disabled="loading" @click="emit('refresh')">{{ t('jskit.payments.account.retryDetails') }}</v-btn>
44
67
  </div>
45
68
  <template v-else-if="account">
46
69
  <dl class="payment-account__summary">
47
- <div><dt>Available credits</dt><dd>{{ number(account.balance) }}</dd></div>
48
- <div><dt>Included features</dt><dd>{{ account.features.length ? account.features.join(', ') : 'No paid features active' }}</dd></div>
70
+ <div><dt>{{ t('jskit.payments.account.availableCredits') }}</dt><dd>{{ number(account.balance) }}</dd></div>
71
+ <div><dt>{{ t('jskit.payments.account.includedFeatures') }}</dt><dd>{{ account.features.length ? account.features.join(', ') : t('jskit.payments.account.noPaidFeatures') }}</dd></div>
49
72
  </dl>
50
- <ul v-if="subscriptions.length" class="payment-account__subscriptions" aria-label="Subscriptions">
73
+ <ul v-if="subscriptions.length" class="payment-account__subscriptions" :aria-label="t('jskit.payments.account.subscriptions')">
51
74
  <li v-for="subscription in subscriptions" :key="subscription.id">
52
75
  <strong>{{ planName(subscription.planId) }}</strong>
53
- <span>{{ statuses[subscription.status] ?? subscription.status }}</span>
54
- <span v-if="subscription.periodEnd > 0">Period end: {{ date(subscription.periodEnd) }} (UTC)</span>
76
+ <span>{{ statusLabel(subscription.status) }}</span>
77
+ <span v-if="subscription.periodEnd > 0">{{ t('jskit.payments.account.periodEnd', { date: date(subscription.periodEnd) }) }}</span>
55
78
  </li>
56
79
  </ul>
57
- <p v-else>No subscription yet.</p>
80
+ <p v-else>{{ t('jskit.payments.account.noSubscription') }}</p>
58
81
  <div class="payment-account__actions">
59
- <v-btn v-if="account.hasCustomer" variant="tonal" :disabled="disabled" @click="emit('portal')">Manage billing</v-btn>
60
- <v-btn variant="text" :disabled="loading || pending" @click="emit('refresh')">Refresh billing details</v-btn>
82
+ <v-btn v-if="account.hasCustomer" variant="tonal" :disabled="disabled" @click="emit('portal')">{{ t('jskit.payments.account.manageBilling') }}</v-btn>
83
+ <v-btn variant="text" :disabled="loading || pending" @click="emit('refresh')">{{ t('jskit.payments.account.refreshDetails') }}</v-btn>
61
84
  </div>
62
- <p v-if="!canManage">A billing administrator can manage this account.</p>
63
- <p v-else-if="hasSubscription">Use Manage billing to update your subscription or payment details.</p>
64
- <p>Access and credits update after payment confirmation. Returning from checkout alone does not activate a plan.</p>
65
- <ul v-if="plans.length" class="payment-account__plans" aria-label="Available plans">
85
+ <p v-if="!canManage">{{ t('jskit.payments.account.adminManages') }}</p>
86
+ <p v-else-if="hasSubscription">{{ t('jskit.payments.account.useManageBilling') }}</p>
87
+ <p>{{ t('jskit.payments.account.confirmationNote') }}</p>
88
+ <ul v-if="plans.length" class="payment-account__plans" :aria-label="t('jskit.payments.account.availablePlans')">
66
89
  <li v-for="plan in plans" :key="plan.id" class="payment-account__plan">
67
90
  <strong>{{ plan.name }}</strong>
68
91
  <p>{{ plan.priceLabel }}</p>
69
92
  <ul v-if="plan.features.length"><li v-for="feature in plan.features" :key="feature">{{ feature }}</li></ul>
70
- <p>{{ number(plan.renewalCredits) }} credits per paid renewal; unused renewal credits expire at period end.</p>
71
- <v-btn variant="flat" color="primary" :disabled="disabled || hasSubscription || plan.available !== true" :aria-label="`Choose ${plan.name}`" @click="checkout(plan)">Choose plan</v-btn>
72
- <p v-if="plan.available !== true">Not available for checkout yet.</p>
93
+ <p>{{ t('jskit.payments.account.renewalCredits', { credits: number(plan.renewalCredits), count: plan.renewalCredits }) }}</p>
94
+ <v-btn variant="flat" color="primary" :disabled="disabled || hasSubscription || plan.available !== true" :aria-label="t('jskit.payments.account.choosePlanNamed', { plan: plan.name })" @click="checkout(plan)">{{ t('jskit.payments.account.choosePlan') }}</v-btn>
95
+ <p v-if="plan.available !== true">{{ t('jskit.payments.account.notAvailableForCheckout') }}</p>
73
96
  </li>
74
97
  </ul>
75
- <p v-else>No plans are available yet.</p>
76
- <section v-if="canReadHistory" aria-label="Billing history" :aria-busy="historyLoading">
98
+ <p v-else>{{ t('jskit.payments.account.noPlans') }}</p>
99
+ <section v-if="canReadHistory" :aria-label="t('jskit.payments.account.history.region')" :aria-busy="historyLoading">
77
100
  <div class="payment-account__actions">
78
- <v-btn variant="tonal" :disabled="historyLoading || loading" @click="loadHistory('transactions')">View invoices and transactions</v-btn>
79
- <v-btn variant="tonal" :disabled="historyLoading || loading" @click="loadHistory('subscriptions')">View subscription history</v-btn>
101
+ <v-btn variant="tonal" :disabled="historyLoading || loading" @click="loadHistory('transactions')">{{ t('jskit.payments.account.history.viewTransactions') }}</v-btn>
102
+ <v-btn variant="tonal" :disabled="historyLoading || loading" @click="loadHistory('subscriptions')">{{ t('jskit.payments.account.history.viewSubscriptions') }}</v-btn>
80
103
  </div>
81
104
  <v-skeleton-loader v-if="historyLoading" type="list-item-three-line, list-item-three-line" />
82
105
  <div v-else-if="historyError" role="alert">
83
106
  <p>{{ historyError }}</p>
84
- <v-btn variant="text" @click="emit('retry-history')">Retry billing history</v-btn>
107
+ <v-btn variant="text" @click="emit('retry-history')">{{ t('jskit.payments.account.history.retry') }}</v-btn>
85
108
  </div>
86
109
  <template v-else-if="history">
87
- <p v-if="history.collection === 'transactions'">Invoices and transactions include unpaid records. Their totals do not confirm payment.</p>
88
- <v-list v-if="history.items.length" lines="three" aria-label="Provider billing records">
110
+ <p v-if="history.collection === 'transactions'">{{ t('jskit.payments.account.history.unpaidNote') }}</p>
111
+ <v-list v-if="history.items.length" lines="three" :aria-label="t('jskit.payments.account.history.records')">
89
112
  <v-list-item v-for="item in history.items" :key="item.id">
90
- <v-list-item-title>{{ item.kind === 'invoice' ? 'Invoice' : item.kind === 'transaction' ? 'Transaction' : 'Subscription' }} {{ item.id }}</v-list-item-title>
91
- <v-list-item-subtitle>{{ statuses[item.status] ?? item.status }} · {{ date(Date.parse(item.createdAt)) }} (UTC)</v-list-item-subtitle>
92
- <p v-if="item.kind !== 'subscription'">Total: {{ item.totalLabel ?? 'Not available yet' }}<span v-if="item.paidLabel"> · Paid: {{ item.paidLabel }}</span></p>
113
+ <v-list-item-title>{{ historyTitle(item) }}</v-list-item-title>
114
+ <v-list-item-subtitle>{{ t('jskit.payments.account.history.statusAndDate', { status: statusLabel(item.status), date: date(Date.parse(item.createdAt)) }) }}</v-list-item-subtitle>
115
+ <p v-if="item.kind !== 'subscription'">{{ historyTotal(item) }}</p>
93
116
  </v-list-item>
94
117
  </v-list>
95
- <p v-else>No billing records found.</p>
96
- <v-btn v-if="history.nextCursor" variant="text" @click="loadHistory(history.collection, history.nextCursor)">Next billing page</v-btn>
118
+ <p v-else>{{ t('jskit.payments.account.history.empty') }}</p>
119
+ <v-btn v-if="history.nextCursor" variant="text" @click="loadHistory(history.collection, history.nextCursor)">{{ t('jskit.payments.account.history.nextPage') }}</v-btn>
97
120
  </template>
98
121
  </section>
99
122
  </template>
100
- <p v-else>Billing details are not available yet.</p>
123
+ <p v-else>{{ t('jskit.payments.account.unavailable') }}</p>
101
124
  </section>
102
125
  </template>
103
126
 
@@ -0,0 +1,6 @@
1
+ import { registerJskitMessages, useJskitText } from "@jskit-ai/kernel/client/i18n";
2
+ import en from "../../locales/en.json" with { type: "json" };
3
+
4
+ registerJskitMessages(en);
5
+
6
+ export { useJskitText };
@@ -0,0 +1,16 @@
1
+ import assert from "node:assert/strict";
2
+ import path from "node:path";
3
+ import test from "node:test";
4
+ import { fileURLToPath } from "node:url";
5
+ import { evaluateI18nCatalogContract } from "../../../tooling/test-support/i18nCatalogContract.mjs";
6
+
7
+ const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
8
+ const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
9
+
10
+ test("payments-web English catalog matches its source", () => {
11
+ const { problems } = evaluateI18nCatalogContract({
12
+ repoRoot: REPO_ROOT,
13
+ packageDir: path.join(REPO_ROOT, "packages", "payments-web")
14
+ });
15
+ assert.deepEqual(problems, []);
16
+ });
@@ -14,7 +14,9 @@ test('billing component preserves read state and guards checkout while leaving a
14
14
  let app;
15
15
  try {
16
16
  const modulePath = `${directory}/component.mjs`;
17
- await writeFile(modulePath, compiled.content);
17
+ // The compiled module lives in a temporary directory, so point its catalog import at the package.
18
+ const i18nModule = new URL('../src/client/i18n.js', import.meta.url).href;
19
+ await writeFile(modulePath, compiled.content.replace('"../i18n.js"', JSON.stringify(i18nModule)));
18
20
  const { default: PaymentAccount } = await import(pathToFileURL(modulePath));
19
21
  const node = (type, text = '') => ({ type, text, props: {}, children: [], parent: null });
20
22
  const renderer = createRenderer({