@colixsystems/widget-sdk 0.277.4 → 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 +5 -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/purchases.js +143 -0
- package/package.json +1 -1
- package/src/contract.js +53 -2
package/README.md
CHANGED
|
@@ -87,7 +87,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
87
87
|
|
|
88
88
|
## Status
|
|
89
89
|
|
|
90
|
-
`v0.
|
|
90
|
+
`v0.278.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**.
|
|
91
|
+
|
|
92
|
+
### What's new in 0.278.0 (contract 1.227.0)
|
|
93
|
+
|
|
94
|
+
**New `usePurchases()` hook: sell digital goods in the native app through the App Store and Google Play.** It runs on the workspace's own RevenueCat project and returns `{ supported, loading, offering, entitlements, error, purchase, restore, refresh }`. `offering` is the current offering (`{ id, packages: [{ id, title, description, priceString, currencyCode }] }`), `entitlements` the signed-in user's active entitlement identifiers, and `purchase(packageId)` / `restore()` resolve the updated entitlements or reject with a `PurchasesError` (`UNSUPPORTED`, `SIGN_IN_REQUIRED`, `CANCELLED`, `NOT_FOUND`, `PURCHASE_FAILED`, `INTERNAL`). It is native-only: a browser has no store billing, so on the web Player `supported` is `false` and the widget must show a visible "available in the app" state. `react-native-purchases` is now a vetted native-only import; prefer the hook, and never configure or log in Purchases yourself.
|
|
91
95
|
|
|
92
96
|
### What's new in 0.277.4
|
|
93
97
|
|
package/dist/contract.cjs
CHANGED
|
@@ -3130,6 +3130,37 @@ const HOOKS = [
|
|
|
3130
3130
|
requiredContextSlice: [],
|
|
3131
3131
|
scopes: null,
|
|
3132
3132
|
},
|
|
3133
|
+
// sc-15163 — native-only in-app purchase. Optional slice; the hook reports
|
|
3134
|
+
// supported:false on the web Player rather than throwing at render.
|
|
3135
|
+
{
|
|
3136
|
+
name: "usePurchases",
|
|
3137
|
+
signature: "usePurchases()",
|
|
3138
|
+
description:
|
|
3139
|
+
"Sell digital goods (a subscription, premium content, credits) in the native app through the App Store and Google " +
|
|
3140
|
+
"Play, on the workspace's own RevenueCat project. Returns { supported, loading, offering, entitlements, error, " +
|
|
3141
|
+
"purchase, restore, refresh }. `offering` is the current offering set up in RevenueCat — { id, packages: [{ id, " +
|
|
3142
|
+
"title, description, priceString, currencyCode }] } or null; show priceString as-is, the store localises it. " +
|
|
3143
|
+
"`entitlements` lists the signed-in user's ACTIVE entitlement identifiers — unlock content by checking it. " +
|
|
3144
|
+
"purchase(packageId) opens the store's own sheet and resolves the updated entitlements; restore() re-reads purchases " +
|
|
3145
|
+
"made on another device (offer it: the stores require a way to restore). Both reject with a PurchasesError whose " +
|
|
3146
|
+
".code is one of UNSUPPORTED | SIGN_IN_REQUIRED | CANCELLED | NOT_FOUND | PURCHASE_FAILED | INTERNAL; CANCELLED means " +
|
|
3147
|
+
"the buyer closed the sheet, so do not show it as a failure. A purchase needs a signed-in app user. NATIVE-ONLY: a " +
|
|
3148
|
+
"browser has no App Store or Google Play billing, so the web Player (and the Studio canvas) report supported:false " +
|
|
3149
|
+
"— ALWAYS render a visible 'available in the app' state there, never a blank or a dead button. The Expo export also " +
|
|
3150
|
+
"reports supported:false until the workspace connects RevenueCat in Payments and rebuilds the app.",
|
|
3151
|
+
returnShape: {
|
|
3152
|
+
supported: "boolean // false on the web Player and in an app built without RevenueCat",
|
|
3153
|
+
loading: "boolean",
|
|
3154
|
+
offering: "{ id, packages: [{ id, title, description, priceString, currencyCode }] } | null",
|
|
3155
|
+
entitlements: "string[] // active entitlement identifiers",
|
|
3156
|
+
error: "PurchasesError | null",
|
|
3157
|
+
purchase: "(packageId: string) => Promise<string[]> // rejects with PurchasesError",
|
|
3158
|
+
restore: "() => Promise<string[]> // rejects with PurchasesError",
|
|
3159
|
+
refresh: "() => Promise<void>",
|
|
3160
|
+
},
|
|
3161
|
+
requiredContextSlice: [],
|
|
3162
|
+
scopes: null,
|
|
3163
|
+
},
|
|
3133
3164
|
{
|
|
3134
3165
|
name: "useAudioPlayer",
|
|
3135
3166
|
signature: "useAudioPlayer(uri)",
|
|
@@ -4432,7 +4463,11 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4432
4463
|
"audioRecorder: { isSupported() -> boolean, getPermission() -> Promise<'granted' | 'denied' | 'undetermined'>, " +
|
|
4433
4464
|
"start() -> Promise<{ getStatus() -> { durationMs, levelDb, ended }, stop() -> Promise<{ file, uri, durationMs, mime, release? }>, cancel() }> }, " +
|
|
4434
4465
|
"audioPlayer: { isSupported() -> boolean, load(uri, onStatus({ loaded, playing, positionMs, durationMs, error? })) -> " +
|
|
4435
|
-
"{ play() -> Promise<void>, pause(), seek(ms) -> Promise<void>, release() } }
|
|
4466
|
+
"{ play() -> Promise<void>, pause(), seek(ms) -> Promise<void>, release() } }, " +
|
|
4467
|
+
"purchases: { isSupported() -> boolean, getOffering() -> Promise<offering | null>, getEntitlements() -> Promise<string[]>, " +
|
|
4468
|
+
"purchase(packageId) -> Promise<string[]>, restore() -> Promise<string[]> } }. " +
|
|
4469
|
+
"purchases backs usePurchases() and is NATIVE-ONLY: the Expo export brokers it through react-native-purchases when the " +
|
|
4470
|
+
"workspace connected RevenueCat; the web Player never does, because a browser has no store billing. " +
|
|
4436
4471
|
"Backs useGeolocation(), useSpeechToText(), useCamera(), useImageEditor(), useBarcodeScanner(), usePdfExport().openPdf, useAudioRecorder() and useAudioPlayer(). The web Player brokers them via " +
|
|
4437
4472
|
"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 " +
|
|
4438
4473
|
"expo-location, expo-speech-recognition, expo-image-picker, expo-image-manipulator, expo-camera, expo-print and expo-audio. " +
|
|
@@ -4472,6 +4507,7 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
4472
4507
|
pdfViewer: "object",
|
|
4473
4508
|
audioRecorder: "object",
|
|
4474
4509
|
audioPlayer: "object",
|
|
4510
|
+
purchases: "object",
|
|
4475
4511
|
},
|
|
4476
4512
|
},
|
|
4477
4513
|
};
|
|
@@ -4702,6 +4738,18 @@ const VETTED_IMPORTS = [
|
|
|
4702
4738
|
description:
|
|
4703
4739
|
"Native clipboard. The SDK's useClipboard() hook already wraps this; reach for it directly only if you need RN-specific options.",
|
|
4704
4740
|
},
|
|
4741
|
+
// sc-15163 — native-only: a browser has no App Store / Google Play billing.
|
|
4742
|
+
{
|
|
4743
|
+
specifier: "react-native-purchases",
|
|
4744
|
+
platforms: ["native"],
|
|
4745
|
+
category: "payments",
|
|
4746
|
+
description:
|
|
4747
|
+
"RevenueCat in-app purchase (StoreKit / Play Billing). PREFER usePurchases() from the SDK, which already runs on the " +
|
|
4748
|
+
"app's configured, signed-in customer. Import this directly only for what the hook does not cover, and NEVER call " +
|
|
4749
|
+
"Purchases.configure, logIn or logOut — the app owns them, and it configures Purchases only when the workspace " +
|
|
4750
|
+
"connected RevenueCat. Native-only because no browser offers store billing: author it in widget.native.jsx and give " +
|
|
4751
|
+
"widget.web.jsx a visible 'available in the app' state.",
|
|
4752
|
+
},
|
|
4705
4753
|
{
|
|
4706
4754
|
specifier: "expo-haptics",
|
|
4707
4755
|
platforms: ["native"],
|
|
@@ -6577,7 +6625,10 @@ const CONTRACT = deepFreeze({
|
|
|
6577
6625
|
// 1.226.0: behaviour (sc-15080) — `<MarkdownInput>` is a rich-text editor
|
|
6578
6626
|
// showing the formatted result; `showPreview`, `previewLabel` and
|
|
6579
6627
|
// `renderImage` are accepted and ignored.
|
|
6580
|
-
|
|
6628
|
+
// 1.227.0: additive (sc-15163) — new native-only `usePurchases()` hook and
|
|
6629
|
+
// `PurchasesError`, a `device.purchases` slice, and the vetted
|
|
6630
|
+
// `react-native-purchases` import.
|
|
6631
|
+
version: "1.227.0",
|
|
6581
6632
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
6582
6633
|
hooks: HOOKS,
|
|
6583
6634
|
primitives: PRIMITIVES,
|
package/dist/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,
|
package/dist/index.d.ts
CHANGED
|
@@ -946,6 +946,14 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
946
946
|
release(): void;
|
|
947
947
|
};
|
|
948
948
|
};
|
|
949
|
+
/** sc-15163 — backs usePurchases. Brokered by the Expo export only. */
|
|
950
|
+
purchases?: {
|
|
951
|
+
isSupported?(): boolean;
|
|
952
|
+
getOffering(): Promise<PurchaseOffering | null>;
|
|
953
|
+
getEntitlements(): Promise<string[]>;
|
|
954
|
+
purchase(packageId: string): Promise<string[]>;
|
|
955
|
+
restore(): Promise<string[]>;
|
|
956
|
+
};
|
|
949
957
|
speech?: {
|
|
950
958
|
isSupported?(): boolean;
|
|
951
959
|
start(
|
|
@@ -2895,6 +2903,54 @@ export interface AudioPlayerResult {
|
|
|
2895
2903
|
*/
|
|
2896
2904
|
export function useAudioPlayer(uri: string | null | undefined): AudioPlayerResult;
|
|
2897
2905
|
|
|
2906
|
+
/** One package of the current offering, as the store prices it. */
|
|
2907
|
+
export interface PurchasePackage {
|
|
2908
|
+
id: string;
|
|
2909
|
+
title: string;
|
|
2910
|
+
description: string;
|
|
2911
|
+
priceString: string;
|
|
2912
|
+
currencyCode: string;
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
/** The current offering the workspace set up in RevenueCat. */
|
|
2916
|
+
export interface PurchaseOffering {
|
|
2917
|
+
id: string;
|
|
2918
|
+
packages: PurchasePackage[];
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/** What `usePurchases()` returns. */
|
|
2922
|
+
export interface PurchasesResult {
|
|
2923
|
+
/** False on the web Player: store billing needs the native app. */
|
|
2924
|
+
supported: boolean;
|
|
2925
|
+
loading: boolean;
|
|
2926
|
+
offering: PurchaseOffering | null;
|
|
2927
|
+
/** The signed-in user's active entitlement identifiers. */
|
|
2928
|
+
entitlements: string[];
|
|
2929
|
+
error: PurchasesError | null;
|
|
2930
|
+
purchase(packageId: string): Promise<string[]>;
|
|
2931
|
+
restore(): Promise<string[]>;
|
|
2932
|
+
refresh(): Promise<void>;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* Sell digital goods through the App Store and Google Play, on the
|
|
2937
|
+
* workspace's own RevenueCat project. Native-only: on the web Player
|
|
2938
|
+
* `supported` is false — render a visible "not available on the web" state.
|
|
2939
|
+
*/
|
|
2940
|
+
export function usePurchases(): PurchasesResult;
|
|
2941
|
+
|
|
2942
|
+
/** Error surfaced by `usePurchases()`. */
|
|
2943
|
+
export class PurchasesError extends Error {
|
|
2944
|
+
code:
|
|
2945
|
+
| "UNSUPPORTED"
|
|
2946
|
+
| "SIGN_IN_REQUIRED"
|
|
2947
|
+
| "CANCELLED"
|
|
2948
|
+
| "NOT_FOUND"
|
|
2949
|
+
| "PURCHASE_FAILED"
|
|
2950
|
+
| "INTERNAL";
|
|
2951
|
+
constructor(code: PurchasesError["code"], message: string, opts?: { cause?: unknown });
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2898
2954
|
/** Error surfaced by `useAudioPlayer()`. */
|
|
2899
2955
|
export class AudioPlayerError extends Error {
|
|
2900
2956
|
code: "UNSUPPORTED" | "LOAD_FAILED" | "BLOCKED" | "INTERNAL";
|
package/dist/index.js
CHANGED
|
@@ -132,6 +132,8 @@ export {
|
|
|
132
132
|
useAudioPlayer,
|
|
133
133
|
AudioPlayerError,
|
|
134
134
|
} from "./audio.js";
|
|
135
|
+
// sc-15163 — native-only in-app purchase; the web Player brokers none.
|
|
136
|
+
export { usePurchases, PurchasesError } from "./purchases.js";
|
|
135
137
|
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
136
138
|
export {
|
|
137
139
|
useDatastoreAggregate,
|
package/dist/index.native.js
CHANGED
|
@@ -130,6 +130,8 @@ export {
|
|
|
130
130
|
useAudioPlayer,
|
|
131
131
|
AudioPlayerError,
|
|
132
132
|
} from "./audio.js";
|
|
133
|
+
// sc-15163 — native-only in-app purchase; the web Player brokers none.
|
|
134
|
+
export { usePurchases, PurchasesError } from "./purchases.js";
|
|
133
135
|
export { useDatastoreFigure } from "./datastore-figure.js";
|
|
134
136
|
export {
|
|
135
137
|
useDatastoreAggregate,
|
|
@@ -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",
|
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,
|