@crediball/react 0.3.2 → 0.3.7
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 +17 -6
- package/dist/fetchWatcher.js +78 -29
- 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
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Patches window.fetch at module-import time — before any React component
|
|
3
|
+
* mounts and before any AI SDK can capture the fetch reference. This is the
|
|
4
|
+
* key difference from installing the patch in a useEffect: by the time a
|
|
5
|
+
* useEffect runs, the Vercel AI SDK (and others) may have already resolved
|
|
6
|
+
* their fetch reference and bypass the patch entirely.
|
|
7
|
+
*
|
|
8
|
+
* Handles three credit-error surfaces:
|
|
9
|
+
* 1. HTTP 402 with { code: "insufficient_credits" } — regular JSON routes.
|
|
10
|
+
* 2. HTTP 200 streaming body containing "insufficient_credits" — SSE, ndjson,
|
|
11
|
+
* or Vercel AI SDK data-stream (text/plain) routes.
|
|
12
|
+
* 3. Manual call to notifyInsufficientCredits() from any error handler.
|
|
6
13
|
*/
|
|
7
14
|
type Listener = () => void;
|
|
8
15
|
/**
|
|
9
|
-
* Subscribe to insufficient-credits
|
|
10
|
-
* Returns an unsubscribe function
|
|
16
|
+
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
17
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
18
|
+
*
|
|
19
|
+
* The fetch patch is installed at module load time regardless of whether
|
|
20
|
+
* anyone calls this function. This exists for components that need a direct
|
|
21
|
+
* callback rather than the global DOM event.
|
|
11
22
|
*/
|
|
12
23
|
export declare function subscribeFetchWatcher(listener: Listener): () => void;
|
|
13
24
|
export {};
|
package/dist/fetchWatcher.js
CHANGED
|
@@ -1,15 +1,67 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Patches window.fetch at module-import time — before any React component
|
|
3
|
+
* mounts and before any AI SDK can capture the fetch reference. This is the
|
|
4
|
+
* key difference from installing the patch in a useEffect: by the time a
|
|
5
|
+
* useEffect runs, the Vercel AI SDK (and others) may have already resolved
|
|
6
|
+
* their fetch reference and bypass the patch entirely.
|
|
7
|
+
*
|
|
8
|
+
* Handles three credit-error surfaces:
|
|
9
|
+
* 1. HTTP 402 with { code: "insufficient_credits" } — regular JSON routes.
|
|
10
|
+
* 2. HTTP 200 streaming body containing "insufficient_credits" — SSE, ndjson,
|
|
11
|
+
* or Vercel AI SDK data-stream (text/plain) routes.
|
|
12
|
+
* 3. Manual call to notifyInsufficientCredits() from any error handler.
|
|
6
13
|
*/
|
|
7
|
-
|
|
14
|
+
import { notifyInsufficientCredits } from "./creditEvent";
|
|
8
15
|
const _listeners = new Set();
|
|
9
|
-
function
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
function fire() {
|
|
17
|
+
notifyInsufficientCredits();
|
|
18
|
+
_listeners.forEach((fn) => fn());
|
|
19
|
+
}
|
|
20
|
+
function isStreamingContentType(ct) {
|
|
21
|
+
return (ct.includes("text/event-stream") ||
|
|
22
|
+
ct.includes("ndjson") ||
|
|
23
|
+
// Vercel AI SDK uses text/plain for its data-stream protocol
|
|
24
|
+
ct.includes("text/plain"));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Tee the streaming response body and scan each chunk for the
|
|
28
|
+
* `insufficient_credits` signal. Returns a new Response whose body is the
|
|
29
|
+
* pass-through branch so the calling code receives the full, unmodified stream.
|
|
30
|
+
*/
|
|
31
|
+
function watchStream(res) {
|
|
32
|
+
if (!res.body)
|
|
33
|
+
return res;
|
|
34
|
+
const [scanBranch, passBranch] = res.body.tee();
|
|
35
|
+
(async () => {
|
|
36
|
+
const reader = scanBranch.getReader();
|
|
37
|
+
const decoder = new TextDecoder();
|
|
38
|
+
try {
|
|
39
|
+
for (;;) {
|
|
40
|
+
const { done, value } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
break;
|
|
43
|
+
if (decoder.decode(value, { stream: true }).includes("insufficient_credits")) {
|
|
44
|
+
fire();
|
|
45
|
+
reader.cancel().catch(() => { });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// stream error — ignore
|
|
52
|
+
}
|
|
53
|
+
})();
|
|
54
|
+
return new Response(passBranch, {
|
|
55
|
+
status: res.status,
|
|
56
|
+
statusText: res.statusText,
|
|
57
|
+
headers: res.headers,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// ── Install patch immediately at module load time ─────────────────────────
|
|
61
|
+
// Running here (not in a useEffect) ensures the patch is in place before any
|
|
62
|
+
// SDK or framework code captures the window.fetch reference.
|
|
63
|
+
if (typeof window !== "undefined") {
|
|
64
|
+
const _original = window.fetch;
|
|
13
65
|
window.fetch = async (...args) => {
|
|
14
66
|
const res = await _original(...args);
|
|
15
67
|
if (res.status === 402) {
|
|
@@ -20,38 +72,35 @@ function install() {
|
|
|
20
72
|
if (body != null &&
|
|
21
73
|
typeof body === "object" &&
|
|
22
74
|
body.code === "insufficient_credits") {
|
|
23
|
-
|
|
75
|
+
fire();
|
|
24
76
|
}
|
|
25
77
|
})
|
|
26
78
|
.catch(() => {
|
|
27
|
-
// Non-JSON 402
|
|
28
|
-
|
|
29
|
-
// and call trigger() manually if your app uses 402 for other purposes.
|
|
30
|
-
_listeners.forEach((fn) => fn());
|
|
79
|
+
// Non-JSON 402 — fire anyway.
|
|
80
|
+
fire();
|
|
31
81
|
});
|
|
82
|
+
return res;
|
|
83
|
+
}
|
|
84
|
+
if (res.body) {
|
|
85
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
86
|
+
if (isStreamingContentType(ct)) {
|
|
87
|
+
return watchStream(res);
|
|
88
|
+
}
|
|
32
89
|
}
|
|
33
90
|
return res;
|
|
34
91
|
};
|
|
35
92
|
}
|
|
36
|
-
function uninstall() {
|
|
37
|
-
if (typeof window === "undefined" || _original === null)
|
|
38
|
-
return;
|
|
39
|
-
window.fetch = _original;
|
|
40
|
-
_original = null;
|
|
41
|
-
}
|
|
42
93
|
/**
|
|
43
|
-
* Subscribe to insufficient-credits
|
|
44
|
-
* Returns an unsubscribe function
|
|
94
|
+
* Subscribe to insufficient-credits events detected in window.fetch.
|
|
95
|
+
* Returns an unsubscribe function for use in useEffect cleanup.
|
|
96
|
+
*
|
|
97
|
+
* The fetch patch is installed at module load time regardless of whether
|
|
98
|
+
* anyone calls this function. This exists for components that need a direct
|
|
99
|
+
* callback rather than the global DOM event.
|
|
45
100
|
*/
|
|
46
101
|
export function subscribeFetchWatcher(listener) {
|
|
47
102
|
if (typeof window === "undefined")
|
|
48
103
|
return () => { };
|
|
49
|
-
if (_listeners.size === 0)
|
|
50
|
-
install();
|
|
51
104
|
_listeners.add(listener);
|
|
52
|
-
return () =>
|
|
53
|
-
_listeners.delete(listener);
|
|
54
|
-
if (_listeners.size === 0)
|
|
55
|
-
uninstall();
|
|
56
|
-
};
|
|
105
|
+
return () => _listeners.delete(listener);
|
|
57
106
|
}
|
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