@colixsystems/widget-sdk 0.8.0 → 0.10.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 CHANGED
@@ -6,9 +6,18 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
6
6
 
7
7
  ## Status
8
8
 
9
- `v0.8.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**.
9
+ `v0.10.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**.
10
10
 
11
- ### What's new in 0.8.0
11
+ ### What's new in 0.10.0
12
+
13
+ - **`useUser()` is wired.** Returns the active end-user identity `{ id, email, displayName, roles, groupIds }` from the host-provided `WidgetContext`. `id` is `null` for anonymous visitors and on the Studio canvas preview. All fields are guaranteed present (the host fills safe defaults), so widgets read them without optional chaining. Additive — no migration needed for existing widgets.
14
+
15
+ ### What's new in 0.9.0
16
+
17
+ - **`usePayments()` — incoming app-user payments (REQ-BILL-07-WIDGETPAY).** Returns `{ requestPayment, getPayment }`. `requestPayment({ amountCents, currency?, description, metadata?, returnPath? })` triggers a one-time charge from the signed-in app user and resolves to `{ id, status, checkoutUrl? }`: when `checkoutUrl` is present the widget opens it (web: navigate; native: `expo-web-browser`) — the user pays in **hosted Stripe Checkout**; when absent (the platform's built-in **mock** provider, the default until Stripe is configured) the charge auto-confirms (`status: "PAID"`). `getPayment(id)` polls the terminal status. Backed by a new `WidgetContext.payments` slice and gated by the new `payments.charge:appUser` scope. **No card data ever touches the widget** — never collect card fields yourself. The charge settles to the workspace owner; the amount is bounded by a platform per-charge cap. Rejections are a structured `PaymentError` (also a new named export) with a stable `.code`.
18
+ - **`CONTRACT.version` → `1.2.0`** (additive: one new hook, one new context slice, one new scope, one new error class). No existing export changed signature.
19
+
20
+ ### What was in 0.8.0
12
21
 
13
22
  - **`useDirectory()` — read-only user directory hook (REQ-DIR-01).** Returns `{ users, loading, error, refetch }` where each user is `{ id, name, role }`. Backed by a new `WidgetContext.directory.listUsers(query)` slice and gated by the new `directory.read:users` scope. Use it to build a chat people-list, an @-mention picker, or to resolve an author id to a display name. The host reads `GET /api/v1/app/users`, which hands non-Studio (Player) callers the reduced `{ id, name, role }` projection — email and other admin-only fields never leave the server for an app end-user. `query` is an optional `{ q, role, isActive, limit, offset }` (`q` substring-matches the display name; `role` is `"USER"` (default), `"INTEGRATION"`, or `"ALL"`). Mutating users is not part of the widget surface — the directory is read-only.
14
23
  - **`CONTRACT.version` → `1.1.0`** (additive: one new hook, one new context slice, one new scope). No existing export changed signature.
@@ -66,7 +75,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
66
75
 
67
76
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
68
77
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
69
- - `useDatastoreQuery`, `useDatastoreMutation`, `useDirectory`, `useWidgetEvent`, `useTheme`, `useI18n` — hooks that read from the host-provided `WidgetContext`. `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope.
78
+ - `useDatastoreQuery`, `useDatastoreMutation`, `useDirectory`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser` — hooks that read from the host-provided `WidgetContext`. `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, displayName, roles, groupIds }` (`id` is `null` for anonymous / preview).
70
79
  - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet` — re-exported from `react-native`. The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. See https://reactnative.dev/docs/ for per-component props.
71
80
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
72
81
 
package/dist/contract.cjs CHANGED
@@ -55,6 +55,19 @@ const HOOKS = [
55
55
  requiredContextSlice: ["i18n.t", "i18n.locale"],
56
56
  scopes: null,
57
57
  },
58
+ {
59
+ name: "useUser",
60
+ signature: "useUser()",
61
+ returnShape: {
62
+ id: "string | null",
63
+ email: "string | null",
64
+ displayName: "string | null",
65
+ roles: "string[]",
66
+ groupIds: "string[]",
67
+ },
68
+ requiredContextSlice: ["user"],
69
+ scopes: null,
70
+ },
58
71
  {
59
72
  name: "useDatastoreQuery",
60
73
  signature: "useDatastoreQuery(tableId, options?)",
@@ -97,6 +110,18 @@ const HOOKS = [
97
110
  requiredContextSlice: ["events.emit"],
98
111
  scopes: null,
99
112
  },
113
+ {
114
+ name: "usePayments",
115
+ signature: "usePayments()",
116
+ returnShape: {
117
+ requestPayment:
118
+ "({ amountCents, currency?, description, metadata? }) => Promise<{ id, status, checkoutUrl? }> // rejects with PaymentError",
119
+ getPayment:
120
+ "(paymentId) => Promise<{ id, status, amountCents, currency, description }>",
121
+ },
122
+ requiredContextSlice: ["payments.requestPayment"],
123
+ scopes: ["payments.charge:appUser"],
124
+ },
100
125
  ];
101
126
 
102
127
  // REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
@@ -335,6 +360,12 @@ const WIDGET_CONTEXT_SHAPE = {
335
360
  required: true,
336
361
  fields: { emit: "function" },
337
362
  },
363
+ payments: {
364
+ description:
365
+ "Incoming app-user payments (REQ-BILL-07-WIDGETPAY). { requestPayment({ amountCents, currency?, description, metadata? }) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
366
+ required: true,
367
+ fields: { requestPayment: "function", getPayment: "function" },
368
+ },
338
369
  i18n: {
339
370
  description: "{ t(key, fallback?), locale }.",
340
371
  required: true,
@@ -427,7 +458,7 @@ function deepFreeze(value) {
427
458
  }
428
459
 
429
460
  const CONTRACT = deepFreeze({
430
- version: "1.1.0",
461
+ version: "1.2.0",
431
462
  hooks: HOOKS,
432
463
  primitives: PRIMITIVES,
433
464
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -55,6 +55,19 @@ const HOOKS = [
55
55
  requiredContextSlice: ["i18n.t", "i18n.locale"],
56
56
  scopes: null,
57
57
  },
58
+ {
59
+ name: "useUser",
60
+ signature: "useUser()",
61
+ returnShape: {
62
+ id: "string | null",
63
+ email: "string | null",
64
+ displayName: "string | null",
65
+ roles: "string[]",
66
+ groupIds: "string[]",
67
+ },
68
+ requiredContextSlice: ["user"],
69
+ scopes: null,
70
+ },
58
71
  {
59
72
  name: "useDatastoreQuery",
60
73
  signature: "useDatastoreQuery(tableId, options?)",
@@ -98,6 +111,18 @@ const HOOKS = [
98
111
  requiredContextSlice: ["events.emit"],
99
112
  scopes: null,
100
113
  },
114
+ {
115
+ name: "usePayments",
116
+ signature: "usePayments()",
117
+ returnShape: {
118
+ requestPayment:
119
+ "({ amountCents, currency?, description, metadata? }) => Promise<{ id, status, checkoutUrl? }> // rejects with PaymentError",
120
+ getPayment:
121
+ "(paymentId) => Promise<{ id, status, amountCents, currency, description }>",
122
+ },
123
+ requiredContextSlice: ["payments.requestPayment"],
124
+ scopes: ["payments.charge:appUser"],
125
+ },
101
126
  ];
102
127
 
103
128
  // REQ-WSDK-RN-WEB: see contract.cjs for the source-of-truth comment.
@@ -329,6 +354,12 @@ const WIDGET_CONTEXT_SHAPE = {
329
354
  required: true,
330
355
  fields: { emit: "function" },
331
356
  },
357
+ payments: {
358
+ description:
359
+ "Incoming app-user payments (REQ-BILL-07-WIDGETPAY). { requestPayment({ amountCents, currency?, description, metadata? }) -> Promise<{ id, status, checkoutUrl? }>, getPayment(id) -> Promise<payment> }. Backs usePayments(); requires the payments.charge:appUser scope. The host opens hosted Checkout (or auto-confirms under the mock provider); the charge settles to the workspace owner.",
360
+ required: true,
361
+ fields: { requestPayment: "function", getPayment: "function" },
362
+ },
332
363
  i18n: {
333
364
  description: "{ t(key, fallback?), locale }.",
334
365
  required: true,
@@ -419,7 +450,7 @@ function deepFreeze(value) {
419
450
  }
420
451
 
421
452
  const CONTRACT = deepFreeze({
422
- version: "1.1.0",
453
+ version: "1.2.0",
423
454
  hooks: HOOKS,
424
455
  primitives: PRIMITIVES,
425
456
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -352,6 +352,130 @@ export function useTheme() {
352
352
  return ctx.workspace.theme;
353
353
  }
354
354
 
355
+ /**
356
+ * Returns the active end-user identity:
357
+ * `{ id, email, displayName, roles, groupIds }`.
358
+ *
359
+ * `id` is `null` for anonymous visitors (and on the Studio canvas preview,
360
+ * which renders widgets as if signed-out so the public branch shows). All
361
+ * fields are guaranteed present by the host (`buildHostWidgetContext`
362
+ * + the native `WidgetHost`); widgets read them without optional chaining.
363
+ *
364
+ * Use this to render the signed-in user's name in a header, branch on
365
+ * roles, or stamp a created-by field. Email is opaque to widgets that
366
+ * only need a display name — prefer `displayName` for UI strings.
367
+ */
368
+ export function useUser() {
369
+ const ctx = useWidgetContextOrThrow("useUser");
370
+ return ctx.user;
371
+ }
372
+
373
+ /**
374
+ * Structured error thrown by `usePayments` callbacks. Carries a stable
375
+ * `code` so widgets can branch without parsing message strings.
376
+ *
377
+ * `code` is one of:
378
+ * - "AUTH_REQUIRED" — no signed-in app user
379
+ * - "PAYMENTS_SCOPE_NOT_GRANTED"— widget lacks payments.charge:appUser
380
+ * - "INVALID_AMOUNT" / "AMOUNT_TOO_LARGE" / "VALIDATION" — bad request
381
+ * - "CONNECT_NOT_READY" / "PAYMENTS_DISABLED" — provider not ready
382
+ * - "DECLINED" — the charge was declined
383
+ * - "INTERNAL" — anything else
384
+ */
385
+ export class PaymentError extends Error {
386
+ constructor(code, message, opts) {
387
+ super(message);
388
+ this.name = "PaymentError";
389
+ this.code = code;
390
+ if (opts && opts.cause) this.cause = opts.cause;
391
+ }
392
+ }
393
+
394
+ function toPaymentError(err) {
395
+ if (err instanceof PaymentError) return err;
396
+ const status =
397
+ err && err.response && typeof err.response.status === "number"
398
+ ? err.response.status
399
+ : null;
400
+ const bodyCode =
401
+ err && err.response && err.response.data && err.response.data.code;
402
+ const bodyMessage =
403
+ err && err.response && err.response.data && err.response.data.error;
404
+ let code = "INTERNAL";
405
+ if (typeof bodyCode === "string" && bodyCode) code = bodyCode;
406
+ else if (status === 401) code = "AUTH_REQUIRED";
407
+ else if (status === 402) code = "DECLINED";
408
+ else if (status === 403) code = "FORBIDDEN";
409
+ else if (status === 400) code = "VALIDATION";
410
+ const message =
411
+ (typeof bodyMessage === "string" && bodyMessage) ||
412
+ (err && typeof err.message === "string"
413
+ ? err.message
414
+ : "Payment request failed");
415
+ return new PaymentError(code, message, { cause: err });
416
+ }
417
+
418
+ /**
419
+ * Incoming app-user payments (REQ-BILL-07-WIDGETPAY). Returns
420
+ * `{ requestPayment, getPayment }`.
421
+ *
422
+ * requestPayment({ amountCents, currency?, description, metadata? })
423
+ * → Promise<{ id, status, checkoutUrl?, ... }>. The host either
424
+ * auto-confirms (mock provider, `status: "PAID"`, no redirect) or
425
+ * returns a hosted-Checkout `checkoutUrl` the widget should open
426
+ * (Stripe provider, `status: "PENDING"`). Rejects with a
427
+ * `PaymentError`.
428
+ * getPayment(paymentId) → Promise<payment> — poll the terminal status.
429
+ *
430
+ * Requires the `payments.charge:appUser` scope in the manifest's
431
+ * `requestedScopes`. The charge settles to the workspace owner; the app
432
+ * user confirms the amount in hosted Checkout. No card data touches the
433
+ * widget — never collect card fields yourself.
434
+ */
435
+ export function usePayments() {
436
+ const ctx = useWidgetContextOrThrow("usePayments");
437
+ if (!ctx.payments || typeof ctx.payments.requestPayment !== "function") {
438
+ throw new Error(
439
+ "usePayments: host did not inject a payments client. The widget must " +
440
+ "declare the payments.charge:appUser scope and be installed in a " +
441
+ "workspace whose host supports payments.",
442
+ );
443
+ }
444
+ const requestRef = useRef(ctx.payments.requestPayment);
445
+ const getRef = useRef(
446
+ typeof ctx.payments.getPayment === "function"
447
+ ? ctx.payments.getPayment
448
+ : null,
449
+ );
450
+ requestRef.current = ctx.payments.requestPayment;
451
+ getRef.current =
452
+ typeof ctx.payments.getPayment === "function"
453
+ ? ctx.payments.getPayment
454
+ : null;
455
+
456
+ const requestPayment = useCallback(async (args) => {
457
+ try {
458
+ return await requestRef.current(args);
459
+ } catch (err) {
460
+ throw toPaymentError(err);
461
+ }
462
+ }, []);
463
+ const getPayment = useCallback(async (paymentId) => {
464
+ if (!getRef.current) {
465
+ throw new PaymentError(
466
+ "INTERNAL",
467
+ "getPayment is not supported by this host.",
468
+ );
469
+ }
470
+ try {
471
+ return await getRef.current(paymentId);
472
+ } catch (err) {
473
+ throw toPaymentError(err);
474
+ }
475
+ }, []);
476
+ return { requestPayment, getPayment };
477
+ }
478
+
355
479
  /**
356
480
  * Returns { t, locale }. `t(key, fallback)` resolves `{{t:key}}` against
357
481
  * the host's translation table and falls back to `fallback ?? key` when
package/dist/index.d.ts CHANGED
@@ -188,6 +188,10 @@ export interface WidgetContext<TProps = unknown> {
188
188
  listUsers(query?: DirectoryQuery): Promise<DirectoryUser[]>;
189
189
  };
190
190
  events: { emit(eventName: string, payload?: unknown): void };
191
+ payments: {
192
+ requestPayment(args: PaymentRequest): Promise<PaymentResult>;
193
+ getPayment(paymentId: string): Promise<PaymentResult>;
194
+ };
191
195
  i18n: {
192
196
  locale: string;
193
197
  t(key: string, vars?: Record<string, unknown>): string;
@@ -316,6 +320,47 @@ export function useDirectory(query?: DirectoryQuery): DirectoryResult;
316
320
 
317
321
  export function useWidgetEvent(name: string): (payload?: unknown) => void;
318
322
 
323
+ /**
324
+ * Arguments for `usePayments().requestPayment(...)`. `amountCents` is the
325
+ * charge in the currency's minor unit; the app user confirms it in hosted
326
+ * Checkout. `metadata` is an optional flat map carried through for the
327
+ * widget's own reconciliation (never used to derive the amount).
328
+ */
329
+ export interface PaymentRequest {
330
+ amountCents: number;
331
+ currency?: string;
332
+ description: string;
333
+ metadata?: Record<string, string>;
334
+ /** Site-relative path to return to after Checkout (e.g. "/cart"). */
335
+ returnPath?: string;
336
+ }
337
+
338
+ export interface PaymentResult {
339
+ id: string;
340
+ status: "PENDING" | "PAID" | "FAILED" | "REFUNDED" | "CANCELLED";
341
+ amountCents?: number;
342
+ currency?: string;
343
+ description?: string;
344
+ /**
345
+ * Present (Stripe provider) when the app user must complete a hosted
346
+ * Checkout: the widget should open this URL. Absent under the mock
347
+ * provider, where the charge auto-confirms (`status: "PAID"`).
348
+ */
349
+ checkoutUrl?: string | null;
350
+ }
351
+
352
+ export interface PaymentsApi {
353
+ requestPayment(args: PaymentRequest): Promise<PaymentResult>;
354
+ getPayment(paymentId: string): Promise<PaymentResult>;
355
+ }
356
+
357
+ /**
358
+ * Incoming app-user payments (REQ-BILL-07-WIDGETPAY). Requires the
359
+ * `payments.charge:appUser` scope in the widget manifest. The charge
360
+ * settles to the workspace owner; no card data touches the widget.
361
+ */
362
+ export function usePayments(): PaymentsApi;
363
+
319
364
  export function useTheme(): ThemeTokens;
320
365
 
321
366
  export function useI18n(): {
@@ -323,6 +368,19 @@ export function useI18n(): {
323
368
  t(key: string, fallback?: string): string;
324
369
  };
325
370
 
371
+ /**
372
+ * The active end-user identity. `id` is null for anonymous visitors and on
373
+ * the Studio canvas preview; every field is guaranteed present (the host
374
+ * fills safe defaults), so widgets read them without optional chaining.
375
+ */
376
+ export function useUser(): {
377
+ id: string | null;
378
+ email: string | null;
379
+ displayName: string | null;
380
+ roles: string[];
381
+ groupIds: string[];
382
+ };
383
+
326
384
  /**
327
385
  * Error class thrown by useDatastoreMutation callbacks (and surfaced by
328
386
  * useDatastoreQuery in its `error` slot). The `code` is a stable
package/dist/index.js CHANGED
@@ -8,12 +8,15 @@ export { validatePropertySchema, validateProps } from "./property-schema.js";
8
8
  export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
+ PaymentError,
11
12
  useDatastoreQuery,
12
13
  useDatastoreMutation,
13
14
  useDirectory,
14
15
  useWidgetEvent,
16
+ usePayments,
15
17
  useTheme,
16
18
  useI18n,
19
+ useUser,
17
20
  } from "./hooks.js";
18
21
  export {
19
22
  Text,
@@ -8,12 +8,15 @@ export { validatePropertySchema, validateProps } from "./property-schema.js";
8
8
  export {
9
9
  WidgetContextProvider,
10
10
  DatastoreError,
11
+ PaymentError,
11
12
  useDatastoreQuery,
12
13
  useDatastoreMutation,
13
14
  useDirectory,
14
15
  useWidgetEvent,
16
+ usePayments,
15
17
  useTheme,
16
18
  useI18n,
19
+ useUser,
17
20
  } from "./hooks.js";
18
21
  export {
19
22
  Text,
@@ -17,19 +17,41 @@
17
17
  // The static analyzer's allowedBareImports still rejects direct
18
18
  // `react-native` imports; the SDK is the single entry point.
19
19
 
20
- export {
21
- Text,
22
- View,
23
- Pressable,
24
- Image,
25
- ScrollView,
26
- TextInput,
27
- // Additional cross-platform primitives the AI agent + handwritten
28
- // widgets commonly reach for. All work in both react-native-web and
29
- // native react-native without per-platform code.
30
- FlatList,
31
- SectionList,
32
- ActivityIndicator,
33
- Switch,
34
- StyleSheet,
35
- } from "react-native";
20
+ // This is the WEB primitive surface (the native shell uses
21
+ // `primitives.native.js`, which Metro selects via the package.json
22
+ // `react-native` field). It imports `react-native-web` directly rather
23
+ // than the bare `react-native` specifier for two reasons:
24
+ //
25
+ // 1. `react-native` is declared an OPTIONAL peer dep of this package.
26
+ // When the host builds with Vite 8 / rolldown, rolldown intercepts
27
+ // the bare `react-native` import with a generated
28
+ // `__vite-optional-peer-dep` stub BEFORE the host's
29
+ // `react-native$ react-native-web` alias can apply, leaving some
30
+ // members (`Switch`, `StyleSheet`, …) `undefined` at runtime.
31
+ // 2. The direct `export { X } from "react-native"` re-export form
32
+ // additionally failed the production build outright
33
+ // (`[MISSING_EXPORT] "Pressable" is not exported`) because rolldown
34
+ // does not statically trace react-native-web's
35
+ // `export { default as X } from './exports/X'` chains.
36
+ //
37
+ // Importing `react-native-web` as a namespace and re-exporting each
38
+ // member as a local `const` resolves both: react-native-web is a real
39
+ // installed dep (no stub), and runtime property access sidesteps the
40
+ // static re-export tracing. Behaviour is identical to the old
41
+ // `react-native` alias path on the web.
42
+ import * as ReactNative from "react-native-web";
43
+
44
+ export const Text = ReactNative.Text;
45
+ export const View = ReactNative.View;
46
+ export const Pressable = ReactNative.Pressable;
47
+ export const Image = ReactNative.Image;
48
+ export const ScrollView = ReactNative.ScrollView;
49
+ export const TextInput = ReactNative.TextInput;
50
+ // Additional cross-platform primitives the AI agent + handwritten
51
+ // widgets commonly reach for. All work in both react-native-web and
52
+ // native react-native without per-platform code.
53
+ export const FlatList = ReactNative.FlatList;
54
+ export const SectionList = ReactNative.SectionList;
55
+ export const ActivityIndicator = ReactNative.ActivityIndicator;
56
+ export const Switch = ReactNative.Switch;
57
+ export const StyleSheet = ReactNative.StyleSheet;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.8.0",
3
+ "version": "0.10.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",