@superwall/paywalls-react 0.1.5 → 0.2.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/README.md CHANGED
@@ -23,9 +23,29 @@ export function Root() {
23
23
  }
24
24
  ```
25
25
 
26
- `SuperwallProvider` takes the same config as `createSuperwall`
27
- (`apiKey`, `storage`, `delegate`, `identity`, `options`).
26
+ `SuperwallProvider` accepts the following props:
28
27
 
28
+ | Prop | Type | Description | Example |
29
+ |-----------|---------------------|------------------------------------------------------------------------------------------------------|---------------------------------------------|
30
+ | `apiKey` | `string` | **Required.** Your Superwall public key. | `"pk_your_public_key"` |
31
+ | `storage` | `StorageAdapter?` | Optional. Provide a storage layer to customize persistence behavior (defaults to localStorage). | `myCustomStorage` |
32
+ | `delegate`| `SuperwallDelegate?`| Optional. Receives paywall and subscription events (like callbacks for onPresent, onDismiss, etc). | `myDelegate` |
33
+ | `identity`| `{ appUserId?, aliasId?, vendorId? }?` | Optional. Seed identity on configure, e.g. from cookies on SSR hydration. | `{ appUserId: 'abc123' }` |
34
+ | `options` | `object?` | Optional. Additional options for advanced configuration (see the paywalls-js docs for details). | `{ networkEnvironment: "release" }` |
35
+
36
+ Most apps will only need to provide `apiKey`, but you can pass the optional extras as needed.
37
+
38
+ ```tsx
39
+ <SuperwallProvider
40
+ apiKey="pk_your_public_key"
41
+ storage={myCustomStorage}
42
+ delegate={myDelegate}
43
+ identity={{ appUserId: "123" }}
44
+ options={{ networkEnvironment: "release" }}
45
+ >
46
+ <App />
47
+ </SuperwallProvider>
48
+ ```
29
49
  ## Placements
30
50
 
31
51
  ```tsx
@@ -76,6 +96,25 @@ function GoPro() {
76
96
  - The placement has no audience match / holdout — feature runs without showing a paywall
77
97
  - The user completes a purchase or restore through the paywall
78
98
 
99
+ ## SuperwallPaywall component
100
+
101
+ Declarative alternative to `usePlacement`. Calls `register()` on mount and
102
+ renders `children` when the user is entitled — i.e. the feature block fires.
103
+
104
+ ```tsx
105
+ import { SuperwallPaywall } from "@superwall/paywalls-react";
106
+
107
+ function App() {
108
+ return (
109
+ <SuperwallPaywall placement="campaign_trigger" loading={<LoadingSpinner />}>
110
+ <ProContent />
111
+ </SuperwallPaywall>
112
+ );
113
+ }
114
+ ```
115
+
116
+ `loading` renders while the paywall is loading and is swapped out the moment it presents. `children` render once the user is entitled. Pass `inline` to mount the paywall iframe inside the component instead of as a full-viewport overlay. Optional handler props (`onPresent`, `onDismiss`, `onSkip`, `onError`) work the same as in `usePlacement`.
117
+
79
118
  ## Subscription status & user
80
119
 
81
120
  ```tsx
@@ -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
+ }
@@ -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
+ };
@@ -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;
@@ -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,26 +1,38 @@
1
1
  {
2
2
  "name": "@superwall/paywalls-react",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "sideEffects": false,
10
+ "main": "./dist/index.js",
11
+ "module": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
6
16
  "exports": {
7
17
  ".": {
8
- "types": "./src/index.ts",
9
- "default": "./src/index.ts"
18
+ "@superwall/source": "./src/index.ts",
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
10
22
  }
11
23
  },
12
24
  "scripts": {
13
25
  "test": "bun test",
14
26
  "typecheck": "tsc --noEmit",
15
- "build": "echo 'no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM'",
16
- "clean": "rm -rf node_modules .turbo *.tsbuildinfo"
27
+ "build": "bun run ../../scripts/build-package.ts",
28
+ "clean": "rm -rf dist node_modules .turbo *.tsbuildinfo"
17
29
  },
18
30
  "peerDependencies": {
19
31
  "react": "^19.0.0",
20
- "@superwall/paywalls-js": "^0.1.3"
32
+ "@superwall/paywalls-js": "^0.2.0"
21
33
  },
22
34
  "dependencies": {
23
- "@superwall/paywalls-js": "^0.1.3"
35
+ "@superwall/paywalls-js": "^0.2.0"
24
36
  },
25
37
  "devDependencies": {
26
38
  "@happy-dom/global-registrator": "^20.0.0",
@@ -1,3 +0,0 @@
1
-
2
- $ echo 'no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM'
3
- no-op for v0; consumers import .ts directly via Bun/Vite/Next ESM