@superwall/paywalls-react 0.2.0 → 0.2.5
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/SuperwallPaywall.d.ts +28 -0
- package/dist/SuperwallPaywall.js +50 -0
- package/dist/hooks.d.ts +108 -0
- package/dist/hooks.js +231 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +9 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +65 -0
- package/package.json +21 -7
- package/.turbo/turbo-build.log +0 -3
- package/.turbo/turbo-test.log +0 -249
- package/.turbo/turbo-typecheck.log +0 -11
- package/src/SuperwallPaywall.tsx +0 -94
- package/src/hooks.test.tsx +0 -483
- package/src/hooks.ts +0 -368
- package/src/index.test.ts +0 -7
- package/src/index.ts +0 -36
- package/src/provider.test.tsx +0 -102
- package/src/provider.tsx +0 -99
- package/test-setup-dom.ts +0 -11
- package/tsconfig.json +0 -9
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { PlacementParams } from "@superwall/paywalls-js";
|
|
3
|
+
import type { PaywallPresentationHandlerHooks } from "./hooks.js";
|
|
4
|
+
export interface SuperwallPaywallProps extends PaywallPresentationHandlerHooks {
|
|
5
|
+
placement: string;
|
|
6
|
+
params?: PlacementParams;
|
|
7
|
+
/** Rendered when the user is entitled — i.e. the feature block fires. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/** Rendered while the paywall is loading. Swapped out the moment the paywall presents. */
|
|
10
|
+
loading?: ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* When true the paywall iframe is mounted inside this component instead of
|
|
13
|
+
* as a full-viewport overlay. Default: false.
|
|
14
|
+
*/
|
|
15
|
+
inline?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Declarative feature gate. Calls `register()` on mount and renders
|
|
19
|
+
* `children` when the user is entitled (already subscribed, purchased
|
|
20
|
+
* through the paywall, or the placement has no audience match).
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* <SuperwallPaywall placement="campaign_trigger" loading={<Spinner />}>
|
|
24
|
+
* <ProContent />
|
|
25
|
+
* </SuperwallPaywall>
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export declare function SuperwallPaywall({ placement, params, children, loading, inline, onPresent, onDismiss, onSkip, onError, }: SuperwallPaywallProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { createBrowserPresenter } from "@superwall/paywalls-js/browser";
|
|
4
|
+
import { useSuperwall } from "./hooks.js";
|
|
5
|
+
/**
|
|
6
|
+
* Declarative feature gate. Calls `register()` on mount and renders
|
|
7
|
+
* `children` when the user is entitled (already subscribed, purchased
|
|
8
|
+
* through the paywall, or the placement has no audience match).
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <SuperwallPaywall placement="campaign_trigger" loading={<Spinner />}>
|
|
12
|
+
* <ProContent />
|
|
13
|
+
* </SuperwallPaywall>
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function SuperwallPaywall({ placement, params, children, loading = null, inline = false, onPresent, onDismiss, onSkip, onError, }) {
|
|
17
|
+
const sw = useSuperwall();
|
|
18
|
+
const [unlocked, setUnlocked] = useState(false);
|
|
19
|
+
const [presenting, setPresenting] = useState(false);
|
|
20
|
+
const containerRef = useRef(null);
|
|
21
|
+
const handlersRef = useRef({ onPresent, onDismiss, onSkip, onError });
|
|
22
|
+
handlersRef.current = { onPresent, onDismiss, onSkip, onError };
|
|
23
|
+
const presenter = useMemo(() => inline
|
|
24
|
+
? createBrowserPresenter({ container: () => containerRef.current ?? document.body, inline: true })
|
|
25
|
+
: undefined, [inline]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
sw.register({
|
|
29
|
+
placement,
|
|
30
|
+
...(params && { params }),
|
|
31
|
+
feature: () => { if (!cancelled)
|
|
32
|
+
setUnlocked(true); },
|
|
33
|
+
...(presenter && { presenter }),
|
|
34
|
+
handler: {
|
|
35
|
+
onPresent: (info) => { if (!cancelled)
|
|
36
|
+
setPresenting(true); handlersRef.current.onPresent?.(info); },
|
|
37
|
+
onDismiss: (info, result) => { handlersRef.current.onDismiss?.(info, result); },
|
|
38
|
+
onSkip: (reason) => { handlersRef.current.onSkip?.(reason); },
|
|
39
|
+
onError: (error) => { handlersRef.current.onError?.(error); },
|
|
40
|
+
},
|
|
41
|
+
}).catch(() => { });
|
|
42
|
+
return () => { cancelled = true; };
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, [placement, params, presenter]);
|
|
45
|
+
if (unlocked)
|
|
46
|
+
return _jsx(_Fragment, { children: children });
|
|
47
|
+
// Container always stays in the DOM so the presenter can append the iframe
|
|
48
|
+
// before onPresent fires. We swap loading ↔ iframe via display:none.
|
|
49
|
+
return (_jsxs("div", { style: { position: "relative", width: "100%", height: "100%" }, children: [_jsx("div", { ref: containerRef, style: { display: presenting ? "block" : "none", width: "100%", height: "100%" } }), !presenting && (_jsx("div", { style: { position: "absolute", inset: 0 }, children: loading }))] }));
|
|
50
|
+
}
|
package/dist/hooks.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { AllSuperwallEvents, CustomerInfo, CustomPaywallController, CustomPaywallState, Entitlement, IdentityOptions, IntegrationAttribute, PaywallInfo, PaywallResult, PaywallSkippedReason, PlacementParams, Readable, RegisterPlacementArgs, RegisterPlacementResult, Superwall, SuperwallCustomEvent, SuperwallDelegate, SubscriptionStatus, UserAttributes } from "@superwall/paywalls-js";
|
|
2
|
+
export declare const useSuperwall: () => Superwall;
|
|
3
|
+
/**
|
|
4
|
+
* Subscribe to a `Readable<T>` and re-render on change. Stores the signal
|
|
5
|
+
* in a ref so an unstable `signal` identity per render doesn't cause
|
|
6
|
+
* `useSyncExternalStore` to re-subscribe on every render (which would
|
|
7
|
+
* spin into an infinite re-render loop). The ref always points at the
|
|
8
|
+
* latest signal; subscribe + getSnapshot read through it.
|
|
9
|
+
*/
|
|
10
|
+
export declare const useSignal: <T>(signal: Readable<T>) => T;
|
|
11
|
+
export interface UseUserResult {
|
|
12
|
+
id: string;
|
|
13
|
+
aliasId: string;
|
|
14
|
+
effectiveId: string;
|
|
15
|
+
isLoggedIn: boolean;
|
|
16
|
+
attributes: UserAttributes;
|
|
17
|
+
integrationAttributes: Partial<Record<IntegrationAttribute, string>>;
|
|
18
|
+
subscriptionStatus: SubscriptionStatus;
|
|
19
|
+
customerInfo: CustomerInfo | null;
|
|
20
|
+
/** Active entitlements convenience — derived from subscriptionStatus. */
|
|
21
|
+
entitlements: Entitlement[];
|
|
22
|
+
identify: (userId: string, opts?: IdentityOptions) => Promise<void>;
|
|
23
|
+
signOut: () => Promise<void>;
|
|
24
|
+
setAttributes: (attrs: Partial<UserAttributes>) => void;
|
|
25
|
+
setIntegrationAttribute: (attr: IntegrationAttribute, value: string | null) => void;
|
|
26
|
+
setIntegrationAttributes: (attrs: Partial<Record<IntegrationAttribute, string | null>>) => void;
|
|
27
|
+
}
|
|
28
|
+
export declare const useUser: () => UseUserResult;
|
|
29
|
+
export interface PaywallPresentationHandlerHooks {
|
|
30
|
+
onPresent?(info: PaywallInfo): void;
|
|
31
|
+
onDismiss?(info: PaywallInfo, result: PaywallResult): void;
|
|
32
|
+
onError?(error: Error): void;
|
|
33
|
+
onSkip?(reason: PaywallSkippedReason): void;
|
|
34
|
+
}
|
|
35
|
+
export type PaywallState = {
|
|
36
|
+
type: "idle";
|
|
37
|
+
} | {
|
|
38
|
+
type: "presented";
|
|
39
|
+
info: PaywallInfo;
|
|
40
|
+
} | {
|
|
41
|
+
type: "dismissed";
|
|
42
|
+
info: PaywallInfo;
|
|
43
|
+
result: PaywallResult;
|
|
44
|
+
} | {
|
|
45
|
+
type: "skipped";
|
|
46
|
+
reason: PaywallSkippedReason;
|
|
47
|
+
} | {
|
|
48
|
+
type: "error";
|
|
49
|
+
error: Error;
|
|
50
|
+
};
|
|
51
|
+
export interface UsePlacementResult {
|
|
52
|
+
register: (args: RegisterPlacementArgs) => Promise<RegisterPlacementResult>;
|
|
53
|
+
state: PaywallState;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns a `register` function bound to the active Superwall + a `state`
|
|
57
|
+
* reflecting the latest placement outcome from THIS hook's calls. Handler
|
|
58
|
+
* callbacks fire alongside the global delegate. State is local to the
|
|
59
|
+
* hook (one component's state, not the SDK's).
|
|
60
|
+
*/
|
|
61
|
+
export declare const usePlacement: (handler?: PaywallPresentationHandlerHooks) => UsePlacementResult;
|
|
62
|
+
export interface UseCustomPaywallOptions extends PaywallPresentationHandlerHooks {
|
|
63
|
+
placement: string;
|
|
64
|
+
params?: PlacementParams;
|
|
65
|
+
/** Runs when the user is entitled OR a non-gated paywall is dismissed
|
|
66
|
+
* without purchase. Same semantics as `register({ feature })`. */
|
|
67
|
+
feature?: () => void | Promise<void>;
|
|
68
|
+
}
|
|
69
|
+
export interface CustomPaywallMountSnapshot {
|
|
70
|
+
readonly state: CustomPaywallState;
|
|
71
|
+
readonly controller: CustomPaywallController;
|
|
72
|
+
}
|
|
73
|
+
export interface UseCustomPaywallResult {
|
|
74
|
+
/** Trigger the placement. Runs the full SDK pipeline (rules / holdout /
|
|
75
|
+
* assignment / gating / analytics); if it decides to present, `paywall`
|
|
76
|
+
* flips non-null and your UI should render. Resolves with the placement
|
|
77
|
+
* outcome. */
|
|
78
|
+
register: () => Promise<RegisterPlacementResult>;
|
|
79
|
+
/** Active mount (state snapshot + controller) while presenting, else null.
|
|
80
|
+
* Re-renders the calling component whenever the transaction / restoration
|
|
81
|
+
* phase changes. */
|
|
82
|
+
paywall: CustomPaywallMountSnapshot | null;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* React primitive for custom (developer-rendered) paywalls — the web analogue
|
|
86
|
+
* of Android's `SuperwallCustomPaywall`. You own the UI; the SDK runs the
|
|
87
|
+
* trigger pipeline, resolves products, fires identical lifecycle events, and
|
|
88
|
+
* hands you a controller. Render `paywall` when it's non-null; drive
|
|
89
|
+
* `paywall.controller.buy / restore / close` from your buttons.
|
|
90
|
+
*
|
|
91
|
+
* const { register, paywall } = useCustomPaywall({ placement: "home" });
|
|
92
|
+
* return (
|
|
93
|
+
* <>
|
|
94
|
+
* <button onClick={register}>Go Pro</button>
|
|
95
|
+
* {paywall && (
|
|
96
|
+
* <MyPaywall
|
|
97
|
+
* products={paywall.state.products}
|
|
98
|
+
* busy={paywall.state.transaction.phase === "purchasing"}
|
|
99
|
+
* onBuy={paywall.controller.buy}
|
|
100
|
+
* onClose={paywall.controller.close}
|
|
101
|
+
* />
|
|
102
|
+
* )}
|
|
103
|
+
* </>
|
|
104
|
+
* );
|
|
105
|
+
*/
|
|
106
|
+
export declare const useCustomPaywall: (opts: UseCustomPaywallOptions) => UseCustomPaywallResult;
|
|
107
|
+
export declare const useSuperwallEvent: <K extends keyof AllSuperwallEvents>(type: K, listener: (event: SuperwallCustomEvent<K>) => void) => void;
|
|
108
|
+
export declare const useDelegate: (delegate: SuperwallDelegate | null) => void;
|
package/dist/hooks.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// React hooks — sugar over the public `Superwall` instance + `Readable<T>`.
|
|
2
|
+
// Per API.md §9.3.
|
|
3
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
4
|
+
import { useSuperwallContext } from "./provider.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// useSuperwall — raw instance access
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
export const useSuperwall = () => useSuperwallContext();
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// useSignal — bridge `Readable<T>` to React via useSyncExternalStore
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
/**
|
|
13
|
+
* Subscribe to a `Readable<T>` and re-render on change. Stores the signal
|
|
14
|
+
* in a ref so an unstable `signal` identity per render doesn't cause
|
|
15
|
+
* `useSyncExternalStore` to re-subscribe on every render (which would
|
|
16
|
+
* spin into an infinite re-render loop). The ref always points at the
|
|
17
|
+
* latest signal; subscribe + getSnapshot read through it.
|
|
18
|
+
*/
|
|
19
|
+
export const useSignal = (signal) => {
|
|
20
|
+
const ref = useRef(signal);
|
|
21
|
+
ref.current = signal;
|
|
22
|
+
const subscribe = useCallback((onChange) => ref.current.subscribe(() => onChange()), []);
|
|
23
|
+
const getSnapshot = useCallback(() => ref.current.value, []);
|
|
24
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
25
|
+
};
|
|
26
|
+
export const useUser = () => {
|
|
27
|
+
const sw = useSuperwall();
|
|
28
|
+
return {
|
|
29
|
+
id: useSignal(sw.user.id),
|
|
30
|
+
aliasId: useSignal(sw.user.aliasId),
|
|
31
|
+
effectiveId: useSignal(sw.user.effectiveId),
|
|
32
|
+
isLoggedIn: useSignal(sw.user.isLoggedIn),
|
|
33
|
+
attributes: useSignal(sw.user.attributes),
|
|
34
|
+
integrationAttributes: useSignal(sw.user.integrationAttributes),
|
|
35
|
+
subscriptionStatus: useSignal(sw.subscriptionStatus),
|
|
36
|
+
customerInfo: useSignal(sw.customerInfo),
|
|
37
|
+
entitlements: useSignal(sw.entitlements.active),
|
|
38
|
+
identify: sw.user.identify,
|
|
39
|
+
signOut: sw.user.signOut,
|
|
40
|
+
setAttributes: sw.user.setAttributes,
|
|
41
|
+
setIntegrationAttribute: sw.user.setIntegrationAttribute,
|
|
42
|
+
setIntegrationAttributes: sw.user.setIntegrationAttributes,
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Returns a `register` function bound to the active Superwall + a `state`
|
|
47
|
+
* reflecting the latest placement outcome from THIS hook's calls. Handler
|
|
48
|
+
* callbacks fire alongside the global delegate. State is local to the
|
|
49
|
+
* hook (one component's state, not the SDK's).
|
|
50
|
+
*/
|
|
51
|
+
export const usePlacement = (handler) => {
|
|
52
|
+
const sw = useSuperwall();
|
|
53
|
+
const [state, setState] = useState({ type: "idle" });
|
|
54
|
+
// Latest-handler ref so the user can rely on closure-captured values
|
|
55
|
+
// without re-binding `register` on every render.
|
|
56
|
+
const handlerRef = useRef(handler);
|
|
57
|
+
handlerRef.current = handler;
|
|
58
|
+
const register = useCallback(async (args) => {
|
|
59
|
+
const r = await sw.register({
|
|
60
|
+
...args,
|
|
61
|
+
handler: {
|
|
62
|
+
onPresent: (info) => {
|
|
63
|
+
setState({ type: "presented", info });
|
|
64
|
+
try {
|
|
65
|
+
handlerRef.current?.onPresent?.(info);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
/* swallow */
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
onDismiss: (info, result) => {
|
|
72
|
+
setState({ type: "dismissed", info, result });
|
|
73
|
+
try {
|
|
74
|
+
handlerRef.current?.onDismiss?.(info, result);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* swallow */
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
onError: (error) => {
|
|
81
|
+
setState({ type: "error", error });
|
|
82
|
+
try {
|
|
83
|
+
handlerRef.current?.onError?.(error);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* swallow */
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
onSkip: (reason) => {
|
|
90
|
+
setState({ type: "skipped", reason });
|
|
91
|
+
try {
|
|
92
|
+
handlerRef.current?.onSkip?.(reason);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
/* swallow */
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return r;
|
|
101
|
+
}, [sw]);
|
|
102
|
+
return { register, state };
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* React primitive for custom (developer-rendered) paywalls — the web analogue
|
|
106
|
+
* of Android's `SuperwallCustomPaywall`. You own the UI; the SDK runs the
|
|
107
|
+
* trigger pipeline, resolves products, fires identical lifecycle events, and
|
|
108
|
+
* hands you a controller. Render `paywall` when it's non-null; drive
|
|
109
|
+
* `paywall.controller.buy / restore / close` from your buttons.
|
|
110
|
+
*
|
|
111
|
+
* const { register, paywall } = useCustomPaywall({ placement: "home" });
|
|
112
|
+
* return (
|
|
113
|
+
* <>
|
|
114
|
+
* <button onClick={register}>Go Pro</button>
|
|
115
|
+
* {paywall && (
|
|
116
|
+
* <MyPaywall
|
|
117
|
+
* products={paywall.state.products}
|
|
118
|
+
* busy={paywall.state.transaction.phase === "purchasing"}
|
|
119
|
+
* onBuy={paywall.controller.buy}
|
|
120
|
+
* onClose={paywall.controller.close}
|
|
121
|
+
* />
|
|
122
|
+
* )}
|
|
123
|
+
* </>
|
|
124
|
+
* );
|
|
125
|
+
*/
|
|
126
|
+
export const useCustomPaywall = (opts) => {
|
|
127
|
+
const sw = useSuperwall();
|
|
128
|
+
const [paywall, setPaywall] = useState(null);
|
|
129
|
+
// Latest-opts ref so `register` stays stable but reads fresh callbacks.
|
|
130
|
+
const optsRef = useRef(opts);
|
|
131
|
+
optsRef.current = opts;
|
|
132
|
+
const register = useCallback(() => {
|
|
133
|
+
const o = optsRef.current;
|
|
134
|
+
return sw.register({
|
|
135
|
+
placement: o.placement,
|
|
136
|
+
...(o.params !== undefined && { params: o.params }),
|
|
137
|
+
...(o.feature !== undefined && { feature: o.feature }),
|
|
138
|
+
handler: {
|
|
139
|
+
onPresent: (info) => {
|
|
140
|
+
try {
|
|
141
|
+
optsRef.current.onPresent?.(info);
|
|
142
|
+
}
|
|
143
|
+
catch { /* swallow */ }
|
|
144
|
+
},
|
|
145
|
+
onDismiss: (info, result) => {
|
|
146
|
+
try {
|
|
147
|
+
optsRef.current.onDismiss?.(info, result);
|
|
148
|
+
}
|
|
149
|
+
catch { /* swallow */ }
|
|
150
|
+
},
|
|
151
|
+
onError: (error) => {
|
|
152
|
+
try {
|
|
153
|
+
optsRef.current.onError?.(error);
|
|
154
|
+
}
|
|
155
|
+
catch { /* swallow */ }
|
|
156
|
+
},
|
|
157
|
+
onSkip: (reason) => {
|
|
158
|
+
try {
|
|
159
|
+
optsRef.current.onSkip?.(reason);
|
|
160
|
+
}
|
|
161
|
+
catch { /* swallow */ }
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
// The renderer bridges core's reactive state into React state. Core
|
|
165
|
+
// calls it once on present; we subscribe (fires sync) and mirror each
|
|
166
|
+
// update into component state. Teardown clears the mount on dismiss.
|
|
167
|
+
paywall: ({ state, controller }) => {
|
|
168
|
+
const unsubscribe = state.subscribe((s) => {
|
|
169
|
+
setPaywall({ state: s, controller });
|
|
170
|
+
});
|
|
171
|
+
return () => {
|
|
172
|
+
unsubscribe();
|
|
173
|
+
setPaywall(null);
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}, [sw]);
|
|
178
|
+
return { register, paywall };
|
|
179
|
+
};
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// useSuperwallEvent — typed addEventListener with auto-cleanup
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
export const useSuperwallEvent = (type, listener) => {
|
|
184
|
+
const sw = useSuperwall();
|
|
185
|
+
// Latest-listener ref so the user can capture fresh closures without
|
|
186
|
+
// re-attaching the listener every render.
|
|
187
|
+
const listenerRef = useRef(listener);
|
|
188
|
+
listenerRef.current = listener;
|
|
189
|
+
useEffect(() => {
|
|
190
|
+
const ac = new AbortController();
|
|
191
|
+
sw.events.addEventListener(type, (e) => listenerRef.current(e), { signal: ac.signal });
|
|
192
|
+
return () => ac.abort();
|
|
193
|
+
}, [sw, type]);
|
|
194
|
+
};
|
|
195
|
+
const delegateStacks = new WeakMap();
|
|
196
|
+
const applyTop = (sw) => {
|
|
197
|
+
const stack = delegateStacks.get(sw) ?? [];
|
|
198
|
+
const top = stack.length === 0 ? null : (stack[stack.length - 1].delegate);
|
|
199
|
+
sw.setDelegate(top);
|
|
200
|
+
};
|
|
201
|
+
export const useDelegate = (delegate) => {
|
|
202
|
+
const sw = useSuperwall();
|
|
203
|
+
const ref = useRef(delegate);
|
|
204
|
+
ref.current = delegate;
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const id = Symbol("useDelegate");
|
|
207
|
+
// Always read latest delegate from the ref so callbacks see the current
|
|
208
|
+
// closure values without re-mounting.
|
|
209
|
+
const wrapped = ref.current === null
|
|
210
|
+
? null
|
|
211
|
+
: new Proxy({}, {
|
|
212
|
+
get(_t, prop) {
|
|
213
|
+
const d = ref.current;
|
|
214
|
+
return d ? d[prop] : undefined;
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
const stack = delegateStacks.get(sw) ?? [];
|
|
218
|
+
stack.push({ id, delegate: wrapped });
|
|
219
|
+
delegateStacks.set(sw, stack);
|
|
220
|
+
applyTop(sw);
|
|
221
|
+
return () => {
|
|
222
|
+
const current = delegateStacks.get(sw) ?? [];
|
|
223
|
+
const next = current.filter((e) => e.id !== id);
|
|
224
|
+
if (next.length === 0)
|
|
225
|
+
delegateStacks.delete(sw);
|
|
226
|
+
else
|
|
227
|
+
delegateStacks.set(sw, next);
|
|
228
|
+
applyTop(sw);
|
|
229
|
+
};
|
|
230
|
+
}, [sw]);
|
|
231
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SDK_VERSION } from "@superwall/paywalls-js";
|
|
2
|
+
export { SuperwallProvider, type SuperwallProviderProps, useSuperwallContext, } from "./provider.js";
|
|
3
|
+
export { useSuperwall, useSignal, useUser, usePlacement, useCustomPaywall, useSuperwallEvent, useDelegate, type UseUserResult, type UsePlacementResult, type UseCustomPaywallOptions, type UseCustomPaywallResult, type CustomPaywallMountSnapshot, type PaywallState, type PaywallPresentationHandlerHooks, } from "./hooks.js";
|
|
4
|
+
export { SuperwallPaywall, type SuperwallPaywallProps, } from "./SuperwallPaywall.js";
|
|
5
|
+
export * from "@superwall/paywalls-js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @superwall/paywalls-react — React 19 bindings for the Superwall Web SDK.
|
|
2
|
+
// See /Users/ianrumac/Workspace/Superwall/Superwall-Web/API.md §9.
|
|
3
|
+
export { SDK_VERSION } from "@superwall/paywalls-js";
|
|
4
|
+
export { SuperwallProvider, useSuperwallContext, } from "./provider.js";
|
|
5
|
+
export { useSuperwall, useSignal, useUser, usePlacement, useCustomPaywall, useSuperwallEvent, useDelegate, } from "./hooks.js";
|
|
6
|
+
export { SuperwallPaywall, } from "./SuperwallPaywall.js";
|
|
7
|
+
// Re-export everything public from paywalls-js so React consumers don't
|
|
8
|
+
// need to depend on both packages directly. Tree-shakeable per ESM rules.
|
|
9
|
+
export * from "@superwall/paywalls-js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type CreateSuperwallOptions, type Superwall } from "@superwall/paywalls-js";
|
|
3
|
+
/** Test-only — dispose every registered instance and clear the registry. */
|
|
4
|
+
export declare const _resetProviderRegistry: () => void;
|
|
5
|
+
export interface SuperwallProviderProps extends CreateSuperwallOptions {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export declare const SuperwallProvider: ({ children, ...opts }: SuperwallProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare const useSuperwallContext: () => Superwall;
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// `<SuperwallProvider>` — constructs a `Superwall` instance on mount and
|
|
3
|
+
// exposes it via React Context. Per API.md §9.1.
|
|
4
|
+
//
|
|
5
|
+
// HMR / Fast Refresh resilient (normative §9.1): the Provider holds the
|
|
6
|
+
// instance in a module-level registry keyed by `apiKey`. On unmount it
|
|
7
|
+
// disposes; on re-mount with the same `apiKey` (e.g. dev-server hot
|
|
8
|
+
// reload) it reuses the existing instance instead of building a new one,
|
|
9
|
+
// so iframes don't leak and event listeners don't stack.
|
|
10
|
+
//
|
|
11
|
+
// SSR-safe: on the server, `createSuperwall` is called with whatever
|
|
12
|
+
// identity seed the consumer passes; rendered children are eager (don't
|
|
13
|
+
// gate with `use(sw.ready)` server-side per API.md §9.2).
|
|
14
|
+
import { createContext, useContext, useMemo, } from "react";
|
|
15
|
+
import { createSuperwall, } from "@superwall/paywalls-js";
|
|
16
|
+
const SuperwallContext = createContext(null);
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// HMR-resilient registry — instances live for the page's lifetime.
|
|
19
|
+
//
|
|
20
|
+
// Production: a host app has one Provider per apiKey, mounted once, never
|
|
21
|
+
// unmounted until page navigation. Disposing on unmount is unnecessary
|
|
22
|
+
// and fights HMR (Fast Refresh tears down + re-mounts; the iframe and
|
|
23
|
+
// listeners would leak if we re-created the instance each time). Memory
|
|
24
|
+
// "leak" of one Superwall instance per apiKey for the page's lifetime is
|
|
25
|
+
// the intended trade-off.
|
|
26
|
+
//
|
|
27
|
+
// Tests use `_resetProviderRegistry()` between cases to avoid leakage.
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
const registry = new Map();
|
|
30
|
+
const acquire = (apiKey, opts) => {
|
|
31
|
+
const existing = registry.get(apiKey);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
const sw = createSuperwall(opts);
|
|
35
|
+
registry.set(apiKey, sw);
|
|
36
|
+
return sw;
|
|
37
|
+
};
|
|
38
|
+
/** Test-only — dispose every registered instance and clear the registry. */
|
|
39
|
+
export const _resetProviderRegistry = () => {
|
|
40
|
+
for (const sw of registry.values()) {
|
|
41
|
+
void sw.dispose();
|
|
42
|
+
}
|
|
43
|
+
registry.clear();
|
|
44
|
+
};
|
|
45
|
+
export const SuperwallProvider = ({ children, ...opts }) => {
|
|
46
|
+
// Acquire the registry-cached instance for this apiKey (first call
|
|
47
|
+
// creates; subsequent calls reuse — see registry comment above).
|
|
48
|
+
// `useMemo` keyed by apiKey is sufficient: changing `apiKey` swaps the
|
|
49
|
+
// context value to the next instance; other config props are ignored
|
|
50
|
+
// post-mount (config changes don't reconfigure the SDK in v0).
|
|
51
|
+
const sw = useMemo(() => acquire(opts.apiKey, opts),
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
[opts.apiKey]);
|
|
54
|
+
return (_jsx(SuperwallContext.Provider, { value: sw, children: children }));
|
|
55
|
+
};
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Context consumer
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
export const useSuperwallContext = () => {
|
|
60
|
+
const sw = useContext(SuperwallContext);
|
|
61
|
+
if (sw === null) {
|
|
62
|
+
throw new Error("useSuperwall (or its callers) must be used inside <SuperwallProvider>");
|
|
63
|
+
}
|
|
64
|
+
return sw;
|
|
65
|
+
};
|
package/package.json
CHANGED
|
@@ -1,29 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superwall/paywalls-react",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/superwall/Superwall-Web.git",
|
|
9
|
+
"directory": "packages/paywalls-react"
|
|
10
|
+
},
|
|
6
11
|
"publishConfig": {
|
|
7
12
|
"access": "public"
|
|
8
13
|
},
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"module": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
9
21
|
"exports": {
|
|
10
22
|
".": {
|
|
11
|
-
"
|
|
12
|
-
"
|
|
23
|
+
"@superwall/source": "./src/index.ts",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
13
27
|
}
|
|
14
28
|
},
|
|
15
29
|
"scripts": {
|
|
16
30
|
"test": "bun test",
|
|
17
31
|
"typecheck": "tsc --noEmit",
|
|
18
|
-
"build": "
|
|
19
|
-
"clean": "rm -rf node_modules .turbo *.tsbuildinfo"
|
|
32
|
+
"build": "bun run ../../scripts/build-package.ts",
|
|
33
|
+
"clean": "rm -rf dist node_modules .turbo *.tsbuildinfo"
|
|
20
34
|
},
|
|
21
35
|
"peerDependencies": {
|
|
22
36
|
"react": "^19.0.0",
|
|
23
|
-
"@superwall/paywalls-js": "^0.2.
|
|
37
|
+
"@superwall/paywalls-js": "^0.2.5"
|
|
24
38
|
},
|
|
25
39
|
"dependencies": {
|
|
26
|
-
"@superwall/paywalls-js": "^0.2.
|
|
40
|
+
"@superwall/paywalls-js": "^0.2.5"
|
|
27
41
|
},
|
|
28
42
|
"devDependencies": {
|
|
29
43
|
"@happy-dom/global-registrator": "^20.0.0",
|
package/.turbo/turbo-build.log
DELETED