@colixsystems/widget-sdk 0.277.3 → 0.278.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.
@@ -1,8 +1,10 @@
1
1
  // sc-15080 — `<MarkdownInput>`'s editing surface (web).
2
2
  //
3
- // Mounts the shared editor (./markdown-editor-core.js) straight into the page.
4
- // The editor is loaded on first use, so a widget without a `<MarkdownInput>`
5
- // never downloads it. The native mirror is ./markdown-editor-field.native.js.
3
+ // Mounts the shared editor (./markdown-editor-core.js) straight into the page,
4
+ // from its build with Tiptap inlined, so no host has to install Tiptap
5
+ // (sc-15310). The editor is loaded on first use, so a widget without a
6
+ // `<MarkdownInput>` never downloads it. The native mirror is
7
+ // ./markdown-editor-field.native.js.
6
8
 
7
9
  import React from "react";
8
10
  import { markdownEditorCss } from "./markdown-editor-css.js";
@@ -32,7 +34,7 @@ export const MarkdownEditorField = React.forwardRef(function MarkdownEditorField
32
34
 
33
35
  React.useEffect(() => {
34
36
  let cancelled = false;
35
- import("./markdown-editor-core.js")
37
+ import("./markdown-editor-bundle.js")
36
38
  .then(({ createMarkdownEditor }) => {
37
39
  if (cancelled || !host.current) return;
38
40
  editor.current = createMarkdownEditor({
@@ -0,0 +1,143 @@
1
+ // Host-brokered in-app purchase: `usePurchases()`.
2
+ //
3
+ // Native-only. The Expo export brokers ctx.device.purchases through
4
+ // react-native-purchases on the workspace's own RevenueCat project, which
5
+ // sells through Apple's StoreKit and Google Play Billing. A browser has no
6
+ // store billing, so the web Player brokers nothing and the hook reports
7
+ // `supported: false`; a widget renders its "not available on the web" state.
8
+ //
9
+ // This file avoids JSX so it can ship as a plain .js without a transform.
10
+
11
+ import { useCallback, useEffect, useRef, useState } from "react";
12
+ import { useWidgetContextOrThrow } from "./hooks.js";
13
+
14
+ /**
15
+ * Error surfaced by `usePurchases()` (thrown by `purchase()` / `restore()`
16
+ * and stored in the hook's `error` slot).
17
+ *
18
+ * `code` is one of:
19
+ * - "UNSUPPORTED" — this host sells nothing in-app (the web Player).
20
+ * - "SIGN_IN_REQUIRED" — no app user is signed in to buy for.
21
+ * - "CANCELLED" — the buyer closed the store sheet.
22
+ * - "NOT_FOUND" — no package with that id is for sale.
23
+ * - "PURCHASE_FAILED" — the store refused or failed the purchase.
24
+ * - "INTERNAL" — anything else.
25
+ */
26
+ export class PurchasesError extends Error {
27
+ constructor(code, message, opts) {
28
+ super(message);
29
+ this.name = "PurchasesError";
30
+ this.code = code;
31
+ if (opts && opts.cause) this.cause = opts.cause;
32
+ }
33
+ }
34
+
35
+ const KNOWN_CODES = new Set([
36
+ "UNSUPPORTED",
37
+ "SIGN_IN_REQUIRED",
38
+ "CANCELLED",
39
+ "NOT_FOUND",
40
+ "PURCHASE_FAILED",
41
+ ]);
42
+
43
+ function toPurchasesError(err) {
44
+ if (err instanceof PurchasesError) return err;
45
+ const code = KNOWN_CODES.has(err && err.code) ? err.code : "INTERNAL";
46
+ const message = (err && typeof err.message === "string" && err.message) || "Purchase failed";
47
+ return new PurchasesError(code, message, { cause: err });
48
+ }
49
+
50
+ function isUsable(client) {
51
+ return Boolean(
52
+ client &&
53
+ typeof client.purchase === "function" &&
54
+ (typeof client.isSupported !== "function" || client.isSupported()),
55
+ );
56
+ }
57
+
58
+ const NO_ENTITLEMENTS = Object.freeze([]);
59
+
60
+ /**
61
+ * Sell digital goods in the app through the App Store and Google Play.
62
+ * Returns `{ supported, loading, offering, entitlements, error, purchase,
63
+ * restore, refresh }`.
64
+ *
65
+ * `offering` is the current offering the workspace set up in RevenueCat —
66
+ * `{ id, packages: [{ id, title, description, priceString, currencyCode }] }`
67
+ * or null. `entitlements` lists the signed-in user's active entitlement
68
+ * identifiers. `purchase(packageId)` and `restore()` resolve the updated
69
+ * entitlements and reject with a `PurchasesError`; a cancelled purchase has
70
+ * code "CANCELLED" and is usually not worth showing as an error.
71
+ *
72
+ * Native-only: on the web Player `supported` is false. ALWAYS render a visible
73
+ * "not available on the web" state then, never a blank or a dead button.
74
+ */
75
+ export function usePurchases() {
76
+ const ctx = useWidgetContextOrThrow("usePurchases");
77
+ const client = ctx.device && ctx.device.purchases;
78
+ const clientRef = useRef(client);
79
+ clientRef.current = client;
80
+ const supported = isUsable(client);
81
+
82
+ const [loading, setLoading] = useState(supported);
83
+ const [offering, setOffering] = useState(null);
84
+ const [entitlements, setEntitlements] = useState(NO_ENTITLEMENTS);
85
+ const [error, setError] = useState(null);
86
+ const mountedRef = useRef(true);
87
+ useEffect(() => {
88
+ mountedRef.current = true;
89
+ return () => {
90
+ mountedRef.current = false;
91
+ };
92
+ }, []);
93
+
94
+ const refresh = useCallback(async () => {
95
+ const live = clientRef.current;
96
+ if (!isUsable(live)) return;
97
+ setLoading(true);
98
+ try {
99
+ const [nextOffering, nextEntitlements] = await Promise.all([
100
+ live.getOffering(),
101
+ live.getEntitlements(),
102
+ ]);
103
+ if (!mountedRef.current) return;
104
+ setOffering(nextOffering || null);
105
+ setEntitlements(nextEntitlements || NO_ENTITLEMENTS);
106
+ setError(null);
107
+ } catch (err) {
108
+ if (mountedRef.current) setError(toPurchasesError(err));
109
+ } finally {
110
+ if (mountedRef.current) setLoading(false);
111
+ }
112
+ }, []);
113
+
114
+ useEffect(() => {
115
+ if (supported) refresh();
116
+ }, [supported, refresh]);
117
+
118
+ const run = useCallback(async (method, arg) => {
119
+ const live = clientRef.current;
120
+ if (!isUsable(live)) {
121
+ const unsupported = new PurchasesError("UNSUPPORTED", "In-app purchase is not available here.");
122
+ setError(unsupported);
123
+ throw unsupported;
124
+ }
125
+ try {
126
+ const next = await live[method](arg);
127
+ if (mountedRef.current) {
128
+ setEntitlements(next || NO_ENTITLEMENTS);
129
+ setError(null);
130
+ }
131
+ return next || NO_ENTITLEMENTS;
132
+ } catch (err) {
133
+ const wrapped = toPurchasesError(err);
134
+ if (mountedRef.current) setError(wrapped);
135
+ throw wrapped;
136
+ }
137
+ }, []);
138
+
139
+ const purchase = useCallback((packageId) => run("purchase", packageId), [run]);
140
+ const restore = useCallback(() => run("restore"), [run]);
141
+
142
+ return { supported, loading: supported && loading, offering, entitlements, error, purchase, restore, refresh };
143
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.277.3",
3
+ "version": "0.278.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",
@@ -76,20 +76,6 @@
76
76
  "react-native": "*",
77
77
  "react-dom": ">=18.0.0",
78
78
  "react-native-web": ">=0.19.0",
79
- "@tiptap/core": "^3.32.2",
80
- "@tiptap/extension-bold": "^3.32.2",
81
- "@tiptap/extension-code": "^3.32.2",
82
- "@tiptap/extension-code-block": "^3.32.2",
83
- "@tiptap/extension-document": "^3.32.2",
84
- "@tiptap/extension-heading": "^3.32.2",
85
- "@tiptap/extension-image": "^3.32.2",
86
- "@tiptap/extension-italic": "^3.32.2",
87
- "@tiptap/extension-link": "^3.32.2",
88
- "@tiptap/extension-list": "^3.32.2",
89
- "@tiptap/extension-paragraph": "^3.32.2",
90
- "@tiptap/extension-table": "^3.32.2",
91
- "@tiptap/extension-text": "^3.32.2",
92
- "@tiptap/extensions": "^3.32.2",
93
79
  "react-native-webview": ">=13.7.0"
94
80
  },
95
81
  "peerDependenciesMeta": {
@@ -105,48 +91,6 @@
105
91
  "react-dom": {
106
92
  "optional": true
107
93
  },
108
- "@tiptap/core": {
109
- "optional": true
110
- },
111
- "@tiptap/extension-bold": {
112
- "optional": true
113
- },
114
- "@tiptap/extension-code": {
115
- "optional": true
116
- },
117
- "@tiptap/extension-code-block": {
118
- "optional": true
119
- },
120
- "@tiptap/extension-document": {
121
- "optional": true
122
- },
123
- "@tiptap/extension-heading": {
124
- "optional": true
125
- },
126
- "@tiptap/extension-image": {
127
- "optional": true
128
- },
129
- "@tiptap/extension-italic": {
130
- "optional": true
131
- },
132
- "@tiptap/extension-link": {
133
- "optional": true
134
- },
135
- "@tiptap/extension-list": {
136
- "optional": true
137
- },
138
- "@tiptap/extension-paragraph": {
139
- "optional": true
140
- },
141
- "@tiptap/extension-table": {
142
- "optional": true
143
- },
144
- "@tiptap/extension-text": {
145
- "optional": true
146
- },
147
- "@tiptap/extensions": {
148
- "optional": true
149
- },
150
94
  "react-native-webview": {
151
95
  "optional": true
152
96
  }
package/src/contract.js CHANGED
@@ -3126,6 +3126,37 @@ const HOOKS = [
3126
3126
  requiredContextSlice: [],
3127
3127
  scopes: null,
3128
3128
  },
3129
+ // sc-15163 — native-only in-app purchase. Optional slice; the hook reports
3130
+ // supported:false on the web Player rather than throwing at render.
3131
+ {
3132
+ name: "usePurchases",
3133
+ signature: "usePurchases()",
3134
+ description:
3135
+ "Sell digital goods (a subscription, premium content, credits) in the native app through the App Store and Google " +
3136
+ "Play, on the workspace's own RevenueCat project. Returns { supported, loading, offering, entitlements, error, " +
3137
+ "purchase, restore, refresh }. `offering` is the current offering set up in RevenueCat — { id, packages: [{ id, " +
3138
+ "title, description, priceString, currencyCode }] } or null; show priceString as-is, the store localises it. " +
3139
+ "`entitlements` lists the signed-in user's ACTIVE entitlement identifiers — unlock content by checking it. " +
3140
+ "purchase(packageId) opens the store's own sheet and resolves the updated entitlements; restore() re-reads purchases " +
3141
+ "made on another device (offer it: the stores require a way to restore). Both reject with a PurchasesError whose " +
3142
+ ".code is one of UNSUPPORTED | SIGN_IN_REQUIRED | CANCELLED | NOT_FOUND | PURCHASE_FAILED | INTERNAL; CANCELLED means " +
3143
+ "the buyer closed the sheet, so do not show it as a failure. A purchase needs a signed-in app user. NATIVE-ONLY: a " +
3144
+ "browser has no App Store or Google Play billing, so the web Player (and the Studio canvas) report supported:false " +
3145
+ "— ALWAYS render a visible 'available in the app' state there, never a blank or a dead button. The Expo export also " +
3146
+ "reports supported:false until the workspace connects RevenueCat in Payments and rebuilds the app.",
3147
+ returnShape: {
3148
+ supported: "boolean // false on the web Player and in an app built without RevenueCat",
3149
+ loading: "boolean",
3150
+ offering: "{ id, packages: [{ id, title, description, priceString, currencyCode }] } | null",
3151
+ entitlements: "string[] // active entitlement identifiers",
3152
+ error: "PurchasesError | null",
3153
+ purchase: "(packageId: string) => Promise<string[]> // rejects with PurchasesError",
3154
+ restore: "() => Promise<string[]> // rejects with PurchasesError",
3155
+ refresh: "() => Promise<void>",
3156
+ },
3157
+ requiredContextSlice: [],
3158
+ scopes: null,
3159
+ },
3129
3160
  {
3130
3161
  name: "useAudioPlayer",
3131
3162
  signature: "useAudioPlayer(uri)",
@@ -4428,7 +4459,11 @@ const WIDGET_CONTEXT_SHAPE = {
4428
4459
  "audioRecorder: { isSupported() -> boolean, getPermission() -> Promise<'granted' | 'denied' | 'undetermined'>, " +
4429
4460
  "start() -> Promise<{ getStatus() -> { durationMs, levelDb, ended }, stop() -> Promise<{ file, uri, durationMs, mime, release? }>, cancel() }> }, " +
4430
4461
  "audioPlayer: { isSupported() -> boolean, load(uri, onStatus({ loaded, playing, positionMs, durationMs, error? })) -> " +
4431
- "{ play() -> Promise<void>, pause(), seek(ms) -> Promise<void>, release() } } }. " +
4462
+ "{ play() -> Promise<void>, pause(), seek(ms) -> Promise<void>, release() } }, " +
4463
+ "purchases: { isSupported() -> boolean, getOffering() -> Promise<offering | null>, getEntitlements() -> Promise<string[]>, " +
4464
+ "purchase(packageId) -> Promise<string[]>, restore() -> Promise<string[]> } }. " +
4465
+ "purchases backs usePurchases() and is NATIVE-ONLY: the Expo export brokers it through react-native-purchases when the " +
4466
+ "workspace connected RevenueCat; the web Player never does, because a browser has no store billing. " +
4432
4467
  "Backs useGeolocation(), useSpeechToText(), useCamera(), useImageEditor(), useBarcodeScanner(), usePdfExport().openPdf, useAudioRecorder() and useAudioPlayer(). The web Player brokers them via " +
4433
4468
  "navigator.geolocation, window.SpeechRecognition, a getUserMedia camera preview, a host-side canvas, BarcodeDetector, a new browser tab, MediaRecorder and an <audio> element; the Expo export via " +
4434
4469
  "expo-location, expo-speech-recognition, expo-image-picker, expo-image-manipulator, expo-camera, expo-print and expo-audio. " +
@@ -4468,6 +4503,7 @@ const WIDGET_CONTEXT_SHAPE = {
4468
4503
  pdfViewer: "object",
4469
4504
  audioRecorder: "object",
4470
4505
  audioPlayer: "object",
4506
+ purchases: "object",
4471
4507
  },
4472
4508
  },
4473
4509
  };
@@ -4698,6 +4734,18 @@ const VETTED_IMPORTS = [
4698
4734
  description:
4699
4735
  "Native clipboard. The SDK's useClipboard() hook already wraps this; reach for it directly only if you need RN-specific options.",
4700
4736
  },
4737
+ // sc-15163 — native-only: a browser has no App Store / Google Play billing.
4738
+ {
4739
+ specifier: "react-native-purchases",
4740
+ platforms: ["native"],
4741
+ category: "payments",
4742
+ description:
4743
+ "RevenueCat in-app purchase (StoreKit / Play Billing). PREFER usePurchases() from the SDK, which already runs on the " +
4744
+ "app's configured, signed-in customer. Import this directly only for what the hook does not cover, and NEVER call " +
4745
+ "Purchases.configure, logIn or logOut — the app owns them, and it configures Purchases only when the workspace " +
4746
+ "connected RevenueCat. Native-only because no browser offers store billing: author it in widget.native.jsx and give " +
4747
+ "widget.web.jsx a visible 'available in the app' state.",
4748
+ },
4701
4749
  {
4702
4750
  specifier: "expo-haptics",
4703
4751
  platforms: ["native"],
@@ -6573,7 +6621,10 @@ const CONTRACT = deepFreeze({
6573
6621
  // 1.226.0: behaviour (sc-15080) — `<MarkdownInput>` is a rich-text editor
6574
6622
  // showing the formatted result; `showPreview`, `previewLabel` and
6575
6623
  // `renderImage` are accepted and ignored.
6576
- version: "1.226.0",
6624
+ // 1.227.0: additive (sc-15163) — new native-only `usePurchases()` hook and
6625
+ // `PurchasesError`, a `device.purchases` slice, and the vetted
6626
+ // `react-native-purchases` import.
6627
+ version: "1.227.0",
6577
6628
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6578
6629
  hooks: HOOKS,
6579
6630
  primitives: PRIMITIVES,