@colixsystems/widget-sdk 0.8.0 → 0.9.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,14 @@ 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.9.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.9.0
12
+
13
+ - **`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`.
14
+ - **`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.
15
+
16
+ ### What was in 0.8.0
12
17
 
13
18
  - **`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
19
  - **`CONTRACT.version` → `1.1.0`** (additive: one new hook, one new context slice, one new scope). No existing export changed signature.
@@ -66,7 +71,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
66
71
 
67
72
  - `defineWidget({ manifest, component })` — validates the manifest and produces a widget module the host can register.
68
73
  - `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.
74
+ - `useDatastoreQuery`, `useDatastoreMutation`, `useDirectory`, `useWidgetEvent`, `usePayments`, `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. `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`.
70
75
  - `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
76
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
72
77
 
package/dist/contract.cjs CHANGED
@@ -97,6 +97,18 @@ const HOOKS = [
97
97
  requiredContextSlice: ["events.emit"],
98
98
  scopes: null,
99
99
  },
100
+ {
101
+ name: "usePayments",
102
+ signature: "usePayments()",
103
+ returnShape: {
104
+ requestPayment:
105
+ "({ amountCents, currency?, description, metadata? }) => Promise<{ id, status, checkoutUrl? }> // rejects with PaymentError",
106
+ getPayment:
107
+ "(paymentId) => Promise<{ id, status, amountCents, currency, description }>",
108
+ },
109
+ requiredContextSlice: ["payments.requestPayment"],
110
+ scopes: ["payments.charge:appUser"],
111
+ },
100
112
  ];
101
113
 
102
114
  // REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
@@ -335,6 +347,12 @@ const WIDGET_CONTEXT_SHAPE = {
335
347
  required: true,
336
348
  fields: { emit: "function" },
337
349
  },
350
+ payments: {
351
+ description:
352
+ "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.",
353
+ required: true,
354
+ fields: { requestPayment: "function", getPayment: "function" },
355
+ },
338
356
  i18n: {
339
357
  description: "{ t(key, fallback?), locale }.",
340
358
  required: true,
@@ -427,7 +445,7 @@ function deepFreeze(value) {
427
445
  }
428
446
 
429
447
  const CONTRACT = deepFreeze({
430
- version: "1.1.0",
448
+ version: "1.2.0",
431
449
  hooks: HOOKS,
432
450
  primitives: PRIMITIVES,
433
451
  manifestSchema: MANIFEST_SCHEMA,
package/dist/contract.js CHANGED
@@ -98,6 +98,18 @@ const HOOKS = [
98
98
  requiredContextSlice: ["events.emit"],
99
99
  scopes: null,
100
100
  },
101
+ {
102
+ name: "usePayments",
103
+ signature: "usePayments()",
104
+ returnShape: {
105
+ requestPayment:
106
+ "({ amountCents, currency?, description, metadata? }) => Promise<{ id, status, checkoutUrl? }> // rejects with PaymentError",
107
+ getPayment:
108
+ "(paymentId) => Promise<{ id, status, amountCents, currency, description }>",
109
+ },
110
+ requiredContextSlice: ["payments.requestPayment"],
111
+ scopes: ["payments.charge:appUser"],
112
+ },
101
113
  ];
102
114
 
103
115
  // REQ-WSDK-RN-WEB: see contract.cjs for the source-of-truth comment.
@@ -329,6 +341,12 @@ const WIDGET_CONTEXT_SHAPE = {
329
341
  required: true,
330
342
  fields: { emit: "function" },
331
343
  },
344
+ payments: {
345
+ description:
346
+ "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.",
347
+ required: true,
348
+ fields: { requestPayment: "function", getPayment: "function" },
349
+ },
332
350
  i18n: {
333
351
  description: "{ t(key, fallback?), locale }.",
334
352
  required: true,
@@ -419,7 +437,7 @@ function deepFreeze(value) {
419
437
  }
420
438
 
421
439
  const CONTRACT = deepFreeze({
422
- version: "1.1.0",
440
+ version: "1.2.0",
423
441
  hooks: HOOKS,
424
442
  primitives: PRIMITIVES,
425
443
  manifestSchema: MANIFEST_SCHEMA,
package/dist/hooks.js CHANGED
@@ -352,6 +352,112 @@ export function useTheme() {
352
352
  return ctx.workspace.theme;
353
353
  }
354
354
 
355
+ /**
356
+ * Structured error thrown by `usePayments` callbacks. Carries a stable
357
+ * `code` so widgets can branch without parsing message strings.
358
+ *
359
+ * `code` is one of:
360
+ * - "AUTH_REQUIRED" — no signed-in app user
361
+ * - "PAYMENTS_SCOPE_NOT_GRANTED"— widget lacks payments.charge:appUser
362
+ * - "INVALID_AMOUNT" / "AMOUNT_TOO_LARGE" / "VALIDATION" — bad request
363
+ * - "CONNECT_NOT_READY" / "PAYMENTS_DISABLED" — provider not ready
364
+ * - "DECLINED" — the charge was declined
365
+ * - "INTERNAL" — anything else
366
+ */
367
+ export class PaymentError extends Error {
368
+ constructor(code, message, opts) {
369
+ super(message);
370
+ this.name = "PaymentError";
371
+ this.code = code;
372
+ if (opts && opts.cause) this.cause = opts.cause;
373
+ }
374
+ }
375
+
376
+ function toPaymentError(err) {
377
+ if (err instanceof PaymentError) return err;
378
+ const status =
379
+ err && err.response && typeof err.response.status === "number"
380
+ ? err.response.status
381
+ : null;
382
+ const bodyCode =
383
+ err && err.response && err.response.data && err.response.data.code;
384
+ const bodyMessage =
385
+ err && err.response && err.response.data && err.response.data.error;
386
+ let code = "INTERNAL";
387
+ if (typeof bodyCode === "string" && bodyCode) code = bodyCode;
388
+ else if (status === 401) code = "AUTH_REQUIRED";
389
+ else if (status === 402) code = "DECLINED";
390
+ else if (status === 403) code = "FORBIDDEN";
391
+ else if (status === 400) code = "VALIDATION";
392
+ const message =
393
+ (typeof bodyMessage === "string" && bodyMessage) ||
394
+ (err && typeof err.message === "string"
395
+ ? err.message
396
+ : "Payment request failed");
397
+ return new PaymentError(code, message, { cause: err });
398
+ }
399
+
400
+ /**
401
+ * Incoming app-user payments (REQ-BILL-07-WIDGETPAY). Returns
402
+ * `{ requestPayment, getPayment }`.
403
+ *
404
+ * requestPayment({ amountCents, currency?, description, metadata? })
405
+ * → Promise<{ id, status, checkoutUrl?, ... }>. The host either
406
+ * auto-confirms (mock provider, `status: "PAID"`, no redirect) or
407
+ * returns a hosted-Checkout `checkoutUrl` the widget should open
408
+ * (Stripe provider, `status: "PENDING"`). Rejects with a
409
+ * `PaymentError`.
410
+ * getPayment(paymentId) → Promise<payment> — poll the terminal status.
411
+ *
412
+ * Requires the `payments.charge:appUser` scope in the manifest's
413
+ * `requestedScopes`. The charge settles to the workspace owner; the app
414
+ * user confirms the amount in hosted Checkout. No card data touches the
415
+ * widget — never collect card fields yourself.
416
+ */
417
+ export function usePayments() {
418
+ const ctx = useWidgetContextOrThrow("usePayments");
419
+ if (!ctx.payments || typeof ctx.payments.requestPayment !== "function") {
420
+ throw new Error(
421
+ "usePayments: host did not inject a payments client. The widget must " +
422
+ "declare the payments.charge:appUser scope and be installed in a " +
423
+ "workspace whose host supports payments.",
424
+ );
425
+ }
426
+ const requestRef = useRef(ctx.payments.requestPayment);
427
+ const getRef = useRef(
428
+ typeof ctx.payments.getPayment === "function"
429
+ ? ctx.payments.getPayment
430
+ : null,
431
+ );
432
+ requestRef.current = ctx.payments.requestPayment;
433
+ getRef.current =
434
+ typeof ctx.payments.getPayment === "function"
435
+ ? ctx.payments.getPayment
436
+ : null;
437
+
438
+ const requestPayment = useCallback(async (args) => {
439
+ try {
440
+ return await requestRef.current(args);
441
+ } catch (err) {
442
+ throw toPaymentError(err);
443
+ }
444
+ }, []);
445
+ const getPayment = useCallback(async (paymentId) => {
446
+ if (!getRef.current) {
447
+ throw new PaymentError(
448
+ "INTERNAL",
449
+ "getPayment is not supported by this host.",
450
+ );
451
+ }
452
+ try {
453
+ return await getRef.current(paymentId);
454
+ } catch (err) {
455
+ throw toPaymentError(err);
456
+ }
457
+ }, []);
458
+ return { requestPayment, getPayment };
459
+ }
460
+
355
461
  /**
356
462
  * Returns { t, locale }. `t(key, fallback)` resolves `{{t:key}}` against
357
463
  * 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(): {
package/dist/index.js CHANGED
@@ -8,10 +8,12 @@ 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,
17
19
  } from "./hooks.js";
@@ -8,10 +8,12 @@ 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,
17
19
  } from "./hooks.js";
@@ -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.9.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",