@colixsystems/widget-sdk 0.83.0 → 0.84.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 +5 -1
- package/dist/contract.cjs +1 -1
- package/dist/contract.js +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/linter.cjs +46 -0
- package/dist/linter.js +62 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
48
48
|
| **DIRECTORY** | `useGroups(query?)` | `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` | `directory.groups.*` — `groups.read:*` (mutations also `groups.write:*`) |
|
|
49
49
|
| **DIRECTORY** | `useBankIdLink()` | `{ linked, available, status, qr, message, startLink, refresh, cancel, unlink, refetchStatus, … }` | `directory.bankid.*` — no scope (JWT-gated self-service) |
|
|
50
50
|
| **FILESTORE** (`ctx.filestore`) | `usePdfExport({ spaceType, folderId? })` | `{ exportToPdf, exporting, error, lastExported }` | `ctx.filestore.files.exportPdf` — `files.write:*`. `exportToPdf(html, { fileName?, folderId? })` renders the HTML to a PDF server-side and saves it as a file (`application/pdf`); same server-side renderer on web + native. |
|
|
51
|
-
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. Rejects with `PaymentError { code, message, retryable }`; when `retryable` is `false` show `message` and drop the retry. |
|
|
51
|
+
| **PAYMENTS** (`ctx.payments`) | `usePayments()` | `{ requestPayment, getPayment }` | `ctx.payments.*` — `payments.charge:appUser`. 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). |
|
|
52
52
|
| **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`. |
|
|
53
53
|
| **IDENTIFICATION** (`ctx.identification`) | `useIdentification({ provider?, purpose?, pollIntervalMs? })` | `{ available, status, qr, autoStartToken, message, identity, identificationId, start, refresh, cancel, reset, … }` | `ctx.identification.*` — no scope (the visitor is deliberately NOT signed in). Gate the UI on `available`; `start()` opens the order and the hook polls to completion. `identity` carries `personal_number_masked` + a stable `subject_hash` — never a raw personal number. |
|
|
54
54
|
|
|
@@ -62,6 +62,10 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
62
62
|
|
|
63
63
|
`v0.77.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**.
|
|
64
64
|
|
|
65
|
+
### What's new in 0.84.0 (contract 1.59.0)
|
|
66
|
+
|
|
67
|
+
**A charge is denominated in the WORKSPACE's currency, and a widget that hardcodes a different one no longer publishes (sc-4649).** Every workspace picks the currency it charges its app users in, and `POST /payments/widget-charge` refuses any other code with `UNSUPPORTED_CURRENCY` — but nothing told a widget author which one that was. A widget priced in EUR for a workspace selling in SEK compiled, rendered, and looked finished, then failed every single checkout; the buyer read that as a generic "payment failed" and retried forever. Two changes: `currency` on `requestPayment` is best **omitted** (the platform applies the workspace's own, so it can never be wrong), and a literal that disagrees is now a publish-blocking `payment-currency` finding. Because the expected code is **per-workspace**, the SDK cannot know it: the rule fires only when the caller supplies `lintSource(source, { paymentCurrency })`, which the platform's publish gate does and a local `appstudio-widget lint` does not — it stays silent rather than guessing and flagging correct code. The rule is scoped to the argument of a `requestPayment(...)` call, so a `currency` field elsewhere (a datastore column, an `Intl.NumberFormat` option) is untouched. `CONTRACT.version` → `1.59.0`. Additive; a widget that omits `currency` or already matches its workspace is unaffected.
|
|
68
|
+
|
|
65
69
|
### What's new in 0.83.0 (contract 1.58.0)
|
|
66
70
|
|
|
67
71
|
**`PaymentError` tells you WHY a charge was refused, and whether retrying could ever help (sc-4650).** `usePayments()` mapped its rejections by reading `err.response`, but `@colixsystems/payments-client` throws typed errors carrying `.code` / `.status` / `.details` (the parsed error envelope) and no `.response` at all — so every server refusal arrived as `code: "INTERNAL"` and the real reason was buried on `err.cause`. A widget could not tell a workspace that has not declared its business identity yet (`BUSINESS_IDENTITY_REQUIRED`, which no retry clears) from a declined card.
|
package/dist/contract.cjs
CHANGED
|
@@ -2524,7 +2524,7 @@ const CONTRACT = deepFreeze({
|
|
|
2524
2524
|
// `recipient_expr` shape and no recipient column can name them. The member
|
|
2525
2525
|
// list stays host-side — a script receives a count, never the ids — and
|
|
2526
2526
|
// `exclude_user_id` keeps an author off their own message.
|
|
2527
|
-
version: "1.
|
|
2527
|
+
version: "1.59.0",
|
|
2528
2528
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2529
2529
|
hooks: HOOKS,
|
|
2530
2530
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -2524,7 +2524,7 @@ const CONTRACT = deepFreeze({
|
|
|
2524
2524
|
// `recipient_expr` shape and no recipient column can name them. The member
|
|
2525
2525
|
// list stays host-side — a script receives a count, never the ids — and
|
|
2526
2526
|
// `exclude_user_id` keeps an author off their own message.
|
|
2527
|
-
version: "1.
|
|
2527
|
+
version: "1.59.0",
|
|
2528
2528
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
2529
2529
|
hooks: HOOKS,
|
|
2530
2530
|
primitives: PRIMITIVES,
|
package/dist/index.d.ts
CHANGED
|
@@ -1766,7 +1766,19 @@ export interface LintFinding {
|
|
|
1766
1766
|
line: number;
|
|
1767
1767
|
snippet: string;
|
|
1768
1768
|
}
|
|
1769
|
-
export
|
|
1769
|
+
export interface LintOptions {
|
|
1770
|
+
manifest?: { requestedScopes?: string[]; supportedPlatforms?: string[] };
|
|
1771
|
+
/**
|
|
1772
|
+
* sc-4649 — the currency the target workspace charges its app users in. A
|
|
1773
|
+
* per-workspace value the SDK cannot know, so the `payment-currency` rule
|
|
1774
|
+
* stays silent unless the caller (the backend publish gate) supplies it.
|
|
1775
|
+
*/
|
|
1776
|
+
paymentCurrency?: string;
|
|
1777
|
+
}
|
|
1778
|
+
export function lintSource(
|
|
1779
|
+
source: string,
|
|
1780
|
+
options?: LintOptions,
|
|
1781
|
+
): {
|
|
1770
1782
|
ok: boolean;
|
|
1771
1783
|
findings: LintFinding[];
|
|
1772
1784
|
};
|
package/dist/linter.cjs
CHANGED
|
@@ -728,6 +728,49 @@ function _jsxOpenTagEnd(source, from) {
|
|
|
728
728
|
return source.length;
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
+
// sc-4649 — see linter.js for the rationale comment. The two files must stay
|
|
732
|
+
// in lockstep (the contract test asserts behaviour-equivalence).
|
|
733
|
+
const REQUEST_PAYMENT_CALL = "requestPayment(";
|
|
734
|
+
// A generous window: the options object is usually inline, occasionally spread
|
|
735
|
+
// over a few lines. Bounded so an unbalanced source can't scan to EOF.
|
|
736
|
+
const REQUEST_PAYMENT_WINDOW = 400;
|
|
737
|
+
const CURRENCY_LITERAL_RE = /currency\s*:\s*(['"`])\s*([A-Za-z]{2,8})\s*\1/;
|
|
738
|
+
|
|
739
|
+
function _paymentCurrencyRules(source, paymentCurrency) {
|
|
740
|
+
const expected = String(paymentCurrency || "").trim().toUpperCase();
|
|
741
|
+
if (!expected) return [];
|
|
742
|
+
const findings = [];
|
|
743
|
+
// Comments blanked, strings kept: the currency code IS a string literal.
|
|
744
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
745
|
+
const sourceLines = source.split(/\r?\n/);
|
|
746
|
+
let from = 0;
|
|
747
|
+
for (;;) {
|
|
748
|
+
const at = code.indexOf(REQUEST_PAYMENT_CALL, from);
|
|
749
|
+
if (at === -1) break;
|
|
750
|
+
from = at + REQUEST_PAYMENT_CALL.length;
|
|
751
|
+
const match = CURRENCY_LITERAL_RE.exec(
|
|
752
|
+
code.slice(at, at + REQUEST_PAYMENT_WINDOW),
|
|
753
|
+
);
|
|
754
|
+
if (!match) continue;
|
|
755
|
+
const found = match[2].toUpperCase();
|
|
756
|
+
if (found === expected) continue;
|
|
757
|
+
// Line of the currency literal itself, not of the call.
|
|
758
|
+
const line = code.slice(0, at + match.index).split(/\r?\n/).length;
|
|
759
|
+
findings.push({
|
|
760
|
+
rule: "payment-currency",
|
|
761
|
+
severity: "error",
|
|
762
|
+
label:
|
|
763
|
+
`requestPayment charges in "${found}" but this workspace sells in ` +
|
|
764
|
+
`${expected}, and the server refuses any other currency, so the ` +
|
|
765
|
+
`checkout can never complete. Omit currency (the platform applies ` +
|
|
766
|
+
`${expected}) or pass exactly "${expected}".`,
|
|
767
|
+
line,
|
|
768
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return findings;
|
|
772
|
+
}
|
|
773
|
+
|
|
731
774
|
// sc-4650 — soft warning: a widget that charges must tell a failure worth
|
|
732
775
|
// retrying from a refusal only the workspace owner can lift. Collapsing every
|
|
733
776
|
// rejection into one "please try again" is what sent payers round an
|
|
@@ -851,6 +894,9 @@ function lintSource(source, options) {
|
|
|
851
894
|
findings.push(..._reactInScopeRules(source));
|
|
852
895
|
findings.push(..._imagePercentHeightRules(source));
|
|
853
896
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
897
|
+
findings.push(
|
|
898
|
+
..._paymentCurrencyRules(source, options && options.paymentCurrency),
|
|
899
|
+
);
|
|
854
900
|
findings.push(..._paymentErrorHandlingRules(source));
|
|
855
901
|
findings.push(
|
|
856
902
|
..._scopeRules(source, options && options.manifest).map((f) => ({
|
package/dist/linter.js
CHANGED
|
@@ -846,6 +846,65 @@ function _jsxOpenTagEnd(source, from) {
|
|
|
846
846
|
return source.length;
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
// sc-4649 — payment-currency.
|
|
850
|
+
//
|
|
851
|
+
// Each workspace charges its app users in ONE currency of its own choosing
|
|
852
|
+
// (`Tenant.app_user_price_currency`), and the server refuses a charge in any
|
|
853
|
+
// other with `UNSUPPORTED_CURRENCY`. A widget that hardcodes a different code
|
|
854
|
+
// compiles, renders, and looks finished, but every checkout it runs dies on a
|
|
855
|
+
// 400 — so this is an `error`, not a warning like `payment-error-not-branched`:
|
|
856
|
+
// a literal mismatch is unambiguous and there is no correct code for an opt-out
|
|
857
|
+
// directive to rescue.
|
|
858
|
+
//
|
|
859
|
+
// The expected currency is a PER-WORKSPACE value the SDK cannot know, so this
|
|
860
|
+
// rule only fires when the caller supplies `options.paymentCurrency` — the
|
|
861
|
+
// backend publish gate does, a bare `appstudio-widget lint` does not. There is
|
|
862
|
+
// deliberately no default: guessing one would flag a correct widget.
|
|
863
|
+
//
|
|
864
|
+
// Scoped to the argument of a `requestPayment(...)` call. A `currency` field
|
|
865
|
+
// elsewhere (a datastore column, an `Intl.NumberFormat` option) is the widget's
|
|
866
|
+
// own display concern and is not judged here.
|
|
867
|
+
const REQUEST_PAYMENT_CALL = "requestPayment(";
|
|
868
|
+
// A generous window: the options object is usually inline, occasionally spread
|
|
869
|
+
// over a few lines. Bounded so an unbalanced source can't scan to EOF.
|
|
870
|
+
const REQUEST_PAYMENT_WINDOW = 400;
|
|
871
|
+
const CURRENCY_LITERAL_RE = /currency\s*:\s*(['"`])\s*([A-Za-z]{2,8})\s*\1/;
|
|
872
|
+
|
|
873
|
+
function _paymentCurrencyRules(source, paymentCurrency) {
|
|
874
|
+
const expected = String(paymentCurrency || "").trim().toUpperCase();
|
|
875
|
+
if (!expected) return [];
|
|
876
|
+
const findings = [];
|
|
877
|
+
// Comments blanked, strings kept: the currency code IS a string literal.
|
|
878
|
+
const code = _stripNonCode(source, { keepStrings: true });
|
|
879
|
+
const sourceLines = source.split(/\r?\n/);
|
|
880
|
+
let from = 0;
|
|
881
|
+
for (;;) {
|
|
882
|
+
const at = code.indexOf(REQUEST_PAYMENT_CALL, from);
|
|
883
|
+
if (at === -1) break;
|
|
884
|
+
from = at + REQUEST_PAYMENT_CALL.length;
|
|
885
|
+
const match = CURRENCY_LITERAL_RE.exec(
|
|
886
|
+
code.slice(at, at + REQUEST_PAYMENT_WINDOW),
|
|
887
|
+
);
|
|
888
|
+
if (!match) continue;
|
|
889
|
+
const found = match[2].toUpperCase();
|
|
890
|
+
if (found === expected) continue;
|
|
891
|
+
// Line of the currency literal itself, not of the call.
|
|
892
|
+
const line = code.slice(0, at + match.index).split(/\r?\n/).length;
|
|
893
|
+
findings.push({
|
|
894
|
+
rule: "payment-currency",
|
|
895
|
+
severity: "error",
|
|
896
|
+
label:
|
|
897
|
+
`requestPayment charges in "${found}" but this workspace sells in ` +
|
|
898
|
+
`${expected}, and the server refuses any other currency, so the ` +
|
|
899
|
+
`checkout can never complete. Omit currency (the platform applies ` +
|
|
900
|
+
`${expected}) or pass exactly "${expected}".`,
|
|
901
|
+
line,
|
|
902
|
+
snippet: (sourceLines[line - 1] || "").trim().slice(0, 200),
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
return findings;
|
|
906
|
+
}
|
|
907
|
+
|
|
849
908
|
// sc-4650 — soft warning: a widget that charges must tell a failure worth
|
|
850
909
|
// retrying from a refusal only the workspace owner can lift. Collapsing every
|
|
851
910
|
// rejection into one "please try again" is what sent payers round an
|
|
@@ -981,6 +1040,9 @@ export function lintSource(source, options) {
|
|
|
981
1040
|
// sc-3493 — soft warning: percentage height on an <Image> collapses to 0.
|
|
982
1041
|
findings.push(..._imagePercentHeightRules(source));
|
|
983
1042
|
// sc-4650 — soft warning: every payment refusal reported as "try again".
|
|
1043
|
+
findings.push(
|
|
1044
|
+
..._paymentCurrencyRules(source, options && options.paymentCurrency),
|
|
1045
|
+
);
|
|
984
1046
|
findings.push(..._paymentErrorHandlingRules(source));
|
|
985
1047
|
// REQ-USERMGMT / REQ-ACL-SYS M3 — scope-aware rules. Run after the
|
|
986
1048
|
// line-by-line scan so banned-identifier findings stay first in the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.84.0",
|
|
4
4
|
"description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|