@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.
- package/README.md +9 -1
- package/dist/contract.cjs +53 -2
- package/dist/contract.js +53 -2
- package/dist/index.d.ts +56 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/markdown-editor-bundle.js +158 -0
- package/dist/markdown-editor-field.js +6 -4
- package/dist/purchases.js +143 -0
- package/package.json +1 -57
- package/src/contract.js +53 -2
|
@@ -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
|
-
//
|
|
5
|
-
//
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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,
|