@colixsystems/widget-sdk 0.286.0 → 0.287.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 +7 -2
- package/dist/contract.cjs +23 -2
- package/dist/contract.js +23 -2
- package/dist/hooks.js +98 -58
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1 -0
- package/dist/index.native.js +1 -0
- package/package.json +1 -1
- package/src/contract.js +23 -2
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,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
89
90
|
|
|
90
91
|
## Status
|
|
91
92
|
|
|
92
|
-
`v0.
|
|
93
|
+
`v0.287.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.287.0 (contract 1.235.0)
|
|
96
|
+
|
|
97
|
+
**`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
98
|
|
|
94
99
|
### What's new in 0.286.0 (contract 1.234.0)
|
|
95
100
|
|
package/dist/contract.cjs
CHANGED
|
@@ -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
|
|
@@ -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). 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,10 @@ 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
|
+
version: "1.235.0",
|
|
6728
6749
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6729
6750
|
hooks: HOOKS,
|
|
6730
6751
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -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
|
|
@@ -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). 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,10 @@ 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
|
+
version: "1.235.0",
|
|
6724
6745
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6725
6746
|
hooks: HOOKS,
|
|
6726
6747
|
primitives: PRIMITIVES,
|
package/dist/hooks.js
CHANGED
|
@@ -7104,91 +7104,131 @@ export function usePayments() {
|
|
|
7104
7104
|
* when `retryable` is false. Requires the `payments.charge:appUser` scope.
|
|
7105
7105
|
*/
|
|
7106
7106
|
export function useAccessPlans() {
|
|
7107
|
-
const
|
|
7107
|
+
const { state, refresh, call } = usePaymentsResource(
|
|
7108
|
+
"useAccessPlans",
|
|
7109
|
+
"listAccessPlans",
|
|
7110
|
+
"Plans are only available for an installed widget running in the app.",
|
|
7111
|
+
_accessPlansState,
|
|
7112
|
+
);
|
|
7113
|
+
|
|
7114
|
+
const purchase = useCallback(
|
|
7115
|
+
async (planId, opts) => {
|
|
7116
|
+
const res = await call("purchaseAccessPlan", {
|
|
7117
|
+
plan_id: planId,
|
|
7118
|
+
return_path: opts && opts.return_path,
|
|
7119
|
+
});
|
|
7120
|
+
if (res && res.status === "ACTIVE") await refresh();
|
|
7121
|
+
return res;
|
|
7122
|
+
},
|
|
7123
|
+
[call, refresh],
|
|
7124
|
+
);
|
|
7125
|
+
|
|
7126
|
+
const cancel = useCallback(
|
|
7127
|
+
async (planId) => {
|
|
7128
|
+
const res = await call("cancelAccessPlan", planId);
|
|
7129
|
+
await refresh();
|
|
7130
|
+
return res;
|
|
7131
|
+
},
|
|
7132
|
+
[call, refresh],
|
|
7133
|
+
);
|
|
7134
|
+
|
|
7135
|
+
return { ...state, purchase, cancel, refresh };
|
|
7136
|
+
}
|
|
7137
|
+
|
|
7138
|
+
function _accessPlansState(res) {
|
|
7139
|
+
return {
|
|
7140
|
+
plans: Array.isArray(res && res.data) ? res.data : [],
|
|
7141
|
+
purchaseAvailable: Boolean(res && res.meta && res.meta.purchase_available),
|
|
7142
|
+
};
|
|
7143
|
+
}
|
|
7144
|
+
|
|
7145
|
+
/**
|
|
7146
|
+
* sc-15342 — the signed-in app user's own subscription to the app itself (a
|
|
7147
|
+
* paid app), and the control to stop it renewing.
|
|
7148
|
+
*
|
|
7149
|
+
* subscription → { active, renews, access_ends_at }. `active`: they hold paid
|
|
7150
|
+
* access to the app now; `renews`: a subscription will charge again;
|
|
7151
|
+
* `access_ends_at`: when access ends without a renewal (null while it has
|
|
7152
|
+
* no end, or when not held).
|
|
7153
|
+
* cancel() → Promise<{ active, renews: false, access_ends_at }> — stop the
|
|
7154
|
+
* subscription renewing. Access continues until `access_ends_at`, then
|
|
7155
|
+
* ends; nothing is refunded. Offer it whenever `renews` is true. Access
|
|
7156
|
+
* bought in the App Store or Google Play is cancelled in the store, so it
|
|
7157
|
+
* reads `renews: false` here.
|
|
7158
|
+
* refresh() → re-read the subscription.
|
|
7159
|
+
*
|
|
7160
|
+
* `error` and `cancel` rejections are a `PaymentError` — show its `message`
|
|
7161
|
+
* when `retryable` is false. Requires the `payments.charge:appUser` scope.
|
|
7162
|
+
*/
|
|
7163
|
+
export function useAppSubscription() {
|
|
7164
|
+
const { state, setState, refresh, call } = usePaymentsResource(
|
|
7165
|
+
"useAppSubscription",
|
|
7166
|
+
"getAppSubscription",
|
|
7167
|
+
"The subscription is only available for an installed widget running in the app.",
|
|
7168
|
+
_appSubscriptionState,
|
|
7169
|
+
);
|
|
7170
|
+
|
|
7171
|
+
const cancel = useCallback(async () => {
|
|
7172
|
+
const res = await call("cancelAppSubscription");
|
|
7173
|
+
setState({ ..._appSubscriptionState(res), loading: false, error: null });
|
|
7174
|
+
return res;
|
|
7175
|
+
}, [call, setState]);
|
|
7176
|
+
|
|
7177
|
+
return { ...state, cancel, refresh };
|
|
7178
|
+
}
|
|
7179
|
+
|
|
7180
|
+
function _appSubscriptionState(subscription) {
|
|
7181
|
+
return { subscription };
|
|
7182
|
+
}
|
|
7183
|
+
|
|
7184
|
+
/**
|
|
7185
|
+
* The payments read hooks' shared core: `readMethod` loads the state on mount
|
|
7186
|
+
* and on `refresh()`, `toState` maps its result, and `call(method, ...args)`
|
|
7187
|
+
* invokes a payments method with its rejection as a `PaymentError`.
|
|
7188
|
+
*/
|
|
7189
|
+
function usePaymentsResource(hookName, readMethod, unavailableMessage, toState) {
|
|
7190
|
+
const ctx = useWidgetContextOrThrow(hookName);
|
|
7108
7191
|
const paymentsRef = useRef(ctx.payments);
|
|
7109
7192
|
paymentsRef.current = ctx.payments;
|
|
7110
|
-
const supported = Boolean(
|
|
7111
|
-
ctx.payments && typeof ctx.payments.listAccessPlans === "function",
|
|
7112
|
-
);
|
|
7193
|
+
const supported = Boolean(ctx.payments && typeof ctx.payments[readMethod] === "function");
|
|
7113
7194
|
const [state, setState] = useState(() => ({
|
|
7114
|
-
|
|
7195
|
+
...toState(null),
|
|
7115
7196
|
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,
|
|
7197
|
+
error: supported ? null : new PaymentError("PAYMENTS_UNAVAILABLE", unavailableMessage),
|
|
7123
7198
|
}));
|
|
7124
7199
|
|
|
7125
7200
|
const refresh = useCallback(async () => {
|
|
7126
7201
|
const payments = paymentsRef.current;
|
|
7127
|
-
if (!payments || typeof payments
|
|
7202
|
+
if (!payments || typeof payments[readMethod] !== "function") return;
|
|
7128
7203
|
setState((prev) => ({ ...prev, loading: true }));
|
|
7129
7204
|
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
|
-
});
|
|
7205
|
+
const res = await payments[readMethod]();
|
|
7206
|
+
setState({ ...toState(res), loading: false, error: null });
|
|
7137
7207
|
} catch (err) {
|
|
7138
7208
|
setState((prev) => ({ ...prev, loading: false, error: toPaymentError(err) }));
|
|
7139
7209
|
}
|
|
7140
|
-
}, []);
|
|
7210
|
+
}, [readMethod, toState]);
|
|
7141
7211
|
|
|
7142
7212
|
useEffect(() => {
|
|
7143
7213
|
if (supported) refresh();
|
|
7144
7214
|
}, [supported, refresh]);
|
|
7145
7215
|
|
|
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) => {
|
|
7216
|
+
const call = useCallback(
|
|
7217
|
+
async (method, ...args) => {
|
|
7172
7218
|
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
|
-
);
|
|
7219
|
+
if (!payments || typeof payments[method] !== "function") {
|
|
7220
|
+
throw new PaymentError("PAYMENTS_UNAVAILABLE", unavailableMessage);
|
|
7178
7221
|
}
|
|
7179
|
-
let res;
|
|
7180
7222
|
try {
|
|
7181
|
-
|
|
7223
|
+
return await payments[method](...args);
|
|
7182
7224
|
} catch (err) {
|
|
7183
7225
|
throw toPaymentError(err);
|
|
7184
7226
|
}
|
|
7185
|
-
await refresh();
|
|
7186
|
-
return res;
|
|
7187
7227
|
},
|
|
7188
|
-
[
|
|
7228
|
+
[unavailableMessage],
|
|
7189
7229
|
);
|
|
7190
7230
|
|
|
7191
|
-
return {
|
|
7231
|
+
return { state, setState, refresh, call };
|
|
7192
7232
|
}
|
|
7193
7233
|
|
|
7194
7234
|
/* ============================================================================
|
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
|
/**
|
|
@@ -1610,6 +1612,31 @@ export interface AccessPlansApi {
|
|
|
1610
1612
|
*/
|
|
1611
1613
|
export function useAccessPlans(): AccessPlansApi;
|
|
1612
1614
|
|
|
1615
|
+
/** The signed-in app user's own subscription to the app itself (a paid app). */
|
|
1616
|
+
export interface AppSubscription {
|
|
1617
|
+
/** They hold paid access to the app now. */
|
|
1618
|
+
active: boolean;
|
|
1619
|
+
/** A subscription will charge again; false once cancelled, or bought in a store. */
|
|
1620
|
+
renews: boolean;
|
|
1621
|
+
/** ISO time access ends without a renewal; null while it has no end, or when not held. */
|
|
1622
|
+
access_ends_at: string | null;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
export interface AppSubscriptionApi {
|
|
1626
|
+
subscription: AppSubscription | null;
|
|
1627
|
+
loading: boolean;
|
|
1628
|
+
error: PaymentError | null;
|
|
1629
|
+
/** Stop it renewing; access runs to `access_ends_at`, nothing is refunded. */
|
|
1630
|
+
cancel(): Promise<AppSubscription>;
|
|
1631
|
+
refresh(): Promise<void>;
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
/**
|
|
1635
|
+
* The signed-in app user's subscription to a paid app, and the control to
|
|
1636
|
+
* cancel it themselves. Requires the `payments.charge:appUser` scope.
|
|
1637
|
+
*/
|
|
1638
|
+
export function useAppSubscription(): AppSubscriptionApi;
|
|
1639
|
+
|
|
1613
1640
|
/**
|
|
1614
1641
|
* Rejection thrown by both `usePayments()` callbacks. `code` is the server's
|
|
1615
1642
|
* own reason when it sent one; `message` is its user-safe sentence. Branch on
|
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.287.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
|
@@ -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
|
|
@@ -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). 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,10 @@ 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
|
+
version: "1.235.0",
|
|
6724
6745
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6725
6746
|
hooks: HOOKS,
|
|
6726
6747
|
primitives: PRIMITIVES,
|