@crediball/react 0.3.2 → 0.3.6
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/dist/CrediballProvider.d.ts +7 -9
- package/dist/CrediballProvider.js +7 -2
- package/dist/creditEvent.d.ts +21 -0
- package/dist/creditEvent.js +31 -0
- package/dist/fetchWatcher.d.ts +6 -1
- package/dist/fetchWatcher.js +63 -6
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/usePaywall.d.ts +39 -23
- package/dist/usePaywall.js +36 -19
- package/package.json +1 -1
|
@@ -12,14 +12,12 @@ export interface CrediballProviderProps {
|
|
|
12
12
|
*/
|
|
13
13
|
onTopup?: (amount: number) => void | Promise<void>;
|
|
14
14
|
/**
|
|
15
|
-
* Auto-detect insufficient-credit responses (
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the paywall manually via useCrediball().showPaywall().
|
|
15
|
+
* Auto-detect insufficient-credit responses from any fetch() call and open the
|
|
16
|
+
* paywall automatically — no per-call wiring. Defaults to true. Set false to only
|
|
17
|
+
* open the paywall manually via useCrediball().showPaywall().
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* showPaywall() manually from your SSE error handler in those cases.
|
|
19
|
+
* Handles both HTTP 402 (regular JSON routes) and HTTP 200 text/event-stream /
|
|
20
|
+
* ndjson (streaming/SSE routes where the error arrives as a stream chunk).
|
|
23
21
|
*/
|
|
24
22
|
watchFetch?: boolean;
|
|
25
23
|
/** Allow a free-form amount entry in the paywall. */
|
|
@@ -56,8 +54,8 @@ interface CrediballContextValue {
|
|
|
56
54
|
* Crediball's error), the paywall appears automatically — no extra code per call.
|
|
57
55
|
* You can also open it yourself with useCrediball().showPaywall().
|
|
58
56
|
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
58
|
+
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
61
59
|
*/
|
|
62
60
|
export declare function CrediballProvider({ children, amounts, onTopup, watchFetch, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, }: CrediballProviderProps): import("react").JSX.Element;
|
|
63
61
|
/**
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
4
|
import { PaywallModal } from "./PaywallModal";
|
|
5
5
|
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
6
|
+
import { subscribeInsufficientCredits } from "./creditEvent";
|
|
6
7
|
const CrediballContext = createContext(null);
|
|
7
8
|
/**
|
|
8
9
|
* Pattern B, automated. Wrap your app once:
|
|
@@ -15,8 +16,8 @@ const CrediballContext = createContext(null);
|
|
|
15
16
|
* Crediball's error), the paywall appears automatically — no extra code per call.
|
|
16
17
|
* You can also open it yourself with useCrediball().showPaywall().
|
|
17
18
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* SSE/streaming routes (text/event-stream, ndjson) are also handled automatically —
|
|
20
|
+
* the watcher scans each chunk for credit errors, so no manual wiring is needed.
|
|
20
21
|
*/
|
|
21
22
|
export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, watchFetch = true, allowCustom = false, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
|
|
22
23
|
const [open, setOpen] = useState(false);
|
|
@@ -25,6 +26,10 @@ export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, wa
|
|
|
25
26
|
// Keep a ref so the fetch-watcher closure always calls the latest version.
|
|
26
27
|
const showPaywallRef = useRef(showPaywall);
|
|
27
28
|
showPaywallRef.current = showPaywall;
|
|
29
|
+
// Listen for the global credit-error event (fired by notifyInsufficientCredits()
|
|
30
|
+
// or the fetch watcher). Always active — works regardless of how the request
|
|
31
|
+
// was made (fetch, XHR, AI SDK internals, etc.).
|
|
32
|
+
useEffect(() => subscribeInsufficientCredits(() => showPaywallRef.current()), []);
|
|
28
33
|
useEffect(() => {
|
|
29
34
|
if (!watchFetch)
|
|
30
35
|
return;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global custom-event bus for insufficient-credits signals.
|
|
3
|
+
*
|
|
4
|
+
* Any code — a useChat onError, an XHR error handler, a Redux middleware, a
|
|
5
|
+
* server action catch block — can call notifyInsufficientCredits() and every
|
|
6
|
+
* mounted CrediballProvider / usePaywall instance will open the paywall.
|
|
7
|
+
*
|
|
8
|
+
* This decouples the signal source from the UI, so the paywall works regardless
|
|
9
|
+
* of how the HTTP request was made (fetch, XHR, EventSource, AI SDK internals…).
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Fire the global insufficient-credits signal. Safe to call from anywhere in
|
|
13
|
+
* the app — server components excluded (DOM only). Every mounted paywall
|
|
14
|
+
* component will react to it automatically.
|
|
15
|
+
*/
|
|
16
|
+
export declare function notifyInsufficientCredits(): void;
|
|
17
|
+
/**
|
|
18
|
+
* Subscribe to the global insufficient-credits signal.
|
|
19
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
20
|
+
*/
|
|
21
|
+
export declare function subscribeInsufficientCredits(listener: () => void): () => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global custom-event bus for insufficient-credits signals.
|
|
3
|
+
*
|
|
4
|
+
* Any code — a useChat onError, an XHR error handler, a Redux middleware, a
|
|
5
|
+
* server action catch block — can call notifyInsufficientCredits() and every
|
|
6
|
+
* mounted CrediballProvider / usePaywall instance will open the paywall.
|
|
7
|
+
*
|
|
8
|
+
* This decouples the signal source from the UI, so the paywall works regardless
|
|
9
|
+
* of how the HTTP request was made (fetch, XHR, EventSource, AI SDK internals…).
|
|
10
|
+
*/
|
|
11
|
+
const EVENT = "crediball:insufficient-credits";
|
|
12
|
+
/**
|
|
13
|
+
* Fire the global insufficient-credits signal. Safe to call from anywhere in
|
|
14
|
+
* the app — server components excluded (DOM only). Every mounted paywall
|
|
15
|
+
* component will react to it automatically.
|
|
16
|
+
*/
|
|
17
|
+
export function notifyInsufficientCredits() {
|
|
18
|
+
if (typeof window === "undefined")
|
|
19
|
+
return;
|
|
20
|
+
window.dispatchEvent(new CustomEvent(EVENT));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Subscribe to the global insufficient-credits signal.
|
|
24
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
25
|
+
*/
|
|
26
|
+
export function subscribeInsufficientCredits(listener) {
|
|
27
|
+
if (typeof window === "undefined")
|
|
28
|
+
return () => { };
|
|
29
|
+
window.addEventListener(EVENT, listener);
|
|
30
|
+
return () => window.removeEventListener(EVENT, listener);
|
|
31
|
+
}
|
package/dist/fetchWatcher.d.ts
CHANGED
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
* registered listeners (from CrediballProvider and/or usePaywall). Only one
|
|
4
4
|
* fetch patch exists at a time regardless of how many components subscribe.
|
|
5
5
|
* Reference-counted: the patch is removed when the last listener unsubscribes.
|
|
6
|
+
*
|
|
7
|
+
* Handles two credit-error surfaces:
|
|
8
|
+
* 1. HTTP 402 — regular JSON responses (non-streaming routes).
|
|
9
|
+
* 2. HTTP 200 text/event-stream or ndjson — streaming routes where the error
|
|
10
|
+
* arrives as a chunk in the stream body rather than an HTTP status code.
|
|
6
11
|
*/
|
|
7
12
|
type Listener = () => void;
|
|
8
13
|
/**
|
|
9
|
-
* Subscribe to insufficient-credits
|
|
14
|
+
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
10
15
|
* Returns an unsubscribe function — call it in your useEffect cleanup.
|
|
11
16
|
*/
|
|
12
17
|
export declare function subscribeFetchWatcher(listener: Listener): () => void;
|
package/dist/fetchWatcher.js
CHANGED
|
@@ -3,9 +3,57 @@
|
|
|
3
3
|
* registered listeners (from CrediballProvider and/or usePaywall). Only one
|
|
4
4
|
* fetch patch exists at a time regardless of how many components subscribe.
|
|
5
5
|
* Reference-counted: the patch is removed when the last listener unsubscribes.
|
|
6
|
+
*
|
|
7
|
+
* Handles two credit-error surfaces:
|
|
8
|
+
* 1. HTTP 402 — regular JSON responses (non-streaming routes).
|
|
9
|
+
* 2. HTTP 200 text/event-stream or ndjson — streaming routes where the error
|
|
10
|
+
* arrives as a chunk in the stream body rather than an HTTP status code.
|
|
6
11
|
*/
|
|
12
|
+
import { notifyInsufficientCredits } from "./creditEvent";
|
|
7
13
|
let _original = null;
|
|
8
14
|
const _listeners = new Set();
|
|
15
|
+
function fire() {
|
|
16
|
+
// Dispatch the global event so ALL mounted paywall instances react,
|
|
17
|
+
// plus any manual subscribeInsufficientCredits() listeners.
|
|
18
|
+
notifyInsufficientCredits();
|
|
19
|
+
// Also call per-instance listeners registered via subscribeFetchWatcher
|
|
20
|
+
// (kept for backwards compatibility with direct watcher subscribers).
|
|
21
|
+
_listeners.forEach((fn) => fn());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Tee the streaming response body and scan each chunk for the
|
|
25
|
+
* `insufficient_credits` signal. Returns a new Response whose body is the
|
|
26
|
+
* pass-through branch so the calling code receives the full, unmodified stream.
|
|
27
|
+
*/
|
|
28
|
+
function watchStream(res) {
|
|
29
|
+
if (!res.body)
|
|
30
|
+
return res;
|
|
31
|
+
const [scanBranch, passBranch] = res.body.tee();
|
|
32
|
+
(async () => {
|
|
33
|
+
const reader = scanBranch.getReader();
|
|
34
|
+
const decoder = new TextDecoder();
|
|
35
|
+
try {
|
|
36
|
+
for (;;) {
|
|
37
|
+
const { done, value } = await reader.read();
|
|
38
|
+
if (done)
|
|
39
|
+
break;
|
|
40
|
+
if (decoder.decode(value, { stream: true }).includes("insufficient_credits")) {
|
|
41
|
+
fire();
|
|
42
|
+
reader.cancel().catch(() => { });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// stream error — ignore
|
|
49
|
+
}
|
|
50
|
+
})();
|
|
51
|
+
return new Response(passBranch, {
|
|
52
|
+
status: res.status,
|
|
53
|
+
statusText: res.statusText,
|
|
54
|
+
headers: res.headers,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
9
57
|
function install() {
|
|
10
58
|
if (typeof window === "undefined" || _original !== null)
|
|
11
59
|
return;
|
|
@@ -20,15 +68,24 @@ function install() {
|
|
|
20
68
|
if (body != null &&
|
|
21
69
|
typeof body === "object" &&
|
|
22
70
|
body.code === "insufficient_credits") {
|
|
23
|
-
|
|
71
|
+
fire();
|
|
24
72
|
}
|
|
25
73
|
})
|
|
26
74
|
.catch(() => {
|
|
27
|
-
// Non-JSON 402
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
75
|
+
// Non-JSON 402 — fire anyway; a 402 from the app's own API route is
|
|
76
|
+
// almost certainly a credit shortage. Set watchFetch=false and call
|
|
77
|
+
// trigger() manually if your app uses 402 for other purposes.
|
|
78
|
+
fire();
|
|
31
79
|
});
|
|
80
|
+
return res;
|
|
81
|
+
}
|
|
82
|
+
// Watch streaming responses for embedded credit errors (HTTP 200 with
|
|
83
|
+
// text/event-stream or ndjson — the common AI streaming pattern).
|
|
84
|
+
if (res.body) {
|
|
85
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
86
|
+
if (ct.includes("text/event-stream") || ct.includes("ndjson")) {
|
|
87
|
+
return watchStream(res);
|
|
88
|
+
}
|
|
32
89
|
}
|
|
33
90
|
return res;
|
|
34
91
|
};
|
|
@@ -40,7 +97,7 @@ function uninstall() {
|
|
|
40
97
|
_original = null;
|
|
41
98
|
}
|
|
42
99
|
/**
|
|
43
|
-
* Subscribe to insufficient-credits
|
|
100
|
+
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
44
101
|
* Returns an unsubscribe function — call it in your useEffect cleanup.
|
|
45
102
|
*/
|
|
46
103
|
export function subscribeFetchWatcher(listener) {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,5 +2,6 @@ export { CreditsBadge, type CreditsBadgeProps } from "./CreditsBadge";
|
|
|
2
2
|
export { PaywallModal, type PaywallModalProps, type PackageOption, type SubscriptionPlanOption, type ActiveSubscriptionInfo, } from "./PaywallModal";
|
|
3
3
|
export { CreditIndicator, type CreditIndicatorProps } from "./CreditIndicator";
|
|
4
4
|
export { CrediballProvider, useCrediball, type CrediballProviderProps, } from "./CrediballProvider";
|
|
5
|
-
export { usePaywall, type UsePaywallProps, type UsePaywallResult, } from "./usePaywall";
|
|
5
|
+
export { usePaywall, isInsufficientCreditsError, type UsePaywallProps, type UsePaywallResult, } from "./usePaywall";
|
|
6
6
|
export { tokens, type CrediballThemeOverrides } from "./theme";
|
|
7
|
+
export { notifyInsufficientCredits } from "./creditEvent";
|
package/dist/index.js
CHANGED
|
@@ -4,5 +4,6 @@ export { CreditsBadge } from "./CreditsBadge";
|
|
|
4
4
|
export { PaywallModal, } from "./PaywallModal";
|
|
5
5
|
export { CreditIndicator } from "./CreditIndicator";
|
|
6
6
|
export { CrediballProvider, useCrediball, } from "./CrediballProvider";
|
|
7
|
-
export { usePaywall, } from "./usePaywall";
|
|
7
|
+
export { usePaywall, isInsufficientCreditsError, } from "./usePaywall";
|
|
8
8
|
export { tokens } from "./theme";
|
|
9
|
+
export { notifyInsufficientCredits } from "./creditEvent";
|
package/dist/usePaywall.d.ts
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
import { type PaywallModalProps, type PackageOption } from "./PaywallModal";
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { type PaywallModalProps, type PackageOption, type SubscriptionPlanOption } from "./PaywallModal";
|
|
3
|
+
/**
|
|
4
|
+
* Props for usePaywall. Accepts everything PaywallModal accepts, except the
|
|
5
|
+
* three props the hook manages itself (open, onClose, onAdd). Callback props
|
|
6
|
+
* that the hook wraps (onSelectPackage, onSelectPlan) are overridden to also
|
|
7
|
+
* accept async handlers. Add watchFetch to auto-open on detected credit errors.
|
|
8
|
+
*/
|
|
9
|
+
export type UsePaywallProps = Omit<PaywallModalProps, "open" | "onClose" | "onAdd" | "style"> & {
|
|
10
|
+
/** Called when the user picks a legacy preset/custom top-up amount. */
|
|
5
11
|
onTopup?: (amount: number) => void | Promise<void>;
|
|
6
|
-
/** Called when the user picks a package
|
|
12
|
+
/** Called when the user picks a one-time package (async override). */
|
|
7
13
|
onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
|
|
14
|
+
/** Called when the user picks a subscription plan (async override). */
|
|
15
|
+
onSelectPlan?: (plan: SubscriptionPlanOption) => void | Promise<void>;
|
|
8
16
|
/**
|
|
9
|
-
* Auto-detect
|
|
10
|
-
*
|
|
17
|
+
* Auto-detect credit errors from any fetch() call and open the paywall
|
|
18
|
+
* automatically. Defaults to false.
|
|
11
19
|
*
|
|
12
20
|
* Set to true only if you are NOT using <CrediballProvider> and want automatic
|
|
13
21
|
* interception. If your app already wraps with <CrediballProvider watchFetch>
|
|
14
22
|
* (the recommended pattern), leave this false — double-patching is harmless
|
|
15
23
|
* but unnecessary.
|
|
16
24
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
25
|
+
* Both HTTP 402 (regular routes) and text/event-stream / ndjson streaming
|
|
26
|
+
* responses are handled automatically.
|
|
19
27
|
*/
|
|
20
28
|
watchFetch?: boolean;
|
|
21
29
|
};
|
|
@@ -44,6 +52,16 @@ export interface UsePaywallResult {
|
|
|
44
52
|
* );
|
|
45
53
|
*/
|
|
46
54
|
paywall: ReactElement;
|
|
55
|
+
/**
|
|
56
|
+
* Pass directly to useChat / useCompletion as the `onError` prop. Triggers
|
|
57
|
+
* the paywall automatically when the Vercel AI SDK surfaces a credit error
|
|
58
|
+
* from a streaming route (the AI SDK captures `fetch` before window.fetch
|
|
59
|
+
* patching applies, so watchFetch cannot intercept streaming responses).
|
|
60
|
+
*
|
|
61
|
+
* const { paywall, onError } = usePaywall({ ... });
|
|
62
|
+
* const { messages } = useChat({ api: '/api/generate', onError });
|
|
63
|
+
*/
|
|
64
|
+
onError: (error: Error) => void;
|
|
47
65
|
}
|
|
48
66
|
/**
|
|
49
67
|
* Manages a Crediball paywall modal with a simple, misuse-proof API.
|
|
@@ -54,20 +72,18 @@ export interface UsePaywallResult {
|
|
|
54
72
|
* reopening it. This is what makes the paywall reliably reappear on retry.
|
|
55
73
|
*
|
|
56
74
|
* const { trigger, paywall } = usePaywall({
|
|
57
|
-
*
|
|
58
|
-
*
|
|
75
|
+
* packages: pkgs,
|
|
76
|
+
* subscriptionPlans: plans,
|
|
77
|
+
* activeSubscription: activeSub,
|
|
78
|
+
* onSelectPackage: async (pkg) => checkout(pkg),
|
|
79
|
+
* onSelectPlan: async (plan) => subscribe(plan),
|
|
80
|
+
* onCancelSubscription: async (id) => cancel(id),
|
|
59
81
|
* });
|
|
60
|
-
*
|
|
61
|
-
* try {
|
|
62
|
-
* const page = await meter.wrapMeteredAction(userId, "generate_page", async () => {
|
|
63
|
-
* const res = await streamAI(prompt);
|
|
64
|
-
* return { result: res, tokens: res.usage.totalTokens };
|
|
65
|
-
* });
|
|
66
|
-
* } catch (e) {
|
|
67
|
-
* if (e instanceof InsufficientCreditsError) trigger(); // always call — no guard
|
|
68
|
-
* else throw e;
|
|
69
|
-
* }
|
|
70
|
-
*
|
|
71
|
-
* return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
|
|
72
82
|
*/
|
|
73
|
-
export declare function usePaywall({
|
|
83
|
+
export declare function usePaywall({ onTopup, onSelectPackage, onSelectPlan, watchFetch, ...rest }?: UsePaywallProps): UsePaywallResult;
|
|
84
|
+
/**
|
|
85
|
+
* Returns true when an error from the Vercel AI SDK (or any other source)
|
|
86
|
+
* represents an insufficient-credits rejection from Crediball. Useful when
|
|
87
|
+
* you need to check the error yourself before calling trigger().
|
|
88
|
+
*/
|
|
89
|
+
export declare function isInsufficientCreditsError(error: unknown): boolean;
|
package/dist/usePaywall.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
-
import { PaywallModal } from "./PaywallModal";
|
|
4
|
+
import { PaywallModal, } from "./PaywallModal";
|
|
5
5
|
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
6
|
+
import { subscribeInsufficientCredits, notifyInsufficientCredits } from "./creditEvent";
|
|
6
7
|
/**
|
|
7
8
|
* Manages a Crediball paywall modal with a simple, misuse-proof API.
|
|
8
9
|
*
|
|
@@ -12,29 +13,24 @@ import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
|
12
13
|
* reopening it. This is what makes the paywall reliably reappear on retry.
|
|
13
14
|
*
|
|
14
15
|
* const { trigger, paywall } = usePaywall({
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* packages: pkgs,
|
|
17
|
+
* subscriptionPlans: plans,
|
|
18
|
+
* activeSubscription: activeSub,
|
|
19
|
+
* onSelectPackage: async (pkg) => checkout(pkg),
|
|
20
|
+
* onSelectPlan: async (plan) => subscribe(plan),
|
|
21
|
+
* onCancelSubscription: async (id) => cancel(id),
|
|
17
22
|
* });
|
|
18
|
-
*
|
|
19
|
-
* try {
|
|
20
|
-
* const page = await meter.wrapMeteredAction(userId, "generate_page", async () => {
|
|
21
|
-
* const res = await streamAI(prompt);
|
|
22
|
-
* return { result: res, tokens: res.usage.totalTokens };
|
|
23
|
-
* });
|
|
24
|
-
* } catch (e) {
|
|
25
|
-
* if (e instanceof InsufficientCreditsError) trigger(); // always call — no guard
|
|
26
|
-
* else throw e;
|
|
27
|
-
* }
|
|
28
|
-
*
|
|
29
|
-
* return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
|
|
30
23
|
*/
|
|
31
|
-
export function usePaywall({
|
|
24
|
+
export function usePaywall({ onTopup, onSelectPackage, onSelectPlan, watchFetch = false, ...rest } = {}) {
|
|
32
25
|
const [isOpen, setIsOpen] = useState(false);
|
|
33
26
|
const trigger = useCallback(() => setIsOpen(true), []);
|
|
34
27
|
const dismiss = useCallback(() => setIsOpen(false), []);
|
|
35
|
-
// Keep a ref so
|
|
28
|
+
// Keep a ref so closures always call the latest trigger.
|
|
36
29
|
const triggerRef = useRef(trigger);
|
|
37
30
|
triggerRef.current = trigger;
|
|
31
|
+
// Always listen for the global credit-error event — works regardless of how
|
|
32
|
+
// the request was made (fetch, XHR, AI SDK internals, etc.).
|
|
33
|
+
useEffect(() => subscribeInsufficientCredits(() => triggerRef.current()), []);
|
|
38
34
|
useEffect(() => {
|
|
39
35
|
if (!watchFetch)
|
|
40
36
|
return;
|
|
@@ -48,6 +44,27 @@ export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectP
|
|
|
48
44
|
setIsOpen(false);
|
|
49
45
|
await onSelectPackage?.(pkg);
|
|
50
46
|
}, [onSelectPackage]);
|
|
51
|
-
const
|
|
52
|
-
|
|
47
|
+
const handleSelectPlan = useCallback(async (plan) => {
|
|
48
|
+
setIsOpen(false);
|
|
49
|
+
await onSelectPlan?.(plan);
|
|
50
|
+
}, [onSelectPlan]);
|
|
51
|
+
// onError fires the global event so ALL mounted paywall instances react,
|
|
52
|
+
// not just this hook's local state.
|
|
53
|
+
const onError = useCallback((error) => {
|
|
54
|
+
if (isInsufficientCreditsError(error))
|
|
55
|
+
notifyInsufficientCredits();
|
|
56
|
+
}, []);
|
|
57
|
+
const paywall = (_jsx(PaywallModal, { ...rest, open: isOpen, onAdd: handleTopup, onSelectPackage: handleSelectPackage, onSelectPlan: handleSelectPlan, onClose: dismiss }));
|
|
58
|
+
return { trigger, dismiss, isOpen, paywall, onError };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns true when an error from the Vercel AI SDK (or any other source)
|
|
62
|
+
* represents an insufficient-credits rejection from Crediball. Useful when
|
|
63
|
+
* you need to check the error yourself before calling trigger().
|
|
64
|
+
*/
|
|
65
|
+
export function isInsufficientCreditsError(error) {
|
|
66
|
+
if (!(error instanceof Error))
|
|
67
|
+
return false;
|
|
68
|
+
const msg = error.message.toLowerCase();
|
|
69
|
+
return msg.includes("insufficient_credits") || msg.includes("insufficient credits");
|
|
53
70
|
}
|
package/package.json
CHANGED