@crediball/react 0.3.0 → 0.3.1
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.js +4 -37
- package/dist/PaywallModal.js +1 -1
- package/dist/fetchWatcher.d.ts +13 -0
- package/dist/fetchWatcher.js +57 -0
- package/dist/usePaywall.d.ts +12 -1
- package/dist/usePaywall.js +11 -2
- package/package.json +1 -1
|
@@ -2,10 +2,8 @@
|
|
|
2
2
|
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
|
+
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
5
6
|
const CrediballContext = createContext(null);
|
|
6
|
-
// Symbol used to mark a patched fetch so double-mount in React Strict Mode (dev)
|
|
7
|
-
// or a second <CrediballProvider> doesn't stack intercepts.
|
|
8
|
-
const CREDIBALL_FETCH_PATCHED = Symbol.for("crediball.fetch.patched");
|
|
9
7
|
/**
|
|
10
8
|
* Pattern B, automated. Wrap your app once:
|
|
11
9
|
*
|
|
@@ -22,46 +20,15 @@ const CREDIBALL_FETCH_PATCHED = Symbol.for("crediball.fetch.patched");
|
|
|
22
20
|
*/
|
|
23
21
|
export function CrediballProvider({ children, amounts = [5, 10, 20], onTopup, watchFetch = true, allowCustom = false, customMin, customMax, currencySymbol = "€", title, description, accentColor, }) {
|
|
24
22
|
const [open, setOpen] = useState(false);
|
|
25
|
-
// Stable callback refs so the fetch interceptor closure doesn't go stale.
|
|
26
23
|
const showPaywall = useCallback(() => setOpen(true), []);
|
|
27
24
|
const hidePaywall = useCallback(() => setOpen(false), []);
|
|
25
|
+
// Keep a ref so the fetch-watcher closure always calls the latest version.
|
|
28
26
|
const showPaywallRef = useRef(showPaywall);
|
|
29
27
|
showPaywallRef.current = showPaywall;
|
|
30
28
|
useEffect(() => {
|
|
31
|
-
if (!watchFetch
|
|
29
|
+
if (!watchFetch)
|
|
32
30
|
return;
|
|
33
|
-
|
|
34
|
-
// patched this window.fetch in the same tick, skip — the cleanup will restore.
|
|
35
|
-
const win = window;
|
|
36
|
-
if (win[CREDIBALL_FETCH_PATCHED])
|
|
37
|
-
return;
|
|
38
|
-
const original = window.fetch;
|
|
39
|
-
win[CREDIBALL_FETCH_PATCHED] = true;
|
|
40
|
-
window.fetch = async (...args) => {
|
|
41
|
-
const res = await original(...args);
|
|
42
|
-
if (res.status === 402) {
|
|
43
|
-
// Clone so the original response body is still readable by the caller.
|
|
44
|
-
res
|
|
45
|
-
.clone()
|
|
46
|
-
.json()
|
|
47
|
-
.then((body) => {
|
|
48
|
-
if (body && body.code === "insufficient_credits") {
|
|
49
|
-
showPaywallRef.current();
|
|
50
|
-
}
|
|
51
|
-
})
|
|
52
|
-
.catch(() => {
|
|
53
|
-
// Non-JSON 402 (e.g. Stripe webhook): open paywall anyway since any
|
|
54
|
-
// 402 from the app's own API routes is very likely a credit shortage.
|
|
55
|
-
// If the app has other 402 uses, set watchFetch=false and call
|
|
56
|
-
// showPaywall() manually.
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
return res;
|
|
60
|
-
};
|
|
61
|
-
return () => {
|
|
62
|
-
window.fetch = original;
|
|
63
|
-
delete win[CREDIBALL_FETCH_PATCHED];
|
|
64
|
-
};
|
|
31
|
+
return subscribeFetchWatcher(() => showPaywallRef.current());
|
|
65
32
|
}, [watchFetch]);
|
|
66
33
|
const ctx = useMemo(() => ({ showPaywall, hidePaywall, open }), [showPaywall, hidePaywall, open]);
|
|
67
34
|
async function handleAdd(amount) {
|
package/dist/PaywallModal.js
CHANGED
|
@@ -65,7 +65,7 @@ export function PaywallModal({ open, packages, onSelectPackage, amounts = [5, 10
|
|
|
65
65
|
return (_jsx("div", { role: "dialog", "aria-modal": "true", style: {
|
|
66
66
|
position: "fixed",
|
|
67
67
|
inset: 0,
|
|
68
|
-
zIndex:
|
|
68
|
+
zIndex: 2147483647,
|
|
69
69
|
display: "flex",
|
|
70
70
|
alignItems: "center",
|
|
71
71
|
justifyContent: "center",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level singleton that patches window.fetch once and fans out to all
|
|
3
|
+
* registered listeners (from CrediballProvider and/or usePaywall). Only one
|
|
4
|
+
* fetch patch exists at a time regardless of how many components subscribe.
|
|
5
|
+
* Reference-counted: the patch is removed when the last listener unsubscribes.
|
|
6
|
+
*/
|
|
7
|
+
type Listener = () => void;
|
|
8
|
+
/**
|
|
9
|
+
* Subscribe to insufficient-credits 402 events detected in window.fetch.
|
|
10
|
+
* Returns an unsubscribe function — call it in your useEffect cleanup.
|
|
11
|
+
*/
|
|
12
|
+
export declare function subscribeFetchWatcher(listener: Listener): () => void;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module-level singleton that patches window.fetch once and fans out to all
|
|
3
|
+
* registered listeners (from CrediballProvider and/or usePaywall). Only one
|
|
4
|
+
* fetch patch exists at a time regardless of how many components subscribe.
|
|
5
|
+
* Reference-counted: the patch is removed when the last listener unsubscribes.
|
|
6
|
+
*/
|
|
7
|
+
let _original = null;
|
|
8
|
+
const _listeners = new Set();
|
|
9
|
+
function install() {
|
|
10
|
+
if (typeof window === "undefined" || _original !== null)
|
|
11
|
+
return;
|
|
12
|
+
_original = window.fetch;
|
|
13
|
+
window.fetch = async (...args) => {
|
|
14
|
+
const res = await _original(...args);
|
|
15
|
+
if (res.status === 402) {
|
|
16
|
+
res
|
|
17
|
+
.clone()
|
|
18
|
+
.json()
|
|
19
|
+
.then((body) => {
|
|
20
|
+
if (body != null &&
|
|
21
|
+
typeof body === "object" &&
|
|
22
|
+
body.code === "insufficient_credits") {
|
|
23
|
+
_listeners.forEach((fn) => fn());
|
|
24
|
+
}
|
|
25
|
+
})
|
|
26
|
+
.catch(() => {
|
|
27
|
+
// Non-JSON 402. Fire listeners anyway — a 402 from the app's own API
|
|
28
|
+
// route is almost certainly a credit shortage. Set watchFetch=false
|
|
29
|
+
// and call trigger() manually if your app uses 402 for other purposes.
|
|
30
|
+
_listeners.forEach((fn) => fn());
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return res;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function uninstall() {
|
|
37
|
+
if (typeof window === "undefined" || _original === null)
|
|
38
|
+
return;
|
|
39
|
+
window.fetch = _original;
|
|
40
|
+
_original = null;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Subscribe to insufficient-credits 402 events detected in window.fetch.
|
|
44
|
+
* Returns an unsubscribe function — call it in your useEffect cleanup.
|
|
45
|
+
*/
|
|
46
|
+
export function subscribeFetchWatcher(listener) {
|
|
47
|
+
if (typeof window === "undefined")
|
|
48
|
+
return () => { };
|
|
49
|
+
if (_listeners.size === 0)
|
|
50
|
+
install();
|
|
51
|
+
_listeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
_listeners.delete(listener);
|
|
54
|
+
if (_listeners.size === 0)
|
|
55
|
+
uninstall();
|
|
56
|
+
};
|
|
57
|
+
}
|
package/dist/usePaywall.d.ts
CHANGED
|
@@ -5,6 +5,17 @@ export type UsePaywallProps = Pick<PaywallModalProps, "amounts" | "amountPrefix"
|
|
|
5
5
|
onTopup?: (amount: number) => void | Promise<void>;
|
|
6
6
|
/** Called when the user picks a package from `packages`. */
|
|
7
7
|
onSelectPackage?: (pkg: PackageOption) => void | Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* Auto-detect HTTP 402 `insufficient_credits` responses from any fetch() call
|
|
10
|
+
* and open the paywall automatically. Defaults to true.
|
|
11
|
+
*
|
|
12
|
+
* Set to false if your app uses <CrediballProvider> (it already watches fetch)
|
|
13
|
+
* or if you want to trigger the paywall exclusively via trigger().
|
|
14
|
+
*
|
|
15
|
+
* Note: SSE/streaming routes surface credit errors as stream events (HTTP 200),
|
|
16
|
+
* not HTTP 402 — call trigger() manually from your stream error handler.
|
|
17
|
+
*/
|
|
18
|
+
watchFetch?: boolean;
|
|
8
19
|
};
|
|
9
20
|
export interface UsePaywallResult {
|
|
10
21
|
/**
|
|
@@ -57,4 +68,4 @@ export interface UsePaywallResult {
|
|
|
57
68
|
*
|
|
58
69
|
* return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
|
|
59
70
|
*/
|
|
60
|
-
export declare function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, }?: UsePaywallProps): UsePaywallResult;
|
|
71
|
+
export declare function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch, }?: UsePaywallProps): UsePaywallResult;
|
package/dist/usePaywall.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { useCallback, useState } from "react";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { PaywallModal } from "./PaywallModal";
|
|
5
|
+
import { subscribeFetchWatcher } from "./fetchWatcher";
|
|
5
6
|
/**
|
|
6
7
|
* Manages a Crediball paywall modal with a simple, misuse-proof API.
|
|
7
8
|
*
|
|
@@ -27,10 +28,18 @@ import { PaywallModal } from "./PaywallModal";
|
|
|
27
28
|
*
|
|
28
29
|
* return <div>...<button onClick={generate}>Generate</button>...{paywall}</div>;
|
|
29
30
|
*/
|
|
30
|
-
export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, } = {}) {
|
|
31
|
+
export function usePaywall({ amounts, amountPrefix, packages, onTopup, onSelectPackage, allowCustom, customMin, customMax, currencySymbol, title, description, accentColor, balance, balanceRate, currency, watchFetch = true, } = {}) {
|
|
31
32
|
const [isOpen, setIsOpen] = useState(false);
|
|
32
33
|
const trigger = useCallback(() => setIsOpen(true), []);
|
|
33
34
|
const dismiss = useCallback(() => setIsOpen(false), []);
|
|
35
|
+
// Keep a ref so the fetch-watcher closure always calls the latest trigger.
|
|
36
|
+
const triggerRef = useRef(trigger);
|
|
37
|
+
triggerRef.current = trigger;
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!watchFetch)
|
|
40
|
+
return;
|
|
41
|
+
return subscribeFetchWatcher(() => triggerRef.current());
|
|
42
|
+
}, [watchFetch]);
|
|
34
43
|
const handleTopup = useCallback(async (amount) => {
|
|
35
44
|
setIsOpen(false);
|
|
36
45
|
await onTopup?.(amount);
|
package/package.json
CHANGED