@colixsystems/widget-sdk 0.286.0 → 0.288.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 +11 -2
- package/dist/contract.cjs +30 -5
- package/dist/contract.js +30 -5
- package/dist/hooks.js +105 -58
- package/dist/index.d.ts +47 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +30 -5
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
10
10
|
| `ctx.directory` | `@colixsystems/directory-client` | `me()`, `users.{list,get,invite,deactivate,reactivate}`, `groups.{list,create,remove,addMember,removeMember,listMine}`, `invites.{list,revoke,resend}` |
|
|
11
11
|
| `ctx.assets` | `@colixsystems/assets-client` | the Asset Manager: `get(id)`, `list(query)`, `upload(formData)` over `/files` — what `useAsset()` (single asset by id) and `useAssetsByTag()` (every asset carrying a tag) resolve |
|
|
12
12
|
| `ctx.widget.assets` | — (built by the host) | the files the widget package bundles: a map of file name → absolute URL, `{}` when it bundles nothing — what `useBundledAsset()` resolves |
|
|
13
|
-
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)
|
|
13
|
+
| `ctx.payments` | `@colixsystems/payments-client` | `requestPayment(body)`, `getPayment(id)`, the access plans and the app subscription |
|
|
14
14
|
|
|
15
15
|
**Wire / casing: snake_case end to end.** The clients send and return snake_case **verbatim** (`created_at`, `group_ids`, `can_read`, `amount_cents`, `data_type`, `is_active`, …). There is **no case transform anywhere** — not on the client and not in the backend; the only casing boundary is Prisma `@map` (snake_case field → camelCase column). Author-defined record column values pass through verbatim. Every `list(...)` returns the `{ data, meta }` envelope; the read hooks unwrap `res.data` for you.
|
|
16
16
|
|
|
@@ -75,6 +75,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
75
75
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
76
76
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId })` (optional: spaceType, folderId) | `{ openPdf, opening, canOpenPdf, exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.renderPdf` + `ctx.device.pdfViewer`, `ctx.filestore.files.exportPdf` — `files.write:*`. `openPdf(html, { fileName? })` renders the HTML to a PDF server-side and OPENS it (web: PDF tab; native: print / share sheet) without saving. `exportToPdf(html, { fileName?, folderId? })` renders it and saves it as a file (`application/pdf`). Same server-side renderer on web + native. |
|
|
77
77
|
| **PAYMENTS** (`ctx.payments`) | `useAccessPlans()` | `{ plans, loading, error, purchaseAvailable, purchase, cancel, refresh }` | `ctx.payments.{listAccessPlans,purchaseAccessPlan,cancelAccessPlan}` — `payments.charge:appUser`. Plans selling access to restricted pages; `purchase(planId)` names only the plan, the server sets price and group. `purchaseAvailable` is `false` in an iOS / Android build (digital goods need store billing): show a visible not-available state. |
|
|
78
|
+
| **PAYMENTS** (`ctx.payments`) | `useAppSubscription()` | `{ subscription, loading, error, cancel, refresh }` | `ctx.payments.{getAppSubscription,cancelAppSubscription}` — `payments.charge:appUser`. The signed-in user's subscription to a paid app (`{ active, renews, access_ends_at }`); `cancel()` stops it renewing, and access runs to `access_ends_at`. |
|
|
78
79
|
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. `requestPayment` requires `kind` (`"physical"` \| `"service"` \| `"digital"`): what is sold. A `"digital"` charge is refused in an iOS or Android build (`DIGITAL_SALE_NEEDS_STORE_BILLING`). Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. Charges are accepted ONLY in the currency the workspace sells in — omit `currency` and the platform applies it (a disagreeing literal is a publish-blocking `payment-currency` finding). |
|
|
79
80
|
| **NOTIFICATIONS** (`ctx.notifications`) | `useSendNotification()` | `{ send, sending, error }` | `ctx.notifications.send` — `notifications.send:appUser`. `send({ recipient_user_id, title, body, link?, payload? })` notifies one app user in the same workspace; call from an event handler (never render); rejects with `NotificationError`. |
|
|
80
81
|
| **NOTIFICATIONS** (`ctx.notifications`) | `useRecordMutes(tableId)` | `{ isMuted, loading, error, refetch }` | `ctx.notifications.mutes` — no scope. Which records of one table the signed-in user has muted, in one request for a whole list; `isMuted(recordId)` turns false when a timed mute lapses, and a `useRecordMute` write shows at once. |
|
|
@@ -89,7 +90,15 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
89
90
|
|
|
90
91
|
## Status
|
|
91
92
|
|
|
92
|
-
`v0.
|
|
93
|
+
`v0.288.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
|
|
94
|
+
|
|
95
|
+
### What's new in 0.288.0 (contract 1.236.0)
|
|
96
|
+
|
|
97
|
+
**A payment can pay for an order, and only the payment marks it paid.** Until now a shop widget charged the buyer and then wrote "Paid" to the order itself, so a buyer who could edit their own order could mark it paid without paying. `usePayments().requestPayment` now takes an optional `order: { table_id, record_id }`: a row the buyer may edit, in a table whose payment-status column the workspace has set (a STRING or BOOL column, one per table). The server checks it before the charge starts and refuses a missing or unreachable order (`ORDER_NOT_FOUND`), a table with no payment-status column (`ORDER_NOT_PAYABLE`) and an order that is already paid (`ORDER_ALREADY_PAID`); all three are non-retryable. Only one checkout per order can be open at a time: a second one while the first is still open is refused with `ORDER_PAYMENT_IN_PROGRESS`, which is retryable and clears when that checkout ends. When the payment settles, the server writes the column (`"PAID"`, or `true` on a BOOL; `"REFUNDED"` / `false` after a full refund); a failed or cancelled payment leaves it alone. Nothing else can write that column: every client value for it is ignored on create and update, so drop any code that sets "paid" yourself. The payment returned by `requestPayment` and `getPayment` carries `order` too, so a buyer coming back from Checkout lands on the right order. A `datastoreTemplate` column may declare `paymentStatus: true` to ship the column with the widget. Additive: a charge without `order` behaves as before.
|
|
98
|
+
|
|
99
|
+
### What's new in 0.287.0 (contract 1.235.0)
|
|
100
|
+
|
|
101
|
+
**`useAppSubscription()` — a paid app's subscriber cancels their own subscription.** When a workspace charges its app users a monthly price for the whole app, `useAppSubscription()` returns `{ subscription, loading, error, cancel, refresh }`, where `subscription` is `{ active, renews, access_ends_at }`: `active` says the signed-in user holds paid access now, `renews` that a subscription will charge again, and `access_ends_at` when access ends without a renewal (null while it has no end). Offer a "Cancel subscription" control whenever `renews` is true: `cancel()` stops the renewal, access continues until `access_ends_at`, and nothing is refunded. Access bought in the App Store or Google Play is cancelled in the store and reads `renews: false`. Needs the `payments.charge:appUser` scope. The injected payments client gains `getAppSubscription()` and `cancelAppSubscription()` (`@colixsystems/payments-client` 0.5.0).
|
|
93
102
|
|
|
94
103
|
### What's new in 0.286.0 (contract 1.234.0)
|
|
95
104
|
|
package/dist/contract.cjs
CHANGED
|
@@ -2376,9 +2376,9 @@ const HOOKS = [
|
|
|
2376
2376
|
signature: "usePayments()",
|
|
2377
2377
|
returnShape: {
|
|
2378
2378
|
requestPayment:
|
|
2379
|
-
"({ amount_cents, currency?, description, kind, return_path? }) => Promise<{ id, status }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2379
|
+
"({ amount_cents, currency?, description, kind, return_path?, order? }) => Promise<{ id, status, order }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); order: { table_id, record_id } — the order row this pays for, in a table with a payment-status column; the SERVER marks it paid when the payment settles, so never write \"paid\" yourself (refused: ORDER_NOT_FOUND, ORDER_NOT_PAYABLE, ORDER_ALREADY_PAID; ORDER_PAYMENT_IN_PROGRESS is retryable while another checkout for the order is open); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2380
2380
|
getPayment:
|
|
2381
|
-
"(paymentId) => Promise<{ id, status, amount_cents, currency, description }>",
|
|
2381
|
+
"(paymentId) => Promise<{ id, status, amount_cents, currency, description, order }> // order: { table_id, record_id } | null — read it after returning from Checkout",
|
|
2382
2382
|
},
|
|
2383
2383
|
requiredContextSlice: ["payments.requestPayment"],
|
|
2384
2384
|
scopes: ["payments.charge:appUser"],
|
|
@@ -2407,6 +2407,22 @@ const HOOKS = [
|
|
|
2407
2407
|
],
|
|
2408
2408
|
scopes: ["payments.charge:appUser"],
|
|
2409
2409
|
},
|
|
2410
|
+
// sc-15342 — a paid app's subscriber cancels their own subscription.
|
|
2411
|
+
{
|
|
2412
|
+
name: "useAppSubscription",
|
|
2413
|
+
signature: "useAppSubscription()",
|
|
2414
|
+
returnShape: {
|
|
2415
|
+
subscription:
|
|
2416
|
+
"{ active, renews, access_ends_at } | null // the signed-in user's subscription to the app itself (a paid app): active = they hold paid access now; renews = a subscription will charge again (false once cancelled, or when bought in the App Store / Google Play); access_ends_at = when access ends without a renewal (null while it has no end, or when not held)",
|
|
2417
|
+
loading: "boolean",
|
|
2418
|
+
error: "PaymentError | null",
|
|
2419
|
+
cancel:
|
|
2420
|
+
"() => Promise<{ active, renews: false, access_ends_at }> // stop it renewing; access runs to access_ends_at, nothing is refunded — offer it whenever renews is true",
|
|
2421
|
+
refresh: "() => Promise<void>",
|
|
2422
|
+
},
|
|
2423
|
+
requiredContextSlice: ["payments.getAppSubscription", "payments.cancelAppSubscription"],
|
|
2424
|
+
scopes: ["payments.charge:appUser"],
|
|
2425
|
+
},
|
|
2410
2426
|
// sc-4686 — the workspace's charge currency + the only sanctioned way to
|
|
2411
2427
|
// render money. A widget must NOT name a currency: the owner can change it
|
|
2412
2428
|
// after the widget was authored, so a baked symbol goes stale over a charge
|
|
@@ -4154,7 +4170,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4154
4170
|
type: "object",
|
|
4155
4171
|
required: false,
|
|
4156
4172
|
description:
|
|
4157
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4173
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). `paymentStatus: true` makes a STRING or BOOL column the table's payment status (at most one per table; not required, encrypted, immutable or defaulting to paid): a settled `requestPayment({ order: { table_id, record_id } })` for one of its rows writes it (`PAID`, or `true` on a BOOL; `REFUNDED` / `false` after a full refund), and every client value for it is ignored, so a buyer cannot mark their own order paid. A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4158
4174
|
},
|
|
4159
4175
|
translations: {
|
|
4160
4176
|
type: "object",
|
|
@@ -4455,7 +4471,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4455
4471
|
},
|
|
4456
4472
|
payments: {
|
|
4457
4473
|
description:
|
|
4458
|
-
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path). Backs usePayments() and
|
|
4474
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }>, getAppSubscription() -> Promise<{ active, renews, access_ends_at }>, cancelAppSubscription() -> Promise<{ active, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path, order). Backs usePayments(), useAccessPlans() and useAppSubscription(); requires the payments.charge:appUser scope. The host opens hosted Checkout itself — same-tab on web, in-app browser on native (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
4459
4475
|
required: true,
|
|
4460
4476
|
fields: {
|
|
4461
4477
|
requestPayment: "function",
|
|
@@ -4463,6 +4479,8 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4463
4479
|
listAccessPlans: "function",
|
|
4464
4480
|
purchaseAccessPlan: "function",
|
|
4465
4481
|
cancelAccessPlan: "function",
|
|
4482
|
+
getAppSubscription: "function",
|
|
4483
|
+
cancelAppSubscription: "function",
|
|
4466
4484
|
},
|
|
4467
4485
|
},
|
|
4468
4486
|
// sc-890 — backs useSendNotification().
|
|
@@ -6724,7 +6742,14 @@ const CONTRACT = deepFreeze({
|
|
|
6724
6742
|
// `deleteWhenLastMemberLeaves: true`.
|
|
6725
6743
|
// 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
|
|
6726
6744
|
// `changedFields`; no widget hook, primitive or manifest field changed.
|
|
6727
|
-
|
|
6745
|
+
// 1.235.0: additive (sc-15342) — `useAppSubscription()`, and the payments
|
|
6746
|
+
// slice gains `getAppSubscription` / `cancelAppSubscription`: a paid
|
|
6747
|
+
// app's subscriber cancels their own subscription.
|
|
6748
|
+
// 1.236.0: additive (sc-15358) — `requestPayment` takes an optional
|
|
6749
|
+
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6750
|
+
// a template column may declare `paymentStatus: true`, and four new
|
|
6751
|
+
// PaymentError codes.
|
|
6752
|
+
version: "1.236.0",
|
|
6728
6753
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6729
6754
|
hooks: HOOKS,
|
|
6730
6755
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2372,9 +2372,9 @@ const HOOKS = [
|
|
|
2372
2372
|
signature: "usePayments()",
|
|
2373
2373
|
returnShape: {
|
|
2374
2374
|
requestPayment:
|
|
2375
|
-
"({ amount_cents, currency?, description, kind, return_path? }) => Promise<{ id, status }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2375
|
+
"({ amount_cents, currency?, description, kind, return_path?, order? }) => Promise<{ id, status, order }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); order: { table_id, record_id } — the order row this pays for, in a table with a payment-status column; the SERVER marks it paid when the payment settles, so never write \"paid\" yourself (refused: ORDER_NOT_FOUND, ORDER_NOT_PAYABLE, ORDER_ALREADY_PAID; ORDER_PAYMENT_IN_PROGRESS is retryable while another checkout for the order is open); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2376
2376
|
getPayment:
|
|
2377
|
-
"(paymentId) => Promise<{ id, status, amount_cents, currency, description }>",
|
|
2377
|
+
"(paymentId) => Promise<{ id, status, amount_cents, currency, description, order }> // order: { table_id, record_id } | null — read it after returning from Checkout",
|
|
2378
2378
|
},
|
|
2379
2379
|
requiredContextSlice: ["payments.requestPayment"],
|
|
2380
2380
|
scopes: ["payments.charge:appUser"],
|
|
@@ -2403,6 +2403,22 @@ const HOOKS = [
|
|
|
2403
2403
|
],
|
|
2404
2404
|
scopes: ["payments.charge:appUser"],
|
|
2405
2405
|
},
|
|
2406
|
+
// sc-15342 — a paid app's subscriber cancels their own subscription.
|
|
2407
|
+
{
|
|
2408
|
+
name: "useAppSubscription",
|
|
2409
|
+
signature: "useAppSubscription()",
|
|
2410
|
+
returnShape: {
|
|
2411
|
+
subscription:
|
|
2412
|
+
"{ active, renews, access_ends_at } | null // the signed-in user's subscription to the app itself (a paid app): active = they hold paid access now; renews = a subscription will charge again (false once cancelled, or when bought in the App Store / Google Play); access_ends_at = when access ends without a renewal (null while it has no end, or when not held)",
|
|
2413
|
+
loading: "boolean",
|
|
2414
|
+
error: "PaymentError | null",
|
|
2415
|
+
cancel:
|
|
2416
|
+
"() => Promise<{ active, renews: false, access_ends_at }> // stop it renewing; access runs to access_ends_at, nothing is refunded — offer it whenever renews is true",
|
|
2417
|
+
refresh: "() => Promise<void>",
|
|
2418
|
+
},
|
|
2419
|
+
requiredContextSlice: ["payments.getAppSubscription", "payments.cancelAppSubscription"],
|
|
2420
|
+
scopes: ["payments.charge:appUser"],
|
|
2421
|
+
},
|
|
2406
2422
|
// sc-4686 — the workspace's charge currency + the only sanctioned way to
|
|
2407
2423
|
// render money. A widget must NOT name a currency: the owner can change it
|
|
2408
2424
|
// after the widget was authored, so a baked symbol goes stale over a charge
|
|
@@ -4150,7 +4166,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4150
4166
|
type: "object",
|
|
4151
4167
|
required: false,
|
|
4152
4168
|
description:
|
|
4153
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4169
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). `paymentStatus: true` makes a STRING or BOOL column the table's payment status (at most one per table; not required, encrypted, immutable or defaulting to paid): a settled `requestPayment({ order: { table_id, record_id } })` for one of its rows writes it (`PAID`, or `true` on a BOOL; `REFUNDED` / `false` after a full refund), and every client value for it is ignored, so a buyer cannot mark their own order paid. A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4154
4170
|
},
|
|
4155
4171
|
translations: {
|
|
4156
4172
|
type: "object",
|
|
@@ -4451,7 +4467,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4451
4467
|
},
|
|
4452
4468
|
payments: {
|
|
4453
4469
|
description:
|
|
4454
|
-
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path). Backs usePayments() and
|
|
4470
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }>, getAppSubscription() -> Promise<{ active, renews, access_ends_at }>, cancelAppSubscription() -> Promise<{ active, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path, order). Backs usePayments(), useAccessPlans() and useAppSubscription(); requires the payments.charge:appUser scope. The host opens hosted Checkout itself — same-tab on web, in-app browser on native (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
4455
4471
|
required: true,
|
|
4456
4472
|
fields: {
|
|
4457
4473
|
requestPayment: "function",
|
|
@@ -4459,6 +4475,8 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4459
4475
|
listAccessPlans: "function",
|
|
4460
4476
|
purchaseAccessPlan: "function",
|
|
4461
4477
|
cancelAccessPlan: "function",
|
|
4478
|
+
getAppSubscription: "function",
|
|
4479
|
+
cancelAppSubscription: "function",
|
|
4462
4480
|
},
|
|
4463
4481
|
},
|
|
4464
4482
|
// sc-890 — backs useSendNotification().
|
|
@@ -6720,7 +6738,14 @@ const CONTRACT = deepFreeze({
|
|
|
6720
6738
|
// `deleteWhenLastMemberLeaves: true`.
|
|
6721
6739
|
// 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
|
|
6722
6740
|
// `changedFields`; no widget hook, primitive or manifest field changed.
|
|
6723
|
-
|
|
6741
|
+
// 1.235.0: additive (sc-15342) — `useAppSubscription()`, and the payments
|
|
6742
|
+
// slice gains `getAppSubscription` / `cancelAppSubscription`: a paid
|
|
6743
|
+
// app's subscriber cancels their own subscription.
|
|
6744
|
+
// 1.236.0: additive (sc-15358) — `requestPayment` takes an optional
|
|
6745
|
+
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6746
|
+
// a template column may declare `paymentStatus: true`, and four new
|
|
6747
|
+
// PaymentError codes.
|
|
6748
|
+
version: "1.236.0",
|
|
6724
6749
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6725
6750
|
hooks: HOOKS,
|
|
6726
6751
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -6893,6 +6893,9 @@ const NON_RETRYABLE_PAYMENT_CODES = new Set([
|
|
|
6893
6893
|
"FORBIDDEN",
|
|
6894
6894
|
"INVALID_AMOUNT",
|
|
6895
6895
|
"NOT_FOUND",
|
|
6896
|
+
"ORDER_ALREADY_PAID",
|
|
6897
|
+
"ORDER_NOT_FOUND",
|
|
6898
|
+
"ORDER_NOT_PAYABLE",
|
|
6896
6899
|
"PAYMENTS_DISABLED",
|
|
6897
6900
|
"PAYMENTS_SCOPE_NOT_GRANTED",
|
|
6898
6901
|
"PAYMENTS_UNAVAILABLE",
|
|
@@ -6920,6 +6923,10 @@ const NON_RETRYABLE_PAYMENT_CODES = new Set([
|
|
|
6920
6923
|
* - "CHARGE_KIND_NOT_ENABLED" — this workspace cannot sell that kind yet
|
|
6921
6924
|
* - "DIGITAL_SALE_NEEDS_STORE_BILLING" — a digital sale from an iOS / Android
|
|
6922
6925
|
* build: store policy requires store billing
|
|
6926
|
+
* - "ORDER_NOT_FOUND" / "ORDER_NOT_PAYABLE" / "ORDER_ALREADY_PAID" — the
|
|
6927
|
+
* `order` is not one this user can pay for
|
|
6928
|
+
* - "ORDER_PAYMENT_IN_PROGRESS" — another checkout for the order is open
|
|
6929
|
+
* (retryable: it clears when that one ends)
|
|
6923
6930
|
* - "CONNECT_NOT_READY" / "PAYMENTS_DISABLED" — provider not ready
|
|
6924
6931
|
* - "DECLINED" — the charge was declined
|
|
6925
6932
|
* - "INTERNAL" — anything else
|
|
@@ -7104,91 +7111,131 @@ export function usePayments() {
|
|
|
7104
7111
|
* when `retryable` is false. Requires the `payments.charge:appUser` scope.
|
|
7105
7112
|
*/
|
|
7106
7113
|
export function useAccessPlans() {
|
|
7107
|
-
const
|
|
7114
|
+
const { state, refresh, call } = usePaymentsResource(
|
|
7115
|
+
"useAccessPlans",
|
|
7116
|
+
"listAccessPlans",
|
|
7117
|
+
"Plans are only available for an installed widget running in the app.",
|
|
7118
|
+
_accessPlansState,
|
|
7119
|
+
);
|
|
7120
|
+
|
|
7121
|
+
const purchase = useCallback(
|
|
7122
|
+
async (planId, opts) => {
|
|
7123
|
+
const res = await call("purchaseAccessPlan", {
|
|
7124
|
+
plan_id: planId,
|
|
7125
|
+
return_path: opts && opts.return_path,
|
|
7126
|
+
});
|
|
7127
|
+
if (res && res.status === "ACTIVE") await refresh();
|
|
7128
|
+
return res;
|
|
7129
|
+
},
|
|
7130
|
+
[call, refresh],
|
|
7131
|
+
);
|
|
7132
|
+
|
|
7133
|
+
const cancel = useCallback(
|
|
7134
|
+
async (planId) => {
|
|
7135
|
+
const res = await call("cancelAccessPlan", planId);
|
|
7136
|
+
await refresh();
|
|
7137
|
+
return res;
|
|
7138
|
+
},
|
|
7139
|
+
[call, refresh],
|
|
7140
|
+
);
|
|
7141
|
+
|
|
7142
|
+
return { ...state, purchase, cancel, refresh };
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
function _accessPlansState(res) {
|
|
7146
|
+
return {
|
|
7147
|
+
plans: Array.isArray(res && res.data) ? res.data : [],
|
|
7148
|
+
purchaseAvailable: Boolean(res && res.meta && res.meta.purchase_available),
|
|
7149
|
+
};
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
/**
|
|
7153
|
+
* sc-15342 — the signed-in app user's own subscription to the app itself (a
|
|
7154
|
+
* paid app), and the control to stop it renewing.
|
|
7155
|
+
*
|
|
7156
|
+
* subscription → { active, renews, access_ends_at }. `active`: they hold paid
|
|
7157
|
+
* access to the app now; `renews`: a subscription will charge again;
|
|
7158
|
+
* `access_ends_at`: when access ends without a renewal (null while it has
|
|
7159
|
+
* no end, or when not held).
|
|
7160
|
+
* cancel() → Promise<{ active, renews: false, access_ends_at }> — stop the
|
|
7161
|
+
* subscription renewing. Access continues until `access_ends_at`, then
|
|
7162
|
+
* ends; nothing is refunded. Offer it whenever `renews` is true. Access
|
|
7163
|
+
* bought in the App Store or Google Play is cancelled in the store, so it
|
|
7164
|
+
* reads `renews: false` here.
|
|
7165
|
+
* refresh() → re-read the subscription.
|
|
7166
|
+
*
|
|
7167
|
+
* `error` and `cancel` rejections are a `PaymentError` — show its `message`
|
|
7168
|
+
* when `retryable` is false. Requires the `payments.charge:appUser` scope.
|
|
7169
|
+
*/
|
|
7170
|
+
export function useAppSubscription() {
|
|
7171
|
+
const { state, setState, refresh, call } = usePaymentsResource(
|
|
7172
|
+
"useAppSubscription",
|
|
7173
|
+
"getAppSubscription",
|
|
7174
|
+
"The subscription is only available for an installed widget running in the app.",
|
|
7175
|
+
_appSubscriptionState,
|
|
7176
|
+
);
|
|
7177
|
+
|
|
7178
|
+
const cancel = useCallback(async () => {
|
|
7179
|
+
const res = await call("cancelAppSubscription");
|
|
7180
|
+
setState({ ..._appSubscriptionState(res), loading: false, error: null });
|
|
7181
|
+
return res;
|
|
7182
|
+
}, [call, setState]);
|
|
7183
|
+
|
|
7184
|
+
return { ...state, cancel, refresh };
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
function _appSubscriptionState(subscription) {
|
|
7188
|
+
return { subscription };
|
|
7189
|
+
}
|
|
7190
|
+
|
|
7191
|
+
/**
|
|
7192
|
+
* The payments read hooks' shared core: `readMethod` loads the state on mount
|
|
7193
|
+
* and on `refresh()`, `toState` maps its result, and `call(method, ...args)`
|
|
7194
|
+
* invokes a payments method with its rejection as a `PaymentError`.
|
|
7195
|
+
*/
|
|
7196
|
+
function usePaymentsResource(hookName, readMethod, unavailableMessage, toState) {
|
|
7197
|
+
const ctx = useWidgetContextOrThrow(hookName);
|
|
7108
7198
|
const paymentsRef = useRef(ctx.payments);
|
|
7109
7199
|
paymentsRef.current = ctx.payments;
|
|
7110
|
-
const supported = Boolean(
|
|
7111
|
-
ctx.payments && typeof ctx.payments.listAccessPlans === "function",
|
|
7112
|
-
);
|
|
7200
|
+
const supported = Boolean(ctx.payments && typeof ctx.payments[readMethod] === "function");
|
|
7113
7201
|
const [state, setState] = useState(() => ({
|
|
7114
|
-
|
|
7202
|
+
...toState(null),
|
|
7115
7203
|
loading: supported,
|
|
7116
|
-
error: supported
|
|
7117
|
-
? null
|
|
7118
|
-
: new PaymentError(
|
|
7119
|
-
"PAYMENTS_UNAVAILABLE",
|
|
7120
|
-
"Plans are only available for an installed widget running in the app.",
|
|
7121
|
-
),
|
|
7122
|
-
purchaseAvailable: false,
|
|
7204
|
+
error: supported ? null : new PaymentError("PAYMENTS_UNAVAILABLE", unavailableMessage),
|
|
7123
7205
|
}));
|
|
7124
7206
|
|
|
7125
7207
|
const refresh = useCallback(async () => {
|
|
7126
7208
|
const payments = paymentsRef.current;
|
|
7127
|
-
if (!payments || typeof payments
|
|
7209
|
+
if (!payments || typeof payments[readMethod] !== "function") return;
|
|
7128
7210
|
setState((prev) => ({ ...prev, loading: true }));
|
|
7129
7211
|
try {
|
|
7130
|
-
const res = await payments
|
|
7131
|
-
setState({
|
|
7132
|
-
plans: Array.isArray(res && res.data) ? res.data : [],
|
|
7133
|
-
loading: false,
|
|
7134
|
-
error: null,
|
|
7135
|
-
purchaseAvailable: Boolean(res && res.meta && res.meta.purchase_available),
|
|
7136
|
-
});
|
|
7212
|
+
const res = await payments[readMethod]();
|
|
7213
|
+
setState({ ...toState(res), loading: false, error: null });
|
|
7137
7214
|
} catch (err) {
|
|
7138
7215
|
setState((prev) => ({ ...prev, loading: false, error: toPaymentError(err) }));
|
|
7139
7216
|
}
|
|
7140
|
-
}, []);
|
|
7217
|
+
}, [readMethod, toState]);
|
|
7141
7218
|
|
|
7142
7219
|
useEffect(() => {
|
|
7143
7220
|
if (supported) refresh();
|
|
7144
7221
|
}, [supported, refresh]);
|
|
7145
7222
|
|
|
7146
|
-
const
|
|
7147
|
-
async (
|
|
7148
|
-
const payments = paymentsRef.current;
|
|
7149
|
-
if (!payments || typeof payments.purchaseAccessPlan !== "function") {
|
|
7150
|
-
throw new PaymentError(
|
|
7151
|
-
"PAYMENTS_UNAVAILABLE",
|
|
7152
|
-
"Plans are only available for an installed widget running in the app.",
|
|
7153
|
-
);
|
|
7154
|
-
}
|
|
7155
|
-
let res;
|
|
7156
|
-
try {
|
|
7157
|
-
res = await payments.purchaseAccessPlan({
|
|
7158
|
-
plan_id: planId,
|
|
7159
|
-
return_path: opts && opts.return_path,
|
|
7160
|
-
});
|
|
7161
|
-
} catch (err) {
|
|
7162
|
-
throw toPaymentError(err);
|
|
7163
|
-
}
|
|
7164
|
-
if (res && res.status === "ACTIVE") await refresh();
|
|
7165
|
-
return res;
|
|
7166
|
-
},
|
|
7167
|
-
[refresh],
|
|
7168
|
-
);
|
|
7169
|
-
|
|
7170
|
-
const cancel = useCallback(
|
|
7171
|
-
async (planId) => {
|
|
7223
|
+
const call = useCallback(
|
|
7224
|
+
async (method, ...args) => {
|
|
7172
7225
|
const payments = paymentsRef.current;
|
|
7173
|
-
if (!payments || typeof payments
|
|
7174
|
-
throw new PaymentError(
|
|
7175
|
-
"PAYMENTS_UNAVAILABLE",
|
|
7176
|
-
"Plans are only available for an installed widget running in the app.",
|
|
7177
|
-
);
|
|
7226
|
+
if (!payments || typeof payments[method] !== "function") {
|
|
7227
|
+
throw new PaymentError("PAYMENTS_UNAVAILABLE", unavailableMessage);
|
|
7178
7228
|
}
|
|
7179
|
-
let res;
|
|
7180
7229
|
try {
|
|
7181
|
-
|
|
7230
|
+
return await payments[method](...args);
|
|
7182
7231
|
} catch (err) {
|
|
7183
7232
|
throw toPaymentError(err);
|
|
7184
7233
|
}
|
|
7185
|
-
await refresh();
|
|
7186
|
-
return res;
|
|
7187
7234
|
},
|
|
7188
|
-
[
|
|
7235
|
+
[unavailableMessage],
|
|
7189
7236
|
);
|
|
7190
7237
|
|
|
7191
|
-
return {
|
|
7238
|
+
return { state, setState, refresh, call };
|
|
7192
7239
|
}
|
|
7193
7240
|
|
|
7194
7241
|
/* ============================================================================
|
package/dist/index.d.ts
CHANGED
|
@@ -713,6 +713,8 @@ export interface PaymentsClient {
|
|
|
713
713
|
cancelAccessPlan(
|
|
714
714
|
planId: string,
|
|
715
715
|
): Promise<{ plan_id: string; renews: false; access_ends_at: string | null }>;
|
|
716
|
+
getAppSubscription(): Promise<AppSubscription>;
|
|
717
|
+
cancelAppSubscription(): Promise<AppSubscription>;
|
|
716
718
|
}
|
|
717
719
|
|
|
718
720
|
/**
|
|
@@ -1536,6 +1538,20 @@ export interface PaymentRequest {
|
|
|
1536
1538
|
* to the current page, so the user lands back where they started.
|
|
1537
1539
|
*/
|
|
1538
1540
|
return_path?: string;
|
|
1541
|
+
/**
|
|
1542
|
+
* The order this charge pays for: a row the user may edit, in a table with
|
|
1543
|
+
* a payment-status column. When the payment settles the server sets that
|
|
1544
|
+
* column, so never write "paid" yourself; the platform ignores it anyway.
|
|
1545
|
+
* Refused with ORDER_NOT_FOUND, ORDER_NOT_PAYABLE or ORDER_ALREADY_PAID, and
|
|
1546
|
+
* with the retryable ORDER_PAYMENT_IN_PROGRESS while another checkout is open.
|
|
1547
|
+
*/
|
|
1548
|
+
order?: PaymentOrder;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
/** A row of a table with a payment-status column, by the ids a widget holds. */
|
|
1552
|
+
export interface PaymentOrder {
|
|
1553
|
+
table_id: string;
|
|
1554
|
+
record_id: string;
|
|
1539
1555
|
}
|
|
1540
1556
|
|
|
1541
1557
|
export interface PaymentResult {
|
|
@@ -1545,6 +1561,8 @@ export interface PaymentResult {
|
|
|
1545
1561
|
currency?: string;
|
|
1546
1562
|
description?: string;
|
|
1547
1563
|
kind?: "physical" | "service" | "digital" | null;
|
|
1564
|
+
/** The order the charge pays for, or null. Read it after returning from Checkout. */
|
|
1565
|
+
order?: PaymentOrder | null;
|
|
1548
1566
|
}
|
|
1549
1567
|
|
|
1550
1568
|
export interface PaymentsApi {
|
|
@@ -1610,6 +1628,31 @@ export interface AccessPlansApi {
|
|
|
1610
1628
|
*/
|
|
1611
1629
|
export function useAccessPlans(): AccessPlansApi;
|
|
1612
1630
|
|
|
1631
|
+
/** The signed-in app user's own subscription to the app itself (a paid app). */
|
|
1632
|
+
export interface AppSubscription {
|
|
1633
|
+
/** They hold paid access to the app now. */
|
|
1634
|
+
active: boolean;
|
|
1635
|
+
/** A subscription will charge again; false once cancelled, or bought in a store. */
|
|
1636
|
+
renews: boolean;
|
|
1637
|
+
/** ISO time access ends without a renewal; null while it has no end, or when not held. */
|
|
1638
|
+
access_ends_at: string | null;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
export interface AppSubscriptionApi {
|
|
1642
|
+
subscription: AppSubscription | null;
|
|
1643
|
+
loading: boolean;
|
|
1644
|
+
error: PaymentError | null;
|
|
1645
|
+
/** Stop it renewing; access runs to `access_ends_at`, nothing is refunded. */
|
|
1646
|
+
cancel(): Promise<AppSubscription>;
|
|
1647
|
+
refresh(): Promise<void>;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
/**
|
|
1651
|
+
* The signed-in app user's subscription to a paid app, and the control to
|
|
1652
|
+
* cancel it themselves. Requires the `payments.charge:appUser` scope.
|
|
1653
|
+
*/
|
|
1654
|
+
export function useAppSubscription(): AppSubscriptionApi;
|
|
1655
|
+
|
|
1613
1656
|
/**
|
|
1614
1657
|
* Rejection thrown by both `usePayments()` callbacks. `code` is the server's
|
|
1615
1658
|
* own reason when it sent one; `message` is its user-safe sentence. Branch on
|
|
@@ -1631,6 +1674,10 @@ export class PaymentError extends Error {
|
|
|
1631
1674
|
| "CHARGE_KIND_REQUIRED"
|
|
1632
1675
|
| "CHARGE_KIND_NOT_ENABLED"
|
|
1633
1676
|
| "DIGITAL_SALE_NEEDS_STORE_BILLING"
|
|
1677
|
+
| "ORDER_NOT_FOUND"
|
|
1678
|
+
| "ORDER_NOT_PAYABLE"
|
|
1679
|
+
| "ORDER_ALREADY_PAID"
|
|
1680
|
+
| "ORDER_PAYMENT_IN_PROGRESS"
|
|
1634
1681
|
| "PAYMENTS_DISABLED"
|
|
1635
1682
|
| "DECLINED"
|
|
1636
1683
|
| "FORBIDDEN"
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.288.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"homepage": "https://github.com/Colix-AB/AppStudio",
|
|
6
6
|
"type": "module",
|
package/src/contract.js
CHANGED
|
@@ -2372,9 +2372,9 @@ const HOOKS = [
|
|
|
2372
2372
|
signature: "usePayments()",
|
|
2373
2373
|
returnShape: {
|
|
2374
2374
|
requestPayment:
|
|
2375
|
-
"({ amount_cents, currency?, description, kind, return_path? }) => Promise<{ id, status }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2375
|
+
"({ amount_cents, currency?, description, kind, return_path?, order? }) => Promise<{ id, status, order }> // kind is REQUIRED: \"physical\" | \"service\" | \"digital\" — what is sold; a digital charge is refused in an iOS/Android build (DIGITAL_SALE_NEEDS_STORE_BILLING); order: { table_id, record_id } — the order row this pays for, in a table with a payment-status column; the SERVER marks it paid when the payment settles, so never write \"paid\" yourself (refused: ORDER_NOT_FOUND, ORDER_NOT_PAYABLE, ORDER_ALREADY_PAID; ORDER_PAYMENT_IN_PROGRESS is retryable while another checkout for the order is open); host opens hosted Checkout; rejects with PaymentError { code, message, retryable } — render the message when retryable is false, never \"try again\"",
|
|
2376
2376
|
getPayment:
|
|
2377
|
-
"(paymentId) => Promise<{ id, status, amount_cents, currency, description }>",
|
|
2377
|
+
"(paymentId) => Promise<{ id, status, amount_cents, currency, description, order }> // order: { table_id, record_id } | null — read it after returning from Checkout",
|
|
2378
2378
|
},
|
|
2379
2379
|
requiredContextSlice: ["payments.requestPayment"],
|
|
2380
2380
|
scopes: ["payments.charge:appUser"],
|
|
@@ -2403,6 +2403,22 @@ const HOOKS = [
|
|
|
2403
2403
|
],
|
|
2404
2404
|
scopes: ["payments.charge:appUser"],
|
|
2405
2405
|
},
|
|
2406
|
+
// sc-15342 — a paid app's subscriber cancels their own subscription.
|
|
2407
|
+
{
|
|
2408
|
+
name: "useAppSubscription",
|
|
2409
|
+
signature: "useAppSubscription()",
|
|
2410
|
+
returnShape: {
|
|
2411
|
+
subscription:
|
|
2412
|
+
"{ active, renews, access_ends_at } | null // the signed-in user's subscription to the app itself (a paid app): active = they hold paid access now; renews = a subscription will charge again (false once cancelled, or when bought in the App Store / Google Play); access_ends_at = when access ends without a renewal (null while it has no end, or when not held)",
|
|
2413
|
+
loading: "boolean",
|
|
2414
|
+
error: "PaymentError | null",
|
|
2415
|
+
cancel:
|
|
2416
|
+
"() => Promise<{ active, renews: false, access_ends_at }> // stop it renewing; access runs to access_ends_at, nothing is refunded — offer it whenever renews is true",
|
|
2417
|
+
refresh: "() => Promise<void>",
|
|
2418
|
+
},
|
|
2419
|
+
requiredContextSlice: ["payments.getAppSubscription", "payments.cancelAppSubscription"],
|
|
2420
|
+
scopes: ["payments.charge:appUser"],
|
|
2421
|
+
},
|
|
2406
2422
|
// sc-4686 — the workspace's charge currency + the only sanctioned way to
|
|
2407
2423
|
// render money. A widget must NOT name a currency: the owner can change it
|
|
2408
2424
|
// after the widget was authored, so a baked symbol goes stale over a charge
|
|
@@ -4150,7 +4166,7 @@ const MANIFEST_SCHEMA = {
|
|
|
4150
4166
|
type: "object",
|
|
4151
4167
|
required: false,
|
|
4152
4168
|
description:
|
|
4153
|
-
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4169
|
+
"Optional. Tables the widget needs, seeded into the workspace at install time. Authors wire them into the widget's `tableRef` properties via the Properties Panel — the SDK does not auto-bind. Limits: 100 tables, 200 columns per table, 100 sample rows per table and 20000 sample cells across the whole template (each row counts the keys it sets, and at least one, summed over every table). RELATION columns address siblings by `targetSuffix` (any table in the same array, in any order; tables may relate to each other both ways). Tables persist across uninstalls. A `tableRef` property may also carry `sharedTable: true`, declaring that the table is the APP'S OWN — the author picks an existing table and the widget seeds nothing for it. That is the correct shape for a widget that reads data it does not own (a chart, a metric, a manager view over another widget's table). `sharedTable` exempts the property from needing a seeded table at publish (`manifest.requiredTableRefsHaveTemplate`) WITHOUT making it optional: the author must still bind one, and the host still refuses to place the widget unbound. Use it instead of dropping `required` — a widget that silently renders an empty tile is the defect it looks like a fix for. sc-7530: a table may also declare `roleGrants` — group-scoped access named SYMBOLICALLY, because a group id belongs to the workspace that installs the widget and could never travel in the manifest. Declare the audiences once at template level as `roles: [{ key, label, description? }]` (key is lowercase kebab, max 32 roles), then per table `roleGrants: [{ role, canRead?, canWrite?, canDelete? }]`. The installing workspace binds each role to one of its own user groups (or has one created) during the install consent step, so a permission model where an admin group does everything and signed-in users only read survives the trip between workspaces. Use `publicGrant` for the two id-less audiences (everyone / any signed-in user) and `roleGrants` for anything that names a group. A role nobody binds simply writes no grant — it never fails the install. sc-7557: a column may declare `encrypted: true` — its values are stored AES-256-GCM encrypted at rest under a per-workspace subkey. The widget still reads and writes PLAINTEXT (the platform encrypts on write, decrypts for end users); studio users see `🔒` instead, and the column cannot be searched, filtered or sorted on. Sample `rows` you seed for it are encrypted the same way. Not valid on `RELATION` — its value is a foreign key the backend must resolve — and publishing one is rejected. An `inheritAcl` RELATION column may add `inheritAclReadOnly: true` — rows then inherit READ access only, so members of the parent read every child and may add children, but change or delete only the rows they created (pair with `grantCreatorPermissions`). A column may declare `defaultValue`, written when a create omits it: `@currentUser` on USER, `@now` on DATE / DATETIME, `@uuid` on UUID (a new random id per row), or a literal of the column's own type (none on FILE, RELATION or USER_GROUP — ids belong to the installing workspace). `defaultLocked: true` on an `@currentUser` / `@now` / `@uuid` default makes the server always stamp it and ignore any client value on create and update, so an author column cannot be forged. `immutable: true` makes a column write-once: a create sets it as usual and any value sent for it on update is ignored (not valid on a MANY_TO_MANY relation). `paymentStatus: true` makes a STRING or BOOL column the table's payment status (at most one per table; not required, encrypted, immutable or defaulting to paid): a settled `requestPayment({ order: { table_id, record_id } })` for one of its rows writes it (`PAID`, or `true` on a BOOL; `REFUNDED` / `false` after a full refund), and every client value for it is ignored, so a buyer cannot mark their own order paid. A table may declare `presence: true` so the people who share a row see which of each other are online (useOnlineMembers); it takes effect only while the workspace records user activity, and an upgrade that declares it turns it on for the existing table. A table whose rows people join and leave through per-record grants (conversations) may declare `deleteWhenLastMemberLeaves: true`: when a signed-in member giving up their own grant leaves a row with no grant and no table-wide grant lets anyone read it, the row and every row a CASCADE relation ties to it (including rows deleted earlier) are deleted for good. A row that never had a grant (a seeded sample row) is never deleted this way, and a revoke made in the Studio never deletes. An upgrade that declares it turns it on for the existing table. A ONE_TO_ONE / ONE_TO_MANY RELATION may declare `latestVia: \"<relation column on its target that points back>\"` (onDelete left at SET_NULL; that relation must have inheritAcl and be immutable): the server keeps it at the newest row of the target table that points back at this row. A column may declare `copyOf: { via, column }`: the server keeps it equal to `column` on the row the latest-row column `via` points at (same type, or text into text, cut to 140 characters in a STRING column; a copy of an encrypted column must be encrypted, and a copy cannot inheritAcl). Both are written only by the server, in the same write as the related row; a client value is ignored. A table may declare `constraints`, checked on every write for every client and refused as CONSTRAINT_VIOLATION: `unique` ({ columns, partitionColumns?, unlessRelated?: { relation, column } } — waived for rows whose related row has that yes/no column set), `no-overlap` ({ startColumn, endColumn, partitionColumns? }), `related-open` ({ relation, closedColumn } — no row is added, changed or removed while the related row's column is filled) and `related-index` ({ column, relation, listColumn } — the number must be a position in the related row's list). An upgrade adds a declared constraint to an existing table without judging the rows already in it.",
|
|
4154
4170
|
},
|
|
4155
4171
|
translations: {
|
|
4156
4172
|
type: "object",
|
|
@@ -4451,7 +4467,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4451
4467
|
},
|
|
4452
4468
|
payments: {
|
|
4453
4469
|
description:
|
|
4454
|
-
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path). Backs usePayments() and
|
|
4470
|
+
"Injected @colixsystems/payments-client instance (REQ-BILL-07-WIDGETPAY). { requestPayment(body) -> Promise<{ id, status }>, getPayment(id) -> Promise<payment>, listAccessPlans() -> Promise<{ data, meta }>, purchaseAccessPlan({ plan_id, return_path? }) -> Promise<{ plan_id, status }>, cancelAccessPlan(planId) -> Promise<{ plan_id, renews, access_ends_at }>, getAppSubscription() -> Promise<{ active, renews, access_ends_at }>, cancelAppSubscription() -> Promise<{ active, renews, access_ends_at }> }; wire is snake_case (amount_cents, return_path, order). Backs usePayments(), useAccessPlans() and useAppSubscription(); requires the payments.charge:appUser scope. The host opens hosted Checkout itself — same-tab on web, in-app browser on native (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
|
|
4455
4471
|
required: true,
|
|
4456
4472
|
fields: {
|
|
4457
4473
|
requestPayment: "function",
|
|
@@ -4459,6 +4475,8 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4459
4475
|
listAccessPlans: "function",
|
|
4460
4476
|
purchaseAccessPlan: "function",
|
|
4461
4477
|
cancelAccessPlan: "function",
|
|
4478
|
+
getAppSubscription: "function",
|
|
4479
|
+
cancelAppSubscription: "function",
|
|
4462
4480
|
},
|
|
4463
4481
|
},
|
|
4464
4482
|
// sc-890 — backs useSendNotification().
|
|
@@ -6720,7 +6738,14 @@ const CONTRACT = deepFreeze({
|
|
|
6720
6738
|
// `deleteWhenLastMemberLeaves: true`.
|
|
6721
6739
|
// 1.234.0: additive (sc-15609) — ACTION_SCRIPT_GLOBALS gains `previous` and
|
|
6722
6740
|
// `changedFields`; no widget hook, primitive or manifest field changed.
|
|
6723
|
-
|
|
6741
|
+
// 1.235.0: additive (sc-15342) — `useAppSubscription()`, and the payments
|
|
6742
|
+
// slice gains `getAppSubscription` / `cancelAppSubscription`: a paid
|
|
6743
|
+
// app's subscriber cancels their own subscription.
|
|
6744
|
+
// 1.236.0: additive (sc-15358) — `requestPayment` takes an optional
|
|
6745
|
+
// `order: { table_id, record_id }` the server marks paid on settlement,
|
|
6746
|
+
// a template column may declare `paymentStatus: true`, and four new
|
|
6747
|
+
// PaymentError codes.
|
|
6748
|
+
version: "1.236.0",
|
|
6724
6749
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6725
6750
|
hooks: HOOKS,
|
|
6726
6751
|
primitives: PRIMITIVES,
|