@monetize.software/sdk-react 3.0.0-alpha.4 → 3.0.0-alpha.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/README.md CHANGED
@@ -1,13 +1,17 @@
1
1
  # @monetize.software/sdk-react
2
2
 
3
- React bindings для [`@monetize.software/sdk`](../sdk) — Provider, хуки и декларативные компоненты для пейвола. Работает с web SDK и extension SDK (любой drop-in-совместимый `PaywallUI`).
3
+ React bindings for [`@monetize.software/sdk`](../sdk) — Provider, hooks and
4
+ declarative paywall components. Works with the web SDK and the extension SDK
5
+ (any drop-in-compatible `PaywallUI`).
4
6
 
5
- - **Bundle**: < 2 KB gzip (только bindings, никакого UI он внутри SDK).
6
- - **React**: >= 18, использует `useSyncExternalStore` для concurrent-safe чтения снимков.
7
- - **SSR**: безопасно. На сервере хуки отдают `null` / `{ status: 'loading' }`, инстанс PaywallUI создаётся только на клиенте.
8
- - **TypeScript**: полный тип-уровень контракт ([`src/contract.ts`](src/contract.ts)) если в основном SDK поедет публичная поверхность, сборка sdk-react падает на этапе `tsc`.
7
+ - **Bundle**: < 2 KB gzip (bindings only — the UI lives inside the SDK).
8
+ - **React**: >= 18, uses `useSyncExternalStore` for concurrent-safe snapshot reads.
9
+ - **SSR**: safe out of the box. On the server, hooks return `null` /
10
+ `{ status: 'loading' }`; the `PaywallUI` instance is created only on the client.
11
+ - **TypeScript**: full type-level contract ([`src/contract.ts`](src/contract.ts)) —
12
+ if the public surface of the SDK shifts, the `sdk-react` build fails at `tsc`.
9
13
 
10
- ## Установка
14
+ ## Installation
11
15
 
12
16
  ```bash
13
17
  pnpm add @monetize.software/sdk-react @monetize.software/sdk react
@@ -25,7 +29,13 @@ import {
25
29
 
26
30
  function App() {
27
31
  return (
28
- <PaywallProvider options={{ paywallId: 'YOUR_ID', auth: true }}>
32
+ <PaywallProvider
33
+ options={{
34
+ paywallId: 'YOUR_ID',
35
+ apiOrigin: 'https://your-paywall-domain.com',
36
+ auth: true
37
+ }}
38
+ >
29
39
  <PaywallGate fallback={<UpgradeCTA />}>
30
40
  <PremiumFeature />
31
41
  </PaywallGate>
@@ -37,53 +47,59 @@ function App() {
37
47
 
38
48
  function UpgradeCTA() {
39
49
  const user = usePaywallUser();
40
- return <p>Привет, {user?.email ?? 'гость'}! Открой полный доступ.</p>;
50
+ return <p>Hi, {user?.email ?? 'guest'}! Unlock full access.</p>;
41
51
  }
42
52
  ```
43
53
 
54
+ `apiOrigin` must match the `custom_domain` configured for your paywall in the
55
+ platform.
56
+
44
57
  ## Provider
45
58
 
46
- `<PaywallProvider>` принимает один из двух пропсов:
59
+ `<PaywallProvider>` accepts one of two props:
47
60
 
48
61
  ```tsx
49
- // Вариант 1 — Provider сам создаёт инстанс
62
+ // Option 1 — Provider creates the instance itself
50
63
  <PaywallProvider options={{ paywallId, apiOrigin, auth: true }}>
51
64
 
52
- // Вариант 2 — готовый инстанс снаружи (extension / shared / тесты)
65
+ // Option 2 — host supplies a ready instance (extension / shared singleton / tests)
53
66
  import { createPaywallUI } from '@monetize.software/sdk-extension';
54
- const paywall = createPaywallUI({ paywallId });
67
+ const paywall = createPaywallUI({ paywallId, apiOrigin });
55
68
 
56
69
  <PaywallProvider instance={paywall}>
57
70
  ```
58
71
 
59
- Если `paywallId` динамически меняется, перемонтируй Provider через `<PaywallProvider key={paywallId} options={...}>` — реактивная пересборка опций намеренно не делается.
72
+ If `paywallId` changes dynamically, remount the Provider via
73
+ `<PaywallProvider key={paywallId} options={...}>` — reactive option rebuilds are
74
+ intentionally not performed.
60
75
 
61
- ## Хуки
76
+ ## Hooks
62
77
 
63
- | Хук | Возвращает | Когда триггерит rerender |
78
+ | Hook | Returns | When it triggers a rerender |
64
79
  |---|---|---|
65
- | `usePaywall()` | `PaywallUI \| null` | смена инстанса (редко) |
66
- | `usePaywallState()` | `{ open, view, error }` | любое изменение state-машины |
67
- | `usePaywallUser()` | `PaywallUser \| null` | event `userChange` |
80
+ | `usePaywall()` | `PaywallUI \| null` | instance change (rare) |
81
+ | `usePaywallState()` | `{ open, view, error }` | any state-machine change |
82
+ | `usePaywallUser()` | `PaywallUser \| null` | `userChange` event |
68
83
  | `usePaywallAccess(opts?)` | `{ status, result }` | `userChange` / `purchase_completed` |
69
84
  | `usePaywallPrices()` | `{ prices, loading, error }` | bootstrap refresh |
70
85
  | `usePaywallTrial()` | `TrialStatus \| null` | `trial_blocked` / `trial_expired` |
71
86
  | `usePaywallVisibility()` | `VisibilityStatus \| null` | `ready` / `visibility_blocked` |
72
- | `usePaywallEvent(event, handler)` | — | подписка с stable-handler-ref |
87
+ | `usePaywallEvent(event, handler)` | — | subscribes with a stable handler ref |
73
88
 
74
- Все хуки безопасны до mount-а Provider (отдают `null` / loading) — можно использовать в SSR без `'use client'`-обёрток на ветке дерева.
89
+ All hooks are safe before the Provider mounts (they return `null` / loading) —
90
+ you can use them in SSR without `'use client'` wrappers on the consuming subtree.
75
91
 
76
- ## Компоненты
92
+ ## Components
77
93
 
78
94
  ### `<PaywallGate>`
79
95
 
80
- Декларативный гейт: loading → fallback → children.
96
+ Declarative gate: loading → fallback → children.
81
97
 
82
98
  ```tsx
83
99
  <PaywallGate
84
100
  loading={<Skeleton />}
85
101
  fallback={({ open }) => <button onClick={open}>Upgrade</button>}
86
- openOnBlocked={false} // если true — автоматом дёргает paywall.open()
102
+ openOnBlocked={false} // if true — calls paywall.open() automatically
87
103
  >
88
104
  <PremiumFeature />
89
105
  </PaywallGate>
@@ -91,7 +107,9 @@ const paywall = createPaywallUI({ paywallId });
91
107
 
92
108
  ### `<PaywallButton>` / `<PaywallSupportButton>`
93
109
 
94
- Сахар над `paywall.open()`. По умолчанию рендерится как нативный `<button>` со всеми твоими `className`/`disabled`/`aria-*`. Для кастомного элемента — render-prop:
110
+ Sugar over `paywall.open()`. By default renders a native `<button>` with all
111
+ your `className`/`disabled`/`aria-*` props forwarded. For a custom element use
112
+ the render prop:
95
113
 
96
114
  ```tsx
97
115
  <PaywallButton render={({ open, ready }) => (
@@ -99,58 +117,72 @@ const paywall = createPaywallUI({ paywallId });
99
117
  )} />
100
118
  ```
101
119
 
102
- `mode` переключает между `open()` / `openSupport()` / `openAuth()` / `openAnonGate()`:
120
+ `mode` switches between `open()` / `openSupport()` / `openSignin()` / `openSignup()`:
103
121
 
104
122
  ```tsx
105
123
  <PaywallButton mode="support">Need help?</PaywallButton>
106
- <PaywallButton mode="auth">Sign in</PaywallButton>
124
+ <PaywallButton mode="signin">Sign in</PaywallButton>
125
+ <PaywallButton mode="signup">Create account</PaywallButton>
107
126
  ```
108
127
 
128
+ `mode="auth"` оставлен как алиас для `signin` (back-compat).
129
+
130
+ Для анонимного signin'а используй `usePaywall().signInAnonymously()` напрямую — он headless (без модалки), хост сам управляет loading-стейтом кнопки.
131
+
109
132
  ## SSR / Next.js
110
133
 
111
134
  ```tsx
112
- 'use client'; // на Provider, не на дерево потомков
135
+ 'use client'; // on the Provider, not on the consumer subtree
113
136
 
114
137
  import { PaywallProvider } from '@monetize.software/sdk-react';
115
138
 
116
139
  export function PaywallProviders({ children }) {
117
140
  return (
118
- <PaywallProvider options={{ paywallId: process.env.NEXT_PUBLIC_PAYWALL_ID! }}>
141
+ <PaywallProvider
142
+ options={{
143
+ paywallId: process.env.NEXT_PUBLIC_PAYWALL_ID!,
144
+ apiOrigin: process.env.NEXT_PUBLIC_PAYWALL_ORIGIN!
145
+ }}
146
+ >
119
147
  {children}
120
148
  </PaywallProvider>
121
149
  );
122
150
  }
123
151
  ```
124
152
 
125
- Хуки можно вызывать из server components только при типизированных-null-сценариях (всё равно вернётся `null`/`loading`). Рекомендация — выносить хук-логику в client component.
153
+ Hooks can be called from server components in typed-null scenarios (they'll
154
+ return `null` / loading anyway). The recommendation is to keep hook logic in a
155
+ client component.
126
156
 
127
- ## Защита от изменений в SDK
157
+ ## SDK contract guard
128
158
 
129
- `pnpm typecheck` проверяет [`src/contract.ts`](src/contract.ts) — там перечислены все точки опоры на public API SDK (методы PaywallUI, поля snapshot'ов, имена событий). Любое разъезжание в `../sdk` ловится здесь раньше, чем в проде.
159
+ `pnpm typecheck` validates [`src/contract.ts`](src/contract.ts) — it lists every
160
+ point of contact with the public SDK API (`PaywallUI` methods, snapshot fields,
161
+ event names). Any drift in `../sdk` is caught here before it hits production.
130
162
 
131
- После изменений в SDK обнови dist для типов:
163
+ After SDK changes, refresh the dist for type resolution:
132
164
 
133
165
  ```bash
134
166
  cd ../sdk && pnpm build
135
167
  cd ../sdk-react && pnpm typecheck
136
168
  ```
137
169
 
138
- ## Разработка
170
+ ## Development
139
171
 
140
172
  ```bash
141
173
  pnpm install
142
174
  pnpm dev # → http://localhost:5080/demo/
143
- pnpm typecheck # TS-валидация + контракт
175
+ pnpm typecheck # TS validation + contract guard
144
176
  pnpm test # vitest + @testing-library/react
145
- pnpm test:e2e # playwright против демо
177
+ pnpm test:e2e # playwright against the demo
146
178
  pnpm build # ESM + CJS + d.ts → dist/
147
179
  ```
148
180
 
149
181
  ## API reference
150
182
 
151
- Полные JSDoc-комментарии на каждый публичный экспорт смотри в исходниках:
183
+ Full JSDoc comments on every public export are inline in the sources:
152
184
 
153
185
  - [`src/PaywallProvider.tsx`](src/PaywallProvider.tsx) — Provider, lifecycle
154
- - [`src/hooks/`](src/hooks/) — все хуки
155
- - [`src/components/`](src/components/) — декларативные компоненты
156
- - [`src/contract.ts`](src/contract.ts) — точки опоры на SDK
186
+ - [`src/hooks/`](src/hooks/) — all hooks
187
+ - [`src/components/`](src/components/) — declarative components
188
+ - [`src/contract.ts`](src/contract.ts) — SDK contact points
@@ -7,8 +7,11 @@ import { OpenOptions } from '../../../sdk/src';
7
7
  */
8
8
  type OpenProps = OpenOptions;
9
9
  interface CommonProps extends OpenProps {
10
- /** Что открывать: layout (default), support, auth-gate, anon-gate. */
11
- mode?: 'paywall' | 'support' | 'auth' | 'anon';
10
+ /** Что открывать: layout (default), support, auth-gate (signin),
11
+ * signup-форма. 'auth' эквивалентен 'signin' (исторически openAuth
12
+ * дефолтит в signin-mode). Для анонимного signin используй
13
+ * `usePaywall().signInAnonymously()` напрямую — headless без модалки. */
14
+ mode?: 'paywall' | 'support' | 'auth' | 'signin' | 'signup';
12
15
  /** Render-prop для полного контроля над элементом-триггером. Когда задан,
13
16
  * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */
14
17
  render?: (args: PaywallButtonRenderArgs) => ReactElement;
@@ -1 +1 @@
1
- {"version":3,"file":"PaywallButton.d.ts","sourceRoot":"","sources":["../../src/components/PaywallButton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;GAIG;AACH,KAAK,SAAS,GAAG,WAAW,CAAC;AAE7B,UAAU,WAAY,SAAQ,SAAS;IACrC,sEAAsE;IACtE,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/C;+EAC2E;IAC3E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,YAAY,CAAC;CAC1D;AAED,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG,IAAI,CAC3B,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,SAAS,GAAG,UAAU,CAC7B,GAAG;IACF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,iBAAiB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,aAAa;eA9Bb,SAAS;qDAyFrB,CAAC"}
1
+ {"version":3,"file":"PaywallButton.d.ts","sourceRoot":"","sources":["../../src/components/PaywallButton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,oBAAoB,EACzB,KAAK,YAAY,EACjB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;GAIG;AACH,KAAK,SAAS,GAAG,WAAW,CAAC;AAE7B,UAAU,WAAY,SAAQ,SAAS;IACrC;;;8EAG0E;IAC1E,IAAI,CAAC,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC5D;+EAC2E;IAC3E,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,YAAY,CAAC;CAC1D;AAED,MAAM,WAAW,uBAAuB;IACtC,wDAAwD;IACxD,IAAI,EAAE,MAAM,IAAI,CAAC;IACjB,4EAA4E;IAC5E,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG,IAAI,CAC3B,oBAAoB,CAAC,iBAAiB,CAAC,EACvC,MAAM,SAAS,GAAG,UAAU,CAC7B,GAAG;IACF,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,WAAW,GAAG,iBAAiB,CAAC;AAEjE;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,aAAa;eA9Bb,SAAS;qDA0FrB,CAAC"}
package/dist/index.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const o=require("react/jsx-runtime"),l=require("react"),m=require("@monetize.software/sdk"),b=l.createContext(null);b.displayName="PaywallContext";const g=l.createContext(!1);g.displayName="PaywallProviderMarker";function A(e){const n="instance"in e?e.instance:void 0,t="options"in e?e.options:void 0,[r,a]=l.useState(n??null);return l.useEffect(()=>{if(n){a(n);return}if(!t)return;const s=new m.PaywallUI(t);return a(s),()=>{s.destroy(),a(null)}},[n]),o.jsx(g.Provider,{value:!0,children:o.jsx(b.Provider,{value:r,children:e.children})})}function i(){const e=l.useContext(g),n=l.useContext(b);if(!e)throw new Error("[sdk-react] usePaywall() called outside <PaywallProvider>. Wrap your tree with <PaywallProvider options={...}> or pass an externally-created instance via <PaywallProvider instance={paywall}>.");return n}const S={open:!1,view:null,error:null};function T(){const e=i(),n=l.useCallback(r=>e?e.onStateChange(r,{immediate:"none"}):()=>{},[e]),t=l.useCallback(()=>e?e.getState():S,[e]);return l.useSyncExternalStore(n,t,()=>S)}function j(){const e=i(),n=l.useCallback(r=>e?e.on("userChange",()=>r()):()=>{},[e]),t=l.useCallback(()=>e?e.billing.getCachedUser():null,[e]);return l.useSyncExternalStore(n,t,B)}function B(){return null}function V(e,n){const t=i(),r=l.useRef(n);r.current=n,l.useEffect(()=>{if(t)return t.on(e,a=>{r.current(a)})},[t,e])}const k={status:"loading",result:null};function C(e={}){const n=i(),[t,r]=l.useState(k),a=e.skipTrial===!0,s=e.skipVisibility===!0;return l.useEffect(()=>{if(!n){r(k);return}const c=new AbortController;let y=!1;const u=()=>{n.getAccess({skipTrial:a,skipVisibility:s,signal:c.signal}).then(P=>{y||c.signal.aborted||r({status:"ready",result:P})}).catch(()=>{})};u();const d=n.on("userChange",u),w=n.on("purchase_completed",u);return()=>{y=!0,c.abort(),d(),w()}},[n,a,s]),t}function R(){const e=i(),[n,t]=l.useState(()=>({prices:e?.getCachedPrices()??null,loading:!0,error:null}));return l.useEffect(()=>{if(!e){t({prices:null,loading:!0,error:null});return}const r=e.getCachedPrices();t({prices:r,loading:r===null,error:null});const a=new AbortController;let s=!1;(()=>{e.getPrices({signal:a.signal}).then(u=>{s||t({prices:u,loading:!1,error:null})}).catch(u=>{s||a.signal.aborted||t(d=>({prices:d.prices,loading:!1,error:u instanceof Error?u:new Error(String(u))}))})})();const y=e.on("ready",()=>{const u=e.getCachedPrices();u&&t({prices:u,loading:!1,error:null})});return()=>{s=!0,a.abort(),y()}},[e]),n}function O(){const e=i(),[n,t]=l.useState(()=>e?.getTrialStatus()??null),r=l.useCallback(()=>{if(!e){t(null);return}t(e.getTrialStatus())},[e]);return l.useEffect(()=>{if(!e){t(null);return}r();const a=e.on("trial_blocked",r),s=e.on("trial_expired",r);return()=>{a(),s()}},[e,r]),n}function _(){const e=i(),[n,t]=l.useState(()=>e?.getVisibility()??null),r=l.useCallback(()=>{if(!e){t(null);return}t(e.getVisibility())},[e]);return l.useEffect(()=>{if(!e){t(null);return}r();const a=e.on("ready",r),s=e.on("visibility_blocked",r);return()=>{a(),s()}},[e,r]),n}function U(e){const n=i(),t=C(),r=t.status==="ready"&&t.result.access==="blocked",a=e.openOnBlocked===!0&&r;if(l.useEffect(()=>{a&&n&&n.open()},[a,n]),t.status==="loading")return o.jsx(o.Fragment,{children:e.loading??null});if(t.result.access==="granted")return o.jsx(o.Fragment,{children:e.children});const s=e.fallback;return typeof s=="function"?o.jsx(o.Fragment,{children:s({result:t.result,open:()=>n?.open()})}):o.jsx(o.Fragment,{children:s??null})}const v=l.forwardRef(function(n,t){const r=i(),{mode:a="paywall",identity:s,renew:c,skipTrial:y,skipVisibility:u,render:d,onClick:w,disabled:P,...x}=n,p=r!==null,f={identity:s,renew:c,skipTrial:y,skipVisibility:u},h=()=>{if(r)switch(a){case"support":r.openSupport(f);return;case"auth":r.openAuth(f);return;case"anon":r.openAnonGate(f);return;default:r.open(f)}};return d?d({open:h,ready:p}):o.jsx("button",{ref:t,type:"button",disabled:P||!p,"aria-busy":p?void 0:!0,onClick:E=>{h(),w?.(E)},...x})}),F=l.forwardRef(function(n,t){return o.jsx(v,{...n,mode:"support",ref:t})});exports.PaywallButton=v;exports.PaywallGate=U;exports.PaywallProvider=A;exports.PaywallSupportButton=F;exports.usePaywall=i;exports.usePaywallAccess=C;exports.usePaywallEvent=V;exports.usePaywallPrices=R;exports.usePaywallState=T;exports.usePaywallTrial=O;exports.usePaywallUser=j;exports.usePaywallVisibility=_;
1
+ "use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const i=require("react/jsx-runtime"),l=require("react"),m=require("@monetize.software/sdk"),b=l.createContext(null);b.displayName="PaywallContext";const g=l.createContext(!1);g.displayName="PaywallProviderMarker";function T(e){const n="instance"in e?e.instance:void 0,t="options"in e?e.options:void 0,[r,a]=l.useState(n??null);return l.useEffect(()=>{if(n){a(n);return}if(!t)return;const s=new m.PaywallUI(t);return a(s),()=>{s.destroy(),a(null)}},[n]),i.jsx(g.Provider,{value:!0,children:i.jsx(b.Provider,{value:r,children:e.children})})}function o(){const e=l.useContext(g),n=l.useContext(b);if(!e)throw new Error("[sdk-react] usePaywall() called outside <PaywallProvider>. Wrap your tree with <PaywallProvider options={...}> or pass an externally-created instance via <PaywallProvider instance={paywall}>.");return n}const h={open:!1,view:null,error:null};function j(){const e=o(),n=l.useCallback(r=>e?e.onStateChange(r,{immediate:"none"}):()=>{},[e]),t=l.useCallback(()=>e?e.getState():h,[e]);return l.useSyncExternalStore(n,t,()=>h)}function B(){const e=o(),n=l.useCallback(r=>e?e.on("userChange",()=>r()):()=>{},[e]),t=l.useCallback(()=>e?e.billing.getCachedUser():null,[e]);return l.useSyncExternalStore(n,t,A)}function A(){return null}function V(e,n){const t=o(),r=l.useRef(n);r.current=n,l.useEffect(()=>{if(t)return t.on(e,a=>{r.current(a)})},[t,e])}const k={status:"loading",result:null};function C(e={}){const n=o(),[t,r]=l.useState(k),a=e.skipTrial===!0,s=e.skipVisibility===!0;return l.useEffect(()=>{if(!n){r(k);return}const c=new AbortController;let y=!1;const u=()=>{n.getAccess({skipTrial:a,skipVisibility:s,signal:c.signal}).then(p=>{y||c.signal.aborted||r({status:"ready",result:p})}).catch(()=>{})};u();const d=n.on("userChange",u),w=n.on("purchase_completed",u);return()=>{y=!0,c.abort(),d(),w()}},[n,a,s]),t}function R(){const e=o(),[n,t]=l.useState(()=>({prices:e?.getCachedPrices()??null,loading:!0,error:null}));return l.useEffect(()=>{if(!e){t({prices:null,loading:!0,error:null});return}const r=e.getCachedPrices();t({prices:r,loading:r===null,error:null});const a=new AbortController;let s=!1;(()=>{e.getPrices({signal:a.signal}).then(u=>{s||t({prices:u,loading:!1,error:null})}).catch(u=>{s||a.signal.aborted||t(d=>({prices:d.prices,loading:!1,error:u instanceof Error?u:new Error(String(u))}))})})();const y=e.on("ready",()=>{const u=e.getCachedPrices();u&&t({prices:u,loading:!1,error:null})});return()=>{s=!0,a.abort(),y()}},[e]),n}function O(){const e=o(),[n,t]=l.useState(()=>e?.getTrialStatus()??null),r=l.useCallback(()=>{if(!e){t(null);return}t(e.getTrialStatus())},[e]);return l.useEffect(()=>{if(!e){t(null);return}r();const a=e.on("trial_blocked",r),s=e.on("trial_expired",r);return()=>{a(),s()}},[e,r]),n}function _(){const e=o(),[n,t]=l.useState(()=>e?.getVisibility()??null),r=l.useCallback(()=>{if(!e){t(null);return}t(e.getVisibility())},[e]);return l.useEffect(()=>{if(!e){t(null);return}r();const a=e.on("ready",r),s=e.on("visibility_blocked",r);return()=>{a(),s()}},[e,r]),n}function U(e){const n=o(),t=C(),r=t.status==="ready"&&t.result.access==="blocked",a=e.openOnBlocked===!0&&r;if(l.useEffect(()=>{a&&n&&n.open()},[a,n]),t.status==="loading")return i.jsx(i.Fragment,{children:e.loading??null});if(t.result.access==="granted")return i.jsx(i.Fragment,{children:e.children});const s=e.fallback;return typeof s=="function"?i.jsx(i.Fragment,{children:s({result:t.result,open:()=>n?.open()})}):i.jsx(i.Fragment,{children:s??null})}const v=l.forwardRef(function(n,t){const r=o(),{mode:a="paywall",identity:s,renew:c,skipTrial:y,skipVisibility:u,render:d,onClick:w,disabled:p,...x}=n,P=r!==null,f={identity:s,renew:c,skipTrial:y,skipVisibility:u},S=()=>{if(r)switch(a){case"support":r.openSupport(f);return;case"auth":case"signin":r.openSignin(f);return;case"signup":r.openSignup(f);return;default:r.open(f)}};return d?d({open:S,ready:P}):i.jsx("button",{ref:t,type:"button",disabled:p||!P,"aria-busy":P?void 0:!0,onClick:E=>{S(),w?.(E)},...x})}),F=l.forwardRef(function(n,t){return i.jsx(v,{...n,mode:"support",ref:t})});exports.PaywallButton=v;exports.PaywallGate=U;exports.PaywallProvider=T;exports.PaywallSupportButton=F;exports.usePaywall=o;exports.usePaywallAccess=C;exports.usePaywallEvent=V;exports.usePaywallPrices=R;exports.usePaywallState=j;exports.usePaywallTrial=O;exports.usePaywallUser=B;exports.usePaywallVisibility=_;
2
2
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../src/context.ts","../src/PaywallProvider.tsx","../src/hooks/usePaywall.ts","../src/hooks/usePaywallState.ts","../src/hooks/usePaywallUser.ts","../src/hooks/usePaywallEvent.ts","../src/hooks/usePaywallAccess.ts","../src/hooks/usePaywallPrices.ts","../src/hooks/usePaywallTrial.ts","../src/hooks/usePaywallVisibility.ts","../src/components/PaywallGate.tsx","../src/components/PaywallButton.tsx","../src/components/PaywallSupportButton.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\n\n/**\n * Внутренний React Context, в который PaywallProvider кладёт PaywallUI-инстанс.\n *\n * value === null до того, как Provider успел смонтировать инстанс (SSR,\n * первый render до useEffect, дев double-mount в StrictMode после cleanup).\n * Хуки должны корректно обрабатывать null — отдавать loading/null/no-op,\n * а не падать.\n *\n * defaultValue intentionally `null`, а не `undefined` — это позволяет\n * usePaywall() различать «Provider не оборачивает дерево» (undefined-симуляция\n * через sentinel-объект ниже не нужна, мы это ловим иначе) и «Provider есть,\n * но инстанс ещё не создан» (null).\n */\nexport const PaywallContext = createContext<PaywallUI | null>(null);\nPaywallContext.displayName = 'PaywallContext';\n\n/**\n * Sentinel для отслеживания: «компонент вообще находится внутри Provider'а?».\n *\n * React Context отдаёт defaultValue, когда `<Provider>` не оборачивает дерево.\n * Если defaultValue=null, а Provider тоже легально кладёт null (на SSR /\n * до mount-а) — мы не различаем эти два случая. Поэтому Provider всегда\n * оборачивает второй Context с маркером HAS_PROVIDER=true, который usePaywall\n * проверяет первым.\n */\nexport const PaywallProviderMarker = createContext<boolean>(false);\nPaywallProviderMarker.displayName = 'PaywallProviderMarker';\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { PaywallUI, type PaywallUIOptions } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from './context';\n\n/**\n * Два взаимоисключающих режима использования:\n *\n * - `options` — Provider сам конструирует `PaywallUI` в useEffect и\n * прибирает в cleanup. Самый частый кейс — обычный сайт.\n * - `instance` — хост создаёт PaywallUI сам и передаёт готовым. Нужно для\n * extension'ов (`@monetize.software/sdk-extension` поставляет structurally\n * compatible PaywallUI с RemoteBillingClient), для shared-инстанса между\n * несколькими React-деревьями и для тестов.\n *\n * Discriminated union на уровне типов — TS не даст передать оба сразу.\n */\nexport type PaywallProviderProps =\n | {\n options: PaywallUIOptions;\n instance?: never;\n children: ReactNode;\n }\n | {\n instance: PaywallUI;\n options?: never;\n children: ReactNode;\n };\n\n/**\n * Корневой Provider для всех React-биндингов SDK.\n *\n * ```tsx\n * // вариант 1: Provider сам создаёт инстанс\n * <PaywallProvider options={{ paywallId: '...', auth: true }}>\n * <App />\n * </PaywallProvider>\n *\n * // вариант 2: готовый инстанс снаружи (extension / shared)\n * const paywall = createPaywallUI({ paywallId: '...' });\n * <PaywallProvider instance={paywall}>\n * <App />\n * </PaywallProvider>\n * ```\n *\n * SSR: инстанс создаётся в useEffect, на сервере context value=null. Все\n * хуки делают graceful fallback (`null` / `{ status: 'loading' }`), так что\n * Provider можно безопасно рендерить в Next.js / Remix без `'use client'`-\n * ограничений на дерево потомков.\n *\n * StrictMode: cleanup-эффект зовёт `destroy()`, чтобы dev double-mount не\n * оставлял утечек listener'ов и подписок. Микротик-эффекты PaywallUI-\n * конструктора (`autoDetectReturn`) на первом инстансе становятся no-op\n * после destroy.\n *\n * Смена `options` между рендерами: не реактивна — Provider создаёт инстанс\n * один раз. Если хосту реально нужно пересоздать (поменялся `paywallId`),\n * следует менять `key` у Provider'а — это идиоматичный React-способ форсить\n * пересоздание. Делать «умное» сравнение опций мы намеренно не пытаемся:\n * структурный equality глубоких options всегда ломается на функциях-колбеках\n * и live-обновлениях storage'а.\n */\nexport function PaywallProvider(props: PaywallProviderProps): JSX.Element {\n const externalInstance = 'instance' in props ? props.instance : undefined;\n const options = 'options' in props ? props.options : undefined;\n\n // Внешний инстанс → синхронно кладём его в state, чтобы первый render\n // потомков уже видел реальный PaywallUI (хосту он доступен мгновенно после\n // вызова createPaywallUI). Свой инстанс → null до useEffect, потому что\n // конструктор PaywallUI трогает window/queueMicrotask и не должен крутиться\n // на сервере.\n const [paywall, setPaywall] = useState<PaywallUI | null>(\n externalInstance ?? null\n );\n\n // Сам инстанс создаём в useEffect (только клиент). Если хост даёт готовый —\n // useEffect просто sync'ит state на случай, если ref поменялся между\n // рендерами без unmount'а.\n useEffect(() => {\n if (externalInstance) {\n setPaywall(externalInstance);\n // Externally-owned lifecycle — destroy() не наш.\n return;\n }\n\n if (!options) return;\n\n const created = new PaywallUI(options);\n setPaywall(created);\n return () => {\n created.destroy();\n // null на cleanup — потомки на следующем render'е увидят «инстанс ещё\n // не готов» вместо обращения к destroyed-объекту. В обычной жизни\n // unmount Provider'а сразу размонтирует и потомков, поэтому это\n // подстраховка для редких manual-remount-сценариев и StrictMode'а.\n setPaywall(null);\n };\n // options/instance меняются по reference. Реактивная пересборка инстанса\n // на каждый ре-рендер хост-компонента — не то, что нужно (см. JSDoc выше).\n // Для пересоздания используется React `key`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [externalInstance]);\n\n return (\n <PaywallProviderMarker.Provider value={true}>\n <PaywallContext.Provider value={paywall}>\n {props.children}\n </PaywallContext.Provider>\n </PaywallProviderMarker.Provider>\n );\n}\n","import { useContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from '../context';\n\n/**\n * Достаёт PaywallUI-инстанс из ближайшего {@link PaywallProvider}.\n *\n * Бросает ошибку, если вызван вне Provider'а — это явный программный баг,\n * не runtime-флоу. На SSR / до useEffect Provider'а возвращает `null`\n * (Provider есть, но инстанс ещё не смонтирован).\n *\n * Подавляющему большинству пейволов от хоста нужны `paywall.open()`,\n * `paywall.openSupport()`, подписки на события — для всего этого\n * usePaywall() самый прямой путь:\n *\n * ```tsx\n * const paywall = usePaywall();\n * <button onClick={() => paywall?.open()}>Upgrade</button>\n * ```\n *\n * Для типичных кейсов (gating, state-driven UI) обычно удобнее\n * специализированные хуки: {@link usePaywallState}, {@link usePaywallAccess},\n * {@link usePaywallUser}.\n */\nexport function usePaywall(): PaywallUI | null {\n const hasProvider = useContext(PaywallProviderMarker);\n const paywall = useContext(PaywallContext);\n\n if (!hasProvider) {\n throw new Error(\n '[sdk-react] usePaywall() called outside <PaywallProvider>. ' +\n 'Wrap your tree with <PaywallProvider options={...}> or pass an ' +\n 'externally-created instance via <PaywallProvider instance={paywall}>.'\n );\n }\n\n return paywall;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallStateSnapshot } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Зеркалит CLOSED_STATE из PaywallUI.ts. Хранится локально, чтобы getSnapshot\n// при paywall=null отдавал стабильную ссылку (та же ссылка между рендерами →\n// useSyncExternalStore не дёргает лишний re-render). Не экспортируется\n// наружу: для public API публичная форма доступна через usePaywallState().\n//\n// Shape проверяется в contract.ts — если PaywallStateSnapshot в SDK обзаведётся\n// новым полем, TS-build sdk-react падает раньше, чем кто-то заметит расхождение.\nconst SSR_SNAPSHOT: PaywallStateSnapshot = { open: false, view: null, error: null };\n\n/**\n * Подписка на состояние модалки пейвола: открыта/закрыта, текущий view,\n * последняя ошибка.\n *\n * Реализована поверх `paywall.onStateChange` + `paywall.getState` через\n * `useSyncExternalStore` — это даёт корректную concurrent-rendering семантику\n * (никаких tearing'ов, snapshot стабилен в рамках одного React-commit'а) и\n * минимум re-render'ов (snapshot равенство по `Object.is`).\n *\n * До mount-а Provider'а или на сервере возвращает `{ open: false, view: null,\n * error: null }` — это та же форма, что PaywallUI кладёт во внутренний\n * CLOSED_STATE, так что хосту не нужно отдельно проверять «инстанс готов».\n *\n * ```tsx\n * const { open, view } = usePaywallState();\n * useEffect(() => {\n * if (open) analytics.track('paywall_seen');\n * }, [open]);\n * ```\n */\nexport function usePaywallState(): PaywallStateSnapshot {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n // immediate: 'none' — useSyncExternalStore сам читает snapshot через\n // getSnapshot. Реплей initial-state'а через subscribe был бы лишним\n // вызовом cb, не приносящим новой информации.\n return paywall.onStateChange(cb, { immediate: 'none' });\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallStateSnapshot => {\n return paywall ? paywall.getState() : SSR_SNAPSHOT;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, () => SSR_SNAPSHOT);\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallUser } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * Подписка на текущего юзера пейвола (sync snapshot + автоматический ре-рендер\n * на любой userChange — bootstrap, /me refresh, после-checkout watcher).\n *\n * Возвращает `null` до первого ответа сети или когда инстанс ещё не готов\n * (SSR / до useEffect Provider'а / Provider не оборачивает дерево с инстансом).\n *\n * Удобно для подсветки текущего плана / e-mail юзера в собственном UI без\n * необходимости держать дублирующий state и руками подписываться на\n * `paywall.on('userChange', ...)`.\n *\n * ```tsx\n * const user = usePaywallUser();\n * if (user?.has_active_subscription) {\n * return <ProBadge plan={user.active_subscription?.plan_name} />;\n * }\n * ```\n *\n * Реализация поверх `paywall.on('userChange', cb)` + `billing.getCachedUser()`.\n * `paywall.on` не делает initial replay'я, поэтому useSyncExternalStore сам\n * читает старт-snapshot через getSnapshot — без лишних cb-вызовов.\n *\n * Ссылочная стабильность: BillingClient сравнивает user shape перед update'ом\n * (`sameUser`), так что между неизменными обновлениями `getCachedUser()`\n * возвращает ===-равный объект. Это гарантирует, что useSyncExternalStore\n * не дёргает ре-рендер при no-op refresh'ах.\n */\nexport function usePaywallUser(): PaywallUser | null {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n return paywall.on('userChange', () => cb());\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallUser | null => {\n return paywall ? paywall.billing.getCachedUser() : null;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction getServerSnapshot(): PaywallUser | null {\n return null;\n}\n","import { useEffect, useRef } from 'react';\nimport type { PaywallEvent, PaywallEventHandler } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Payload-тип конкретного события достаём через `Parameters<PaywallEventHandler<E>>[0]`,\n// потому что сам `PaywallEventPayloads` в SDK объявлен локально и не экспортируется.\n// Подход через `Parameters<>` устойчив к этому: пока `PaywallEventHandler` есть в\n// public surface, payload-тип SDK мы выводим корректно — TS-сборка sdk-react\n// упадёт, если сигнатура `PaywallEventHandler` поедет.\ntype EventPayload<E extends PaywallEvent> = Parameters<PaywallEventHandler<E>>[0];\n\n/**\n * Декларативная подписка на событие PaywallUI. Обёртка над `paywall.on(event, cb)`\n * с двумя важными отличиями от ручного useEffect:\n *\n * 1. handler не нужно мемоизировать через `useCallback` — внутри храним\n * последнюю версию в `useRef`, само subscription пересоздаётся только\n * при смене `event` или инстанса paywall'а. Это убирает класс багов с\n * «забыл useCallback → подписка отписывается-переподписывается на каждый\n * рендер → события теряются».\n *\n * 2. Корректно обрабатывает `paywall === null` (SSR / до Provider mount-а):\n * подписка просто не создаётся, ждёт пока инстанс появится.\n *\n * ```tsx\n * usePaywallEvent('purchase_completed', (payload) => {\n * toast.success(`Покупка ${payload.priceId} прошла`);\n * queryClient.invalidateQueries(['user']);\n * });\n * ```\n *\n * Для self-cleaning логики (host эмит'а аналитики, инвалидаций кешей, гидрации\n * локального стейта) это самый прямой паттерн — компонент гарантированно\n * отпишется при unmount'е, и не нужно держать unsub-ref'ы вручную.\n */\nexport function usePaywallEvent<E extends PaywallEvent>(\n event: E,\n handler: PaywallEventHandler<E>\n): void {\n const paywall = usePaywall();\n const handlerRef = useRef(handler);\n\n // Обновляем ref на каждом render'е — следующее срабатывание события\n // подхватит свежий handler. Без отдельного useEffect, потому что синхронный\n // assign в render-фазе для ref'а корректен и не нарушает rules-of-hooks.\n handlerRef.current = handler;\n\n useEffect(() => {\n if (!paywall) return;\n return paywall.on(event, (payload) => {\n // Cast необходим, потому что общий вариант `PaywallEventHandler` теряет\n // narrowing по `E`. handlerRef.current типизирован под конкретный E,\n // но `on()` принимает union — рантайм-shape гарантирован SDK'шным emit'ом.\n (handlerRef.current as (p: EventPayload<E>) => void)(payload);\n });\n }, [paywall, event]);\n}\n","import { useEffect, useState } from 'react';\nimport type {\n GetAccessOptions,\n PaywallAccessResult\n} from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `loading` — первый fetch ещё в полёте (или Provider не готов).\n * `ready` — есть свежий ответ; `result` гарантированно non-null.\n *\n * Сделано discriminated union'ом, чтобы хост мог сужать тип одним if-ом:\n *\n * `if (access.status === 'ready') access.result.access === 'granted'`\n */\nexport type PaywallAccessState =\n | { status: 'loading'; result: null }\n | { status: 'ready'; result: PaywallAccessResult };\n\nconst LOADING_STATE: PaywallAccessState = { status: 'loading', result: null };\n\n/**\n * Главный хук для гейтинга фич: «нужно ли блокировать фичу для этого юзера?».\n *\n * Под капотом — `paywall.getAccess(opts)` без side-effect'ов (модалка не\n * монтируется, trial-storage не двигается). На каждый `userChange` событие\n * автоматически рефетчится — после успешной покупки `has_subscription`\n * сработает мгновенно, и хост перерендерит UI без feature-lock'а.\n *\n * Bootstrap кешируется в BillingClient, так что usePaywallAccess можно дёргать\n * в любом компоненте дерева — сетевой запрос будет ровно один (или ни одного,\n * если кеш свежий).\n *\n * ```tsx\n * const access = usePaywallAccess();\n * const paywall = usePaywall();\n *\n * if (access.status === 'loading') return <Skeleton />;\n * if (access.result.access === 'blocked') {\n * return <button onClick={() => paywall?.open()}>Upgrade</button>;\n * }\n * return <PremiumFeature />;\n * ```\n *\n * Опции `opts` десериализуются по `skipTrial`/`skipVisibility` — стабильность\n * ссылки `opts` не требуется, эффект перезапустится только при реальном\n * изменении этих флагов. `signal` мы дропаем из deps (на каждый рендер у него\n * новый ref) — отмена inflight-запроса делается локально через AbortController\n * в cleanup-эффекте.\n */\nexport function usePaywallAccess(opts: GetAccessOptions = {}): PaywallAccessState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallAccessState>(LOADING_STATE);\n\n const skipTrial = opts.skipTrial === true;\n const skipVisibility = opts.skipVisibility === true;\n\n useEffect(() => {\n if (!paywall) {\n // Инстанс ушёл (Provider unmount / StrictMode cleanup) — честно\n // вернуть loading, чтобы хост не показывал устаревший result от\n // прошлого инстанса.\n setState(LOADING_STATE);\n return;\n }\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getAccess({ skipTrial, skipVisibility, signal: ctrl.signal })\n .then((result) => {\n if (cancelled || ctrl.signal.aborted) return;\n // Каждый refresh даёт новый объект — useState увидит !== и\n // ререндерит. Это ок: для гейтинга интерес представляет именно\n // `access` поле, остальное (visibility/trial snapshot'ы) — auxiliary\n // данные, которые не должны бы менять решение хоста на тех же входах.\n setState({ status: 'ready', result });\n })\n .catch(() => {\n // getAccess() имеет собственный offline-fallback и не throw'ит на\n // failed network'е — сюда мы попадаем только при abort'е, который\n // прилетает в cleanup-эффекте. Молча игнорим.\n });\n };\n\n refresh();\n\n // userChange покрывает оба источника обновления decision'а:\n // - после-checkout watcher эмит'ит userChange когда has_subscription=true\n // - manual /me refresh из хоста (paywall.billing.getUser())\n // Дополнительно слушаем purchase_completed для symmetric'ности — на\n // некоторых платежных провайдерах userChange может задержаться, а\n // purchase_completed летит мгновенно по URL-маркеру/postMessage.\n const unsubUser = paywall.on('userChange', refresh);\n const unsubPurchase = paywall.on('purchase_completed', refresh);\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsubUser();\n unsubPurchase();\n };\n }, [paywall, skipTrial, skipVisibility]);\n\n return state;\n}\n","import { useEffect, useState } from 'react';\nimport type { PaywallPrice } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `prices` — кешированный snapshot bootstrap.prices (`null` до первого fetch'а\n * или когда инстанс ещё не готов).\n * `loading` — true пока первый запрос в полёте, после первого ответа всегда false.\n * `error` — последняя ошибка fetch'а (`null` если успешный или ещё не падал).\n *\n * Намеренно нет дискриминирующего поля типа `status: 'loading'|'ready'|'error'`\n * как в `usePaywallAccess`, потому что для прайсингов хосту обычно нужны три\n * независимые величины одновременно (показать предыдущий список + skeleton +\n * сообщение об ошибке поверх) — discriminated union тут только усложняет.\n */\nexport interface PaywallPricesState {\n prices: PaywallPrice[] | null;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Загружает и подписывается на цены пейвола. Подходит для отдельной\n * прайсинг-страницы / pricing-карточек, где host хочет показать те же цены,\n * что и в модалке, без открытия paywall'а.\n *\n * Реализация:\n * - initial sync read через `getCachedPrices()` (если bootstrap уже в кеше\n * BillingClient'а — например, после `paywall.preload()` или предыдущего\n * open'а — цены доступны мгновенно);\n * - `useEffect` дёргает `getPrices()` для гарантированной загрузки;\n * - subscription на `ready` event — рефетч bootstrap'а на новом open()\n * может принести обновлённые цены, мы обновляем snapshot.\n *\n * ```tsx\n * const { prices, loading } = usePaywallPrices();\n * if (loading && !prices) return <Skeleton />;\n * return prices?.map((p) => <PriceCard key={p.id} price={p} />);\n * ```\n */\nexport function usePaywallPrices(): PaywallPricesState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallPricesState>(() => ({\n prices: paywall?.getCachedPrices() ?? null,\n loading: true,\n error: null\n }));\n\n useEffect(() => {\n if (!paywall) {\n setState({ prices: null, loading: true, error: null });\n return;\n }\n\n // Sync-доступ через cached snapshot — если bootstrap уже загружен,\n // показываем цены немедленно (без флеша «loading → ready»).\n const cached = paywall.getCachedPrices();\n setState({ prices: cached, loading: cached === null, error: null });\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getPrices({ signal: ctrl.signal })\n .then((prices) => {\n if (cancelled) return;\n setState({ prices, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (cancelled || ctrl.signal.aborted) return;\n setState((prev) => ({\n prices: prev.prices,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error))\n }));\n });\n };\n\n refresh();\n\n // `ready` event фаерится из открытого paywall'а с финальным bootstrap'ом —\n // если хост открыл/закрыл модалку, цены могли обновиться через\n // stale-while-revalidate. Слушаем чтобы в pricing-странице цифры не\n // расходились с тем, что юзер увидит в модалке.\n const unsub = paywall.on('ready', () => {\n const fresh = paywall.getCachedPrices();\n if (fresh) setState({ prices: fresh, loading: false, error: null });\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsub();\n };\n }, [paywall]);\n\n return state;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `TrialStatus` локально не экспортируется из SDK, но мы его получаем\n// через ReturnType-инференцию по публичному методу `getTrialStatus()`. Так\n// тип всегда совпадает с тем, что реально возвращает PaywallUI, без зависимости\n// от непубличного namespace'а SDK.\ntype TrialStatus = NonNullable<ReturnType<PaywallUI['getTrialStatus']>>;\n\n/**\n * Текущий статус триала ({@link TrialStatus}) с автоматическим ре-рендером на\n * `trial_blocked` события.\n *\n * Возвращает `null`, пока триал не проверялся (хост не вызывал\n * `paywall.open()` / `paywall.getAccess()`) либо триал отключён в конфиге\n * пейвола. Сам триал-стейт живёт в storage (localStorage / chrome.storage),\n * проверяется в `paywall.open()` и в `paywall.getAccess()` — оба пути обновляют\n * in-memory snapshot, который мы здесь и читаем.\n *\n * Использовать чтобы рисовать собственный UI:\n * - «У тебя осталось 3 показа» (mode `opens`) — `status.remainingActions`;\n * - «Триал истечёт через 2 часа» (mode `time`) — `status.remainingMs`;\n * - «Триал заблокирован, оплати чтобы продолжить» — `status.blocked === true`.\n *\n * ```tsx\n * const trial = usePaywallTrial();\n * if (trial?.mode === 'opens') {\n * return <Banner>Showings left: {trial.remainingActions}</Banner>;\n * }\n * ```\n */\nexport function usePaywallTrial(): TrialStatus | null {\n const paywall = usePaywall();\n const [status, setStatus] = useState<TrialStatus | null>(() =>\n paywall?.getTrialStatus() ?? null\n );\n\n // Стабильный refresh для эффекта — отдельная функция, чтобы deps массив\n // эффекта был чистым (`[paywall]`), без useCallback-цепочек.\n const sync = useCallback(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n setStatus(paywall.getTrialStatus());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n // Sync read на mount-е — getTrialStatus() мог обновиться между прошлым\n // рендером и effect'ом (например, hook вызван после первого open()-а).\n sync();\n\n // `trial_blocked` — единственный event, после которого snapshot реально\n // меняется. `trial_expired` фаерится один раз за жизнь инстанса и не\n // меняет shape статуса (статус становится `mode: 'none'` ИЛИ переходит\n // в un-blocked-режим, что и так читается через sync()).\n const unsubBlock = paywall.on('trial_blocked', sync);\n const unsubExpired = paywall.on('trial_expired', sync);\n\n return () => {\n unsubBlock();\n unsubExpired();\n };\n }, [paywall, sync]);\n\n return status;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `VisibilityStatus` локально не экспортируется из SDK — получаем через\n// ReturnType от публичного `getVisibility()`. См. usePaywallTrial для тех же\n// соображений.\ntype VisibilityStatus = NonNullable<ReturnType<PaywallUI['getVisibility']>>;\n\n/**\n * Server-computed visibility-снимок ({@link VisibilityStatus}): попадает ли\n * юзер в monetization-scope пейвола (страна, девайс, ручной visibility-флаг).\n *\n * Возвращает `null`, пока bootstrap не загружен или сервер не отдал\n * `settings.visibility` (старый online без targeting-патча).\n *\n * Использовать чтобы:\n * - показать собственный fallback («сервис недоступен в вашей стране») вместо\n * модалки, когда `visible === false`;\n * - залогировать impression для аналитики страны/tier'а юзера;\n * - принять решение какой CTA рисовать, не дёргая open() и не дожидаясь\n * visibility_blocked event.\n *\n * ```tsx\n * const visibility = usePaywallVisibility();\n * if (visibility && !visibility.visible) {\n * return <SoftBlock reason={visibility.reason} />;\n * }\n * ```\n */\nexport function usePaywallVisibility(): VisibilityStatus | null {\n const paywall = usePaywall();\n const [visibility, setVisibility] = useState<VisibilityStatus | null>(() =>\n paywall?.getVisibility() ?? null\n );\n\n const sync = useCallback(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n setVisibility(paywall.getVisibility());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n sync();\n\n // `ready` event летит после успешного bootstrap'а — там обновляется\n // `lastVisibility` в PaywallUI. `visibility_blocked` — когда блокировка\n // реально срабатывает на gate'е. Оба меняют snapshot.\n const unsubReady = paywall.on('ready', sync);\n const unsubBlocked = paywall.on('visibility_blocked', sync);\n\n return () => {\n unsubReady();\n unsubBlocked();\n };\n }, [paywall, sync]);\n\n return visibility;\n}\n","import { useEffect, type ReactNode } from 'react';\nimport type { PaywallAccessResult } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\nimport { usePaywallAccess } from '../hooks/usePaywallAccess';\n\nexport interface PaywallGateProps {\n /** Что показать, пока `getAccess()` не вернул ответ (initial fetch / Provider mount). */\n loading?: ReactNode;\n /**\n * Fallback для `blocked` ответа — обычно CTA «Upgrade». Принимает либо\n * статичный ReactNode, либо render-функцию, получающую callback\n * `open()` — удобно, чтобы кастомная кнопка сама дёргала модалку:\n *\n * ```tsx\n * fallback={({ open }) => <MyCTA onClick={open}>Upgrade</MyCTA>}\n * ```\n *\n * Если не передан — компонент рендерит `null` для blocked (host\n * полагается на `openOnBlocked` или ловит open() сам через `usePaywall`).\n */\n fallback?: ReactNode | ((args: BlockedRenderArgs) => ReactNode);\n /**\n * Автоматически дёргать `paywall.open()` сразу как только access перешёл в\n * blocked. Удобно для feature-разделителей вида «нажми и попадёшь на\n * paywall»: компонент сам открывает модалку, не нужно писать onClick.\n *\n * По умолчанию `false` — большинство хостов хотят сначала показать\n * объясняющий CTA, а модалку открывать по клику. Включать осознанно.\n */\n openOnBlocked?: boolean;\n /** Премиум-контент. Рендерится только когда access=granted. */\n children: ReactNode;\n}\n\nexport interface BlockedRenderArgs {\n result: Extract<PaywallAccessResult, { access: 'blocked' }>;\n open: () => void;\n}\n\n/**\n * Декларативная обёртка над {@link usePaywallAccess} + {@link usePaywall}.open().\n *\n * Три состояния:\n * - `loading` (первый fetch / Provider не готов) — рендерим `props.loading`;\n * - `granted` (есть подписка / visibility / trial) — рендерим `children`;\n * - `blocked` — рендерим `fallback` (если задан) и опционально дёргаем\n * `paywall.open()` при `openOnBlocked={true}`.\n *\n * ```tsx\n * <PaywallGate\n * loading={<Skeleton />}\n * fallback={({ open }) => <button onClick={open}>Upgrade</button>}\n * >\n * <PremiumFeature />\n * </PaywallGate>\n * ```\n *\n * Для нестандартных сценариев (показать \"Try free trial\" вместо upgrade,\n * комбинировать с собственным auth-flow'ом) использовать\n * {@link usePaywallAccess} напрямую — gate решает 80% кейсов, не пытаясь\n * стать конфигурируемым на каждый чих.\n */\nexport function PaywallGate(props: PaywallGateProps): JSX.Element | null {\n const paywall = usePaywall();\n const access = usePaywallAccess();\n\n // `openOnBlocked` — side-effect, поэтому в useEffect. Зависим от access\n // через идентификатор `result.access`, а не от объекта целиком, чтобы\n // не дёргать open() на каждом refresh-е getAccess'а с тем же blocked-итогом.\n const isBlocked =\n access.status === 'ready' && access.result.access === 'blocked';\n const shouldAutoOpen = props.openOnBlocked === true && isBlocked;\n\n useEffect(() => {\n if (shouldAutoOpen && paywall) paywall.open();\n }, [shouldAutoOpen, paywall]);\n\n if (access.status === 'loading') {\n return <>{props.loading ?? null}</>;\n }\n\n if (access.result.access === 'granted') {\n return <>{props.children}</>;\n }\n\n // blocked\n const fallback = props.fallback;\n if (typeof fallback === 'function') {\n return (\n <>\n {fallback({\n result: access.result,\n open: () => paywall?.open()\n })}\n </>\n );\n }\n return <>{fallback ?? null}</>;\n}\n","import {\n forwardRef,\n type ButtonHTMLAttributes,\n type ReactElement,\n type ReactNode\n} from 'react';\nimport type { OpenOptions } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\n\n/**\n * Параметры открытия пейвола, проксируются в `paywall.open(opts)`.\n * Любые поля {@link OpenOptions} применимы: `identity`, `renew`, `skipTrial`,\n * `skipVisibility`.\n */\ntype OpenProps = OpenOptions;\n\ninterface CommonProps extends OpenProps {\n /** Что открывать: layout (default), support, auth-gate, anon-gate. */\n mode?: 'paywall' | 'support' | 'auth' | 'anon';\n /** Render-prop для полного контроля над элементом-триггером. Когда задан,\n * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */\n render?: (args: PaywallButtonRenderArgs) => ReactElement;\n}\n\nexport interface PaywallButtonRenderArgs {\n /** Открыть пейвол согласно `mode` + переданным opts. */\n open: () => void;\n /** Готов ли инстанс PaywallUI. До mount-а Provider'а / на SSR — `false`. */\n ready: boolean;\n}\n\n/**\n * Props собственно `<button>`-рендера. Любые HTML-атрибуты — `disabled`,\n * `className`, `aria-label`, `type`, и т.д. — пробрасываются на нативный\n * элемент. `onClick` объединяется с нашим open()-хендлером (мы вызываем\n * наш первым, потом ваш — чтобы хост мог prevent'ить через event.preventDefault).\n */\ntype ButtonRenderProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof OpenProps | 'children'\n> & {\n children?: ReactNode;\n};\n\nexport type PaywallButtonProps = CommonProps & ButtonRenderProps;\n\n/**\n * Сахар над `usePaywall().open()`. Кнопка по умолчанию рендерится как\n * нативный `<button>` со всеми твоими className/style/disabled, но при нужде\n * можно передать `render` для произвольного элемента (Radix-style asChild\n * паттерн через render-prop).\n *\n * ```tsx\n * // обычный кейс\n * <PaywallButton className=\"btn-primary\" renew>\n * Renew subscription\n * </PaywallButton>\n *\n * // custom-элемент\n * <PaywallButton render={({ open, ready }) => (\n * <MyFancyButton onClick={open} disabled={!ready}>Upgrade</MyFancyButton>\n * )} />\n *\n * // саппорт-форма вместо тарифов\n * <PaywallButton mode=\"support\">Need help?</PaywallButton>\n * ```\n *\n * До mount-а Provider'а или на SSR кнопка рендерится с `disabled=true`\n * (через CSS-pseudo `[aria-busy]` хост может стилизовать loading-state) —\n * клик в этот момент no-op, потому что инстанса PaywallUI ещё нет.\n */\nexport const PaywallButton = forwardRef<HTMLButtonElement, PaywallButtonProps>(\n function PaywallButton(props, ref) {\n const paywall = usePaywall();\n const {\n mode = 'paywall',\n identity,\n renew,\n skipTrial,\n skipVisibility,\n render,\n onClick,\n disabled,\n ...buttonProps\n } = props;\n\n const ready = paywall !== null;\n\n const openOpts: OpenOptions = { identity, renew, skipTrial, skipVisibility };\n\n const open = (): void => {\n if (!paywall) return;\n switch (mode) {\n case 'support':\n paywall.openSupport(openOpts);\n return;\n case 'auth':\n paywall.openAuth(openOpts);\n return;\n case 'anon':\n paywall.openAnonGate(openOpts);\n return;\n default:\n paywall.open(openOpts);\n }\n };\n\n if (render) {\n return render({ open, ready });\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled || !ready}\n aria-busy={!ready ? true : undefined}\n onClick={(event) => {\n // Наш handler первым — host через event.preventDefault() ничего\n // не остановит, потому что open() уже стрельнул. Это намеренно:\n // открытие пейвола не должно зависеть от того, забыл ли хост\n // вернуть `false` из своего analytics-handler'а. Если нужен\n // префлайт-чек — паттерн через `render`-prop, там полный контроль.\n open();\n onClick?.(event);\n }}\n {...buttonProps}\n />\n );\n }\n);\n","import { forwardRef } from 'react';\nimport { PaywallButton, type PaywallButtonProps } from './PaywallButton';\n\nexport type PaywallSupportButtonProps = Omit<PaywallButtonProps, 'mode'>;\n\n/**\n * Сахар над `<PaywallButton mode=\"support\">`. Самостоятельная компонента, а\n * не пресет prop'а, для discoverability — название говорит за себя, и в\n * больших layout-ах легче видеть, где саппорт, а где основной upgrade-CTA.\n *\n * ```tsx\n * <PaywallSupportButton className=\"link\">Help</PaywallSupportButton>\n * ```\n */\nexport const PaywallSupportButton = forwardRef<\n HTMLButtonElement,\n PaywallSupportButtonProps\n>(function PaywallSupportButton(props, ref) {\n return <PaywallButton {...props} mode=\"support\" ref={ref} />;\n});\n"],"names":["PaywallContext","createContext","PaywallProviderMarker","PaywallProvider","props","externalInstance","options","paywall","setPaywall","useState","useEffect","created","PaywallUI","jsx","usePaywall","hasProvider","useContext","SSR_SNAPSHOT","usePaywallState","subscribe","useCallback","cb","getSnapshot","useSyncExternalStore","usePaywallUser","getServerSnapshot","usePaywallEvent","event","handler","handlerRef","useRef","payload","LOADING_STATE","usePaywallAccess","opts","state","setState","skipTrial","skipVisibility","ctrl","cancelled","refresh","result","unsubUser","unsubPurchase","usePaywallPrices","cached","prices","error","prev","unsub","fresh","usePaywallTrial","status","setStatus","sync","unsubBlock","unsubExpired","usePaywallVisibility","visibility","setVisibility","unsubReady","unsubBlocked","PaywallGate","access","isBlocked","shouldAutoOpen","Fragment","fallback","PaywallButton","forwardRef","ref","mode","identity","renew","render","onClick","disabled","buttonProps","ready","openOpts","open","PaywallSupportButton"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../src/context.ts","../src/PaywallProvider.tsx","../src/hooks/usePaywall.ts","../src/hooks/usePaywallState.ts","../src/hooks/usePaywallUser.ts","../src/hooks/usePaywallEvent.ts","../src/hooks/usePaywallAccess.ts","../src/hooks/usePaywallPrices.ts","../src/hooks/usePaywallTrial.ts","../src/hooks/usePaywallVisibility.ts","../src/components/PaywallGate.tsx","../src/components/PaywallButton.tsx","../src/components/PaywallSupportButton.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\n\n/**\n * Внутренний React Context, в который PaywallProvider кладёт PaywallUI-инстанс.\n *\n * value === null до того, как Provider успел смонтировать инстанс (SSR,\n * первый render до useEffect, дев double-mount в StrictMode после cleanup).\n * Хуки должны корректно обрабатывать null — отдавать loading/null/no-op,\n * а не падать.\n *\n * defaultValue intentionally `null`, а не `undefined` — это позволяет\n * usePaywall() различать «Provider не оборачивает дерево» (undefined-симуляция\n * через sentinel-объект ниже не нужна, мы это ловим иначе) и «Provider есть,\n * но инстанс ещё не создан» (null).\n */\nexport const PaywallContext = createContext<PaywallUI | null>(null);\nPaywallContext.displayName = 'PaywallContext';\n\n/**\n * Sentinel для отслеживания: «компонент вообще находится внутри Provider'а?».\n *\n * React Context отдаёт defaultValue, когда `<Provider>` не оборачивает дерево.\n * Если defaultValue=null, а Provider тоже легально кладёт null (на SSR /\n * до mount-а) — мы не различаем эти два случая. Поэтому Provider всегда\n * оборачивает второй Context с маркером HAS_PROVIDER=true, который usePaywall\n * проверяет первым.\n */\nexport const PaywallProviderMarker = createContext<boolean>(false);\nPaywallProviderMarker.displayName = 'PaywallProviderMarker';\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { PaywallUI, type PaywallUIOptions } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from './context';\n\n/**\n * Два взаимоисключающих режима использования:\n *\n * - `options` — Provider сам конструирует `PaywallUI` в useEffect и\n * прибирает в cleanup. Самый частый кейс — обычный сайт.\n * - `instance` — хост создаёт PaywallUI сам и передаёт готовым. Нужно для\n * extension'ов (`@monetize.software/sdk-extension` поставляет structurally\n * compatible PaywallUI с RemoteBillingClient), для shared-инстанса между\n * несколькими React-деревьями и для тестов.\n *\n * Discriminated union на уровне типов — TS не даст передать оба сразу.\n */\nexport type PaywallProviderProps =\n | {\n options: PaywallUIOptions;\n instance?: never;\n children: ReactNode;\n }\n | {\n instance: PaywallUI;\n options?: never;\n children: ReactNode;\n };\n\n/**\n * Корневой Provider для всех React-биндингов SDK.\n *\n * ```tsx\n * // вариант 1: Provider сам создаёт инстанс\n * <PaywallProvider options={{ paywallId: '...', auth: true }}>\n * <App />\n * </PaywallProvider>\n *\n * // вариант 2: готовый инстанс снаружи (extension / shared)\n * const paywall = createPaywallUI({ paywallId: '...' });\n * <PaywallProvider instance={paywall}>\n * <App />\n * </PaywallProvider>\n * ```\n *\n * SSR: инстанс создаётся в useEffect, на сервере context value=null. Все\n * хуки делают graceful fallback (`null` / `{ status: 'loading' }`), так что\n * Provider можно безопасно рендерить в Next.js / Remix без `'use client'`-\n * ограничений на дерево потомков.\n *\n * StrictMode: cleanup-эффект зовёт `destroy()`, чтобы dev double-mount не\n * оставлял утечек listener'ов и подписок. Микротик-эффекты PaywallUI-\n * конструктора (`autoDetectReturn`) на первом инстансе становятся no-op\n * после destroy.\n *\n * Смена `options` между рендерами: не реактивна — Provider создаёт инстанс\n * один раз. Если хосту реально нужно пересоздать (поменялся `paywallId`),\n * следует менять `key` у Provider'а — это идиоматичный React-способ форсить\n * пересоздание. Делать «умное» сравнение опций мы намеренно не пытаемся:\n * структурный equality глубоких options всегда ломается на функциях-колбеках\n * и live-обновлениях storage'а.\n */\nexport function PaywallProvider(props: PaywallProviderProps): JSX.Element {\n const externalInstance = 'instance' in props ? props.instance : undefined;\n const options = 'options' in props ? props.options : undefined;\n\n // Внешний инстанс → синхронно кладём его в state, чтобы первый render\n // потомков уже видел реальный PaywallUI (хосту он доступен мгновенно после\n // вызова createPaywallUI). Свой инстанс → null до useEffect, потому что\n // конструктор PaywallUI трогает window/queueMicrotask и не должен крутиться\n // на сервере.\n const [paywall, setPaywall] = useState<PaywallUI | null>(\n externalInstance ?? null\n );\n\n // Сам инстанс создаём в useEffect (только клиент). Если хост даёт готовый —\n // useEffect просто sync'ит state на случай, если ref поменялся между\n // рендерами без unmount'а.\n useEffect(() => {\n if (externalInstance) {\n setPaywall(externalInstance);\n // Externally-owned lifecycle — destroy() не наш.\n return;\n }\n\n if (!options) return;\n\n const created = new PaywallUI(options);\n setPaywall(created);\n return () => {\n created.destroy();\n // null на cleanup — потомки на следующем render'е увидят «инстанс ещё\n // не готов» вместо обращения к destroyed-объекту. В обычной жизни\n // unmount Provider'а сразу размонтирует и потомков, поэтому это\n // подстраховка для редких manual-remount-сценариев и StrictMode'а.\n setPaywall(null);\n };\n // options/instance меняются по reference. Реактивная пересборка инстанса\n // на каждый ре-рендер хост-компонента — не то, что нужно (см. JSDoc выше).\n // Для пересоздания используется React `key`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [externalInstance]);\n\n return (\n <PaywallProviderMarker.Provider value={true}>\n <PaywallContext.Provider value={paywall}>\n {props.children}\n </PaywallContext.Provider>\n </PaywallProviderMarker.Provider>\n );\n}\n","import { useContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from '../context';\n\n/**\n * Достаёт PaywallUI-инстанс из ближайшего {@link PaywallProvider}.\n *\n * Бросает ошибку, если вызван вне Provider'а — это явный программный баг,\n * не runtime-флоу. На SSR / до useEffect Provider'а возвращает `null`\n * (Provider есть, но инстанс ещё не смонтирован).\n *\n * Подавляющему большинству пейволов от хоста нужны `paywall.open()`,\n * `paywall.openSupport()`, подписки на события — для всего этого\n * usePaywall() самый прямой путь:\n *\n * ```tsx\n * const paywall = usePaywall();\n * <button onClick={() => paywall?.open()}>Upgrade</button>\n * ```\n *\n * Для типичных кейсов (gating, state-driven UI) обычно удобнее\n * специализированные хуки: {@link usePaywallState}, {@link usePaywallAccess},\n * {@link usePaywallUser}.\n */\nexport function usePaywall(): PaywallUI | null {\n const hasProvider = useContext(PaywallProviderMarker);\n const paywall = useContext(PaywallContext);\n\n if (!hasProvider) {\n throw new Error(\n '[sdk-react] usePaywall() called outside <PaywallProvider>. ' +\n 'Wrap your tree with <PaywallProvider options={...}> or pass an ' +\n 'externally-created instance via <PaywallProvider instance={paywall}>.'\n );\n }\n\n return paywall;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallStateSnapshot } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Зеркалит CLOSED_STATE из PaywallUI.ts. Хранится локально, чтобы getSnapshot\n// при paywall=null отдавал стабильную ссылку (та же ссылка между рендерами →\n// useSyncExternalStore не дёргает лишний re-render). Не экспортируется\n// наружу: для public API публичная форма доступна через usePaywallState().\n//\n// Shape проверяется в contract.ts — если PaywallStateSnapshot в SDK обзаведётся\n// новым полем, TS-build sdk-react падает раньше, чем кто-то заметит расхождение.\nconst SSR_SNAPSHOT: PaywallStateSnapshot = { open: false, view: null, error: null };\n\n/**\n * Подписка на состояние модалки пейвола: открыта/закрыта, текущий view,\n * последняя ошибка.\n *\n * Реализована поверх `paywall.onStateChange` + `paywall.getState` через\n * `useSyncExternalStore` — это даёт корректную concurrent-rendering семантику\n * (никаких tearing'ов, snapshot стабилен в рамках одного React-commit'а) и\n * минимум re-render'ов (snapshot равенство по `Object.is`).\n *\n * До mount-а Provider'а или на сервере возвращает `{ open: false, view: null,\n * error: null }` — это та же форма, что PaywallUI кладёт во внутренний\n * CLOSED_STATE, так что хосту не нужно отдельно проверять «инстанс готов».\n *\n * ```tsx\n * const { open, view } = usePaywallState();\n * useEffect(() => {\n * if (open) analytics.track('paywall_seen');\n * }, [open]);\n * ```\n */\nexport function usePaywallState(): PaywallStateSnapshot {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n // immediate: 'none' — useSyncExternalStore сам читает snapshot через\n // getSnapshot. Реплей initial-state'а через subscribe был бы лишним\n // вызовом cb, не приносящим новой информации.\n return paywall.onStateChange(cb, { immediate: 'none' });\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallStateSnapshot => {\n return paywall ? paywall.getState() : SSR_SNAPSHOT;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, () => SSR_SNAPSHOT);\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallUser } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * Подписка на текущего юзера пейвола (sync snapshot + автоматический ре-рендер\n * на любой userChange — bootstrap, /me refresh, после-checkout watcher).\n *\n * Возвращает `null` до первого ответа сети или когда инстанс ещё не готов\n * (SSR / до useEffect Provider'а / Provider не оборачивает дерево с инстансом).\n *\n * Удобно для подсветки текущего плана / e-mail юзера в собственном UI без\n * необходимости держать дублирующий state и руками подписываться на\n * `paywall.on('userChange', ...)`.\n *\n * ```tsx\n * const user = usePaywallUser();\n * if (user?.has_active_subscription) {\n * return <ProBadge plan={user.active_subscription?.plan_name} />;\n * }\n * ```\n *\n * Реализация поверх `paywall.on('userChange', cb)` + `billing.getCachedUser()`.\n * `paywall.on` не делает initial replay'я, поэтому useSyncExternalStore сам\n * читает старт-snapshot через getSnapshot — без лишних cb-вызовов.\n *\n * Ссылочная стабильность: BillingClient сравнивает user shape перед update'ом\n * (`sameUser`), так что между неизменными обновлениями `getCachedUser()`\n * возвращает ===-равный объект. Это гарантирует, что useSyncExternalStore\n * не дёргает ре-рендер при no-op refresh'ах.\n */\nexport function usePaywallUser(): PaywallUser | null {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n return paywall.on('userChange', () => cb());\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallUser | null => {\n return paywall ? paywall.billing.getCachedUser() : null;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction getServerSnapshot(): PaywallUser | null {\n return null;\n}\n","import { useEffect, useRef } from 'react';\nimport type { PaywallEvent, PaywallEventHandler } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Payload-тип конкретного события достаём через `Parameters<PaywallEventHandler<E>>[0]`,\n// потому что сам `PaywallEventPayloads` в SDK объявлен локально и не экспортируется.\n// Подход через `Parameters<>` устойчив к этому: пока `PaywallEventHandler` есть в\n// public surface, payload-тип SDK мы выводим корректно — TS-сборка sdk-react\n// упадёт, если сигнатура `PaywallEventHandler` поедет.\ntype EventPayload<E extends PaywallEvent> = Parameters<PaywallEventHandler<E>>[0];\n\n/**\n * Декларативная подписка на событие PaywallUI. Обёртка над `paywall.on(event, cb)`\n * с двумя важными отличиями от ручного useEffect:\n *\n * 1. handler не нужно мемоизировать через `useCallback` — внутри храним\n * последнюю версию в `useRef`, само subscription пересоздаётся только\n * при смене `event` или инстанса paywall'а. Это убирает класс багов с\n * «забыл useCallback → подписка отписывается-переподписывается на каждый\n * рендер → события теряются».\n *\n * 2. Корректно обрабатывает `paywall === null` (SSR / до Provider mount-а):\n * подписка просто не создаётся, ждёт пока инстанс появится.\n *\n * ```tsx\n * usePaywallEvent('purchase_completed', (payload) => {\n * toast.success(`Покупка ${payload.priceId} прошла`);\n * queryClient.invalidateQueries(['user']);\n * });\n * ```\n *\n * Для self-cleaning логики (host эмит'а аналитики, инвалидаций кешей, гидрации\n * локального стейта) это самый прямой паттерн — компонент гарантированно\n * отпишется при unmount'е, и не нужно держать unsub-ref'ы вручную.\n */\nexport function usePaywallEvent<E extends PaywallEvent>(\n event: E,\n handler: PaywallEventHandler<E>\n): void {\n const paywall = usePaywall();\n const handlerRef = useRef(handler);\n\n // Обновляем ref на каждом render'е — следующее срабатывание события\n // подхватит свежий handler. Без отдельного useEffect, потому что синхронный\n // assign в render-фазе для ref'а корректен и не нарушает rules-of-hooks.\n handlerRef.current = handler;\n\n useEffect(() => {\n if (!paywall) return;\n return paywall.on(event, (payload) => {\n // Cast необходим, потому что общий вариант `PaywallEventHandler` теряет\n // narrowing по `E`. handlerRef.current типизирован под конкретный E,\n // но `on()` принимает union — рантайм-shape гарантирован SDK'шным emit'ом.\n (handlerRef.current as (p: EventPayload<E>) => void)(payload);\n });\n }, [paywall, event]);\n}\n","import { useEffect, useState } from 'react';\nimport type {\n GetAccessOptions,\n PaywallAccessResult\n} from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `loading` — первый fetch ещё в полёте (или Provider не готов).\n * `ready` — есть свежий ответ; `result` гарантированно non-null.\n *\n * Сделано discriminated union'ом, чтобы хост мог сужать тип одним if-ом:\n *\n * `if (access.status === 'ready') access.result.access === 'granted'`\n */\nexport type PaywallAccessState =\n | { status: 'loading'; result: null }\n | { status: 'ready'; result: PaywallAccessResult };\n\nconst LOADING_STATE: PaywallAccessState = { status: 'loading', result: null };\n\n/**\n * Главный хук для гейтинга фич: «нужно ли блокировать фичу для этого юзера?».\n *\n * Под капотом — `paywall.getAccess(opts)` без side-effect'ов (модалка не\n * монтируется, trial-storage не двигается). На каждый `userChange` событие\n * автоматически рефетчится — после успешной покупки `has_subscription`\n * сработает мгновенно, и хост перерендерит UI без feature-lock'а.\n *\n * Bootstrap кешируется в BillingClient, так что usePaywallAccess можно дёргать\n * в любом компоненте дерева — сетевой запрос будет ровно один (или ни одного,\n * если кеш свежий).\n *\n * ```tsx\n * const access = usePaywallAccess();\n * const paywall = usePaywall();\n *\n * if (access.status === 'loading') return <Skeleton />;\n * if (access.result.access === 'blocked') {\n * return <button onClick={() => paywall?.open()}>Upgrade</button>;\n * }\n * return <PremiumFeature />;\n * ```\n *\n * Опции `opts` десериализуются по `skipTrial`/`skipVisibility` — стабильность\n * ссылки `opts` не требуется, эффект перезапустится только при реальном\n * изменении этих флагов. `signal` мы дропаем из deps (на каждый рендер у него\n * новый ref) — отмена inflight-запроса делается локально через AbortController\n * в cleanup-эффекте.\n */\nexport function usePaywallAccess(opts: GetAccessOptions = {}): PaywallAccessState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallAccessState>(LOADING_STATE);\n\n const skipTrial = opts.skipTrial === true;\n const skipVisibility = opts.skipVisibility === true;\n\n useEffect(() => {\n if (!paywall) {\n // Инстанс ушёл (Provider unmount / StrictMode cleanup) — честно\n // вернуть loading, чтобы хост не показывал устаревший result от\n // прошлого инстанса.\n setState(LOADING_STATE);\n return;\n }\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getAccess({ skipTrial, skipVisibility, signal: ctrl.signal })\n .then((result) => {\n if (cancelled || ctrl.signal.aborted) return;\n // Каждый refresh даёт новый объект — useState увидит !== и\n // ререндерит. Это ок: для гейтинга интерес представляет именно\n // `access` поле, остальное (visibility/trial snapshot'ы) — auxiliary\n // данные, которые не должны бы менять решение хоста на тех же входах.\n setState({ status: 'ready', result });\n })\n .catch(() => {\n // getAccess() имеет собственный offline-fallback и не throw'ит на\n // failed network'е — сюда мы попадаем только при abort'е, который\n // прилетает в cleanup-эффекте. Молча игнорим.\n });\n };\n\n refresh();\n\n // userChange покрывает оба источника обновления decision'а:\n // - после-checkout watcher эмит'ит userChange когда has_subscription=true\n // - manual /me refresh из хоста (paywall.billing.getUser())\n // Дополнительно слушаем purchase_completed для symmetric'ности — на\n // некоторых платежных провайдерах userChange может задержаться, а\n // purchase_completed летит мгновенно по URL-маркеру/postMessage.\n const unsubUser = paywall.on('userChange', refresh);\n const unsubPurchase = paywall.on('purchase_completed', refresh);\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsubUser();\n unsubPurchase();\n };\n }, [paywall, skipTrial, skipVisibility]);\n\n return state;\n}\n","import { useEffect, useState } from 'react';\nimport type { PaywallPrice } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `prices` — кешированный snapshot bootstrap.prices (`null` до первого fetch'а\n * или когда инстанс ещё не готов).\n * `loading` — true пока первый запрос в полёте, после первого ответа всегда false.\n * `error` — последняя ошибка fetch'а (`null` если успешный или ещё не падал).\n *\n * Намеренно нет дискриминирующего поля типа `status: 'loading'|'ready'|'error'`\n * как в `usePaywallAccess`, потому что для прайсингов хосту обычно нужны три\n * независимые величины одновременно (показать предыдущий список + skeleton +\n * сообщение об ошибке поверх) — discriminated union тут только усложняет.\n */\nexport interface PaywallPricesState {\n prices: PaywallPrice[] | null;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Загружает и подписывается на цены пейвола. Подходит для отдельной\n * прайсинг-страницы / pricing-карточек, где host хочет показать те же цены,\n * что и в модалке, без открытия paywall'а.\n *\n * Реализация:\n * - initial sync read через `getCachedPrices()` (если bootstrap уже в кеше\n * BillingClient'а — например, после `paywall.preload()` или предыдущего\n * open'а — цены доступны мгновенно);\n * - `useEffect` дёргает `getPrices()` для гарантированной загрузки;\n * - subscription на `ready` event — рефетч bootstrap'а на новом open()\n * может принести обновлённые цены, мы обновляем snapshot.\n *\n * ```tsx\n * const { prices, loading } = usePaywallPrices();\n * if (loading && !prices) return <Skeleton />;\n * return prices?.map((p) => <PriceCard key={p.id} price={p} />);\n * ```\n */\nexport function usePaywallPrices(): PaywallPricesState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallPricesState>(() => ({\n prices: paywall?.getCachedPrices() ?? null,\n loading: true,\n error: null\n }));\n\n useEffect(() => {\n if (!paywall) {\n setState({ prices: null, loading: true, error: null });\n return;\n }\n\n // Sync-доступ через cached snapshot — если bootstrap уже загружен,\n // показываем цены немедленно (без флеша «loading → ready»).\n const cached = paywall.getCachedPrices();\n setState({ prices: cached, loading: cached === null, error: null });\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getPrices({ signal: ctrl.signal })\n .then((prices) => {\n if (cancelled) return;\n setState({ prices, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (cancelled || ctrl.signal.aborted) return;\n setState((prev) => ({\n prices: prev.prices,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error))\n }));\n });\n };\n\n refresh();\n\n // `ready` event фаерится из открытого paywall'а с финальным bootstrap'ом —\n // если хост открыл/закрыл модалку, цены могли обновиться через\n // stale-while-revalidate. Слушаем чтобы в pricing-странице цифры не\n // расходились с тем, что юзер увидит в модалке.\n const unsub = paywall.on('ready', () => {\n const fresh = paywall.getCachedPrices();\n if (fresh) setState({ prices: fresh, loading: false, error: null });\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsub();\n };\n }, [paywall]);\n\n return state;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `TrialStatus` локально не экспортируется из SDK, но мы его получаем\n// через ReturnType-инференцию по публичному методу `getTrialStatus()`. Так\n// тип всегда совпадает с тем, что реально возвращает PaywallUI, без зависимости\n// от непубличного namespace'а SDK.\ntype TrialStatus = NonNullable<ReturnType<PaywallUI['getTrialStatus']>>;\n\n/**\n * Текущий статус триала ({@link TrialStatus}) с автоматическим ре-рендером на\n * `trial_blocked` события.\n *\n * Возвращает `null`, пока триал не проверялся (хост не вызывал\n * `paywall.open()` / `paywall.getAccess()`) либо триал отключён в конфиге\n * пейвола. Сам триал-стейт живёт в storage (localStorage / chrome.storage),\n * проверяется в `paywall.open()` и в `paywall.getAccess()` — оба пути обновляют\n * in-memory snapshot, который мы здесь и читаем.\n *\n * Использовать чтобы рисовать собственный UI:\n * - «У тебя осталось 3 показа» (mode `opens`) — `status.remainingActions`;\n * - «Триал истечёт через 2 часа» (mode `time`) — `status.remainingMs`;\n * - «Триал заблокирован, оплати чтобы продолжить» — `status.blocked === true`.\n *\n * ```tsx\n * const trial = usePaywallTrial();\n * if (trial?.mode === 'opens') {\n * return <Banner>Showings left: {trial.remainingActions}</Banner>;\n * }\n * ```\n */\nexport function usePaywallTrial(): TrialStatus | null {\n const paywall = usePaywall();\n const [status, setStatus] = useState<TrialStatus | null>(() =>\n paywall?.getTrialStatus() ?? null\n );\n\n // Стабильный refresh для эффекта — отдельная функция, чтобы deps массив\n // эффекта был чистым (`[paywall]`), без useCallback-цепочек.\n const sync = useCallback(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n setStatus(paywall.getTrialStatus());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n // Sync read на mount-е — getTrialStatus() мог обновиться между прошлым\n // рендером и effect'ом (например, hook вызван после первого open()-а).\n sync();\n\n // `trial_blocked` — единственный event, после которого snapshot реально\n // меняется. `trial_expired` фаерится один раз за жизнь инстанса и не\n // меняет shape статуса (статус становится `mode: 'none'` ИЛИ переходит\n // в un-blocked-режим, что и так читается через sync()).\n const unsubBlock = paywall.on('trial_blocked', sync);\n const unsubExpired = paywall.on('trial_expired', sync);\n\n return () => {\n unsubBlock();\n unsubExpired();\n };\n }, [paywall, sync]);\n\n return status;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `VisibilityStatus` локально не экспортируется из SDK — получаем через\n// ReturnType от публичного `getVisibility()`. См. usePaywallTrial для тех же\n// соображений.\ntype VisibilityStatus = NonNullable<ReturnType<PaywallUI['getVisibility']>>;\n\n/**\n * Server-computed visibility-снимок ({@link VisibilityStatus}): попадает ли\n * юзер в monetization-scope пейвола (страна, девайс, ручной visibility-флаг).\n *\n * Возвращает `null`, пока bootstrap не загружен или сервер не отдал\n * `settings.visibility` (старый online без targeting-патча).\n *\n * Использовать чтобы:\n * - показать собственный fallback («сервис недоступен в вашей стране») вместо\n * модалки, когда `visible === false`;\n * - залогировать impression для аналитики страны/tier'а юзера;\n * - принять решение какой CTA рисовать, не дёргая open() и не дожидаясь\n * visibility_blocked event.\n *\n * ```tsx\n * const visibility = usePaywallVisibility();\n * if (visibility && !visibility.visible) {\n * return <SoftBlock reason={visibility.reason} />;\n * }\n * ```\n */\nexport function usePaywallVisibility(): VisibilityStatus | null {\n const paywall = usePaywall();\n const [visibility, setVisibility] = useState<VisibilityStatus | null>(() =>\n paywall?.getVisibility() ?? null\n );\n\n const sync = useCallback(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n setVisibility(paywall.getVisibility());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n sync();\n\n // `ready` event летит после успешного bootstrap'а — там обновляется\n // `lastVisibility` в PaywallUI. `visibility_blocked` — когда блокировка\n // реально срабатывает на gate'е. Оба меняют snapshot.\n const unsubReady = paywall.on('ready', sync);\n const unsubBlocked = paywall.on('visibility_blocked', sync);\n\n return () => {\n unsubReady();\n unsubBlocked();\n };\n }, [paywall, sync]);\n\n return visibility;\n}\n","import { useEffect, type ReactNode } from 'react';\nimport type { PaywallAccessResult } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\nimport { usePaywallAccess } from '../hooks/usePaywallAccess';\n\nexport interface PaywallGateProps {\n /** Что показать, пока `getAccess()` не вернул ответ (initial fetch / Provider mount). */\n loading?: ReactNode;\n /**\n * Fallback для `blocked` ответа — обычно CTA «Upgrade». Принимает либо\n * статичный ReactNode, либо render-функцию, получающую callback\n * `open()` — удобно, чтобы кастомная кнопка сама дёргала модалку:\n *\n * ```tsx\n * fallback={({ open }) => <MyCTA onClick={open}>Upgrade</MyCTA>}\n * ```\n *\n * Если не передан — компонент рендерит `null` для blocked (host\n * полагается на `openOnBlocked` или ловит open() сам через `usePaywall`).\n */\n fallback?: ReactNode | ((args: BlockedRenderArgs) => ReactNode);\n /**\n * Автоматически дёргать `paywall.open()` сразу как только access перешёл в\n * blocked. Удобно для feature-разделителей вида «нажми и попадёшь на\n * paywall»: компонент сам открывает модалку, не нужно писать onClick.\n *\n * По умолчанию `false` — большинство хостов хотят сначала показать\n * объясняющий CTA, а модалку открывать по клику. Включать осознанно.\n */\n openOnBlocked?: boolean;\n /** Премиум-контент. Рендерится только когда access=granted. */\n children: ReactNode;\n}\n\nexport interface BlockedRenderArgs {\n result: Extract<PaywallAccessResult, { access: 'blocked' }>;\n open: () => void;\n}\n\n/**\n * Декларативная обёртка над {@link usePaywallAccess} + {@link usePaywall}.open().\n *\n * Три состояния:\n * - `loading` (первый fetch / Provider не готов) — рендерим `props.loading`;\n * - `granted` (есть подписка / visibility / trial) — рендерим `children`;\n * - `blocked` — рендерим `fallback` (если задан) и опционально дёргаем\n * `paywall.open()` при `openOnBlocked={true}`.\n *\n * ```tsx\n * <PaywallGate\n * loading={<Skeleton />}\n * fallback={({ open }) => <button onClick={open}>Upgrade</button>}\n * >\n * <PremiumFeature />\n * </PaywallGate>\n * ```\n *\n * Для нестандартных сценариев (показать \"Try free trial\" вместо upgrade,\n * комбинировать с собственным auth-flow'ом) использовать\n * {@link usePaywallAccess} напрямую — gate решает 80% кейсов, не пытаясь\n * стать конфигурируемым на каждый чих.\n */\nexport function PaywallGate(props: PaywallGateProps): JSX.Element | null {\n const paywall = usePaywall();\n const access = usePaywallAccess();\n\n // `openOnBlocked` — side-effect, поэтому в useEffect. Зависим от access\n // через идентификатор `result.access`, а не от объекта целиком, чтобы\n // не дёргать open() на каждом refresh-е getAccess'а с тем же blocked-итогом.\n const isBlocked =\n access.status === 'ready' && access.result.access === 'blocked';\n const shouldAutoOpen = props.openOnBlocked === true && isBlocked;\n\n useEffect(() => {\n if (shouldAutoOpen && paywall) paywall.open();\n }, [shouldAutoOpen, paywall]);\n\n if (access.status === 'loading') {\n return <>{props.loading ?? null}</>;\n }\n\n if (access.result.access === 'granted') {\n return <>{props.children}</>;\n }\n\n // blocked\n const fallback = props.fallback;\n if (typeof fallback === 'function') {\n return (\n <>\n {fallback({\n result: access.result,\n open: () => paywall?.open()\n })}\n </>\n );\n }\n return <>{fallback ?? null}</>;\n}\n","import {\n forwardRef,\n type ButtonHTMLAttributes,\n type ReactElement,\n type ReactNode\n} from 'react';\nimport type { OpenOptions } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\n\n/**\n * Параметры открытия пейвола, проксируются в `paywall.open(opts)`.\n * Любые поля {@link OpenOptions} применимы: `identity`, `renew`, `skipTrial`,\n * `skipVisibility`.\n */\ntype OpenProps = OpenOptions;\n\ninterface CommonProps extends OpenProps {\n /** Что открывать: layout (default), support, auth-gate (signin),\n * signup-форма. 'auth' эквивалентен 'signin' (исторически — openAuth\n * дефолтит в signin-mode). Для анонимного signin используй\n * `usePaywall().signInAnonymously()` напрямую — headless без модалки. */\n mode?: 'paywall' | 'support' | 'auth' | 'signin' | 'signup';\n /** Render-prop для полного контроля над элементом-триггером. Когда задан,\n * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */\n render?: (args: PaywallButtonRenderArgs) => ReactElement;\n}\n\nexport interface PaywallButtonRenderArgs {\n /** Открыть пейвол согласно `mode` + переданным opts. */\n open: () => void;\n /** Готов ли инстанс PaywallUI. До mount-а Provider'а / на SSR — `false`. */\n ready: boolean;\n}\n\n/**\n * Props собственно `<button>`-рендера. Любые HTML-атрибуты — `disabled`,\n * `className`, `aria-label`, `type`, и т.д. — пробрасываются на нативный\n * элемент. `onClick` объединяется с нашим open()-хендлером (мы вызываем\n * наш первым, потом ваш — чтобы хост мог prevent'ить через event.preventDefault).\n */\ntype ButtonRenderProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof OpenProps | 'children'\n> & {\n children?: ReactNode;\n};\n\nexport type PaywallButtonProps = CommonProps & ButtonRenderProps;\n\n/**\n * Сахар над `usePaywall().open()`. Кнопка по умолчанию рендерится как\n * нативный `<button>` со всеми твоими className/style/disabled, но при нужде\n * можно передать `render` для произвольного элемента (Radix-style asChild\n * паттерн через render-prop).\n *\n * ```tsx\n * // обычный кейс\n * <PaywallButton className=\"btn-primary\" renew>\n * Renew subscription\n * </PaywallButton>\n *\n * // custom-элемент\n * <PaywallButton render={({ open, ready }) => (\n * <MyFancyButton onClick={open} disabled={!ready}>Upgrade</MyFancyButton>\n * )} />\n *\n * // саппорт-форма вместо тарифов\n * <PaywallButton mode=\"support\">Need help?</PaywallButton>\n * ```\n *\n * До mount-а Provider'а или на SSR кнопка рендерится с `disabled=true`\n * (через CSS-pseudo `[aria-busy]` хост может стилизовать loading-state) —\n * клик в этот момент no-op, потому что инстанса PaywallUI ещё нет.\n */\nexport const PaywallButton = forwardRef<HTMLButtonElement, PaywallButtonProps>(\n function PaywallButton(props, ref) {\n const paywall = usePaywall();\n const {\n mode = 'paywall',\n identity,\n renew,\n skipTrial,\n skipVisibility,\n render,\n onClick,\n disabled,\n ...buttonProps\n } = props;\n\n const ready = paywall !== null;\n\n const openOpts: OpenOptions = { identity, renew, skipTrial, skipVisibility };\n\n const open = (): void => {\n if (!paywall) return;\n switch (mode) {\n case 'support':\n paywall.openSupport(openOpts);\n return;\n case 'auth':\n case 'signin':\n paywall.openSignin(openOpts);\n return;\n case 'signup':\n paywall.openSignup(openOpts);\n return;\n default:\n paywall.open(openOpts);\n }\n };\n\n if (render) {\n return render({ open, ready });\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled || !ready}\n aria-busy={!ready ? true : undefined}\n onClick={(event) => {\n // Наш handler первым — host через event.preventDefault() ничего\n // не остановит, потому что open() уже стрельнул. Это намеренно:\n // открытие пейвола не должно зависеть от того, забыл ли хост\n // вернуть `false` из своего analytics-handler'а. Если нужен\n // префлайт-чек — паттерн через `render`-prop, там полный контроль.\n open();\n onClick?.(event);\n }}\n {...buttonProps}\n />\n );\n }\n);\n","import { forwardRef } from 'react';\nimport { PaywallButton, type PaywallButtonProps } from './PaywallButton';\n\nexport type PaywallSupportButtonProps = Omit<PaywallButtonProps, 'mode'>;\n\n/**\n * Сахар над `<PaywallButton mode=\"support\">`. Самостоятельная компонента, а\n * не пресет prop'а, для discoverability — название говорит за себя, и в\n * больших layout-ах легче видеть, где саппорт, а где основной upgrade-CTA.\n *\n * ```tsx\n * <PaywallSupportButton className=\"link\">Help</PaywallSupportButton>\n * ```\n */\nexport const PaywallSupportButton = forwardRef<\n HTMLButtonElement,\n PaywallSupportButtonProps\n>(function PaywallSupportButton(props, ref) {\n return <PaywallButton {...props} mode=\"support\" ref={ref} />;\n});\n"],"names":["PaywallContext","createContext","PaywallProviderMarker","PaywallProvider","props","externalInstance","options","paywall","setPaywall","useState","useEffect","created","PaywallUI","jsx","usePaywall","hasProvider","useContext","SSR_SNAPSHOT","usePaywallState","subscribe","useCallback","cb","getSnapshot","useSyncExternalStore","usePaywallUser","getServerSnapshot","usePaywallEvent","event","handler","handlerRef","useRef","payload","LOADING_STATE","usePaywallAccess","opts","state","setState","skipTrial","skipVisibility","ctrl","cancelled","refresh","result","unsubUser","unsubPurchase","usePaywallPrices","cached","prices","error","prev","unsub","fresh","usePaywallTrial","status","setStatus","sync","unsubBlock","unsubExpired","usePaywallVisibility","visibility","setVisibility","unsubReady","unsubBlocked","PaywallGate","access","isBlocked","shouldAutoOpen","Fragment","fallback","PaywallButton","forwardRef","ref","mode","identity","renew","render","onClick","disabled","buttonProps","ready","openOpts","open","PaywallSupportButton"],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as i, Fragment as P } from "react/jsx-runtime";
3
- import { createContext as A, useState as p, useEffect as u, useContext as C, useCallback as f, useSyncExternalStore as T, useRef as _, forwardRef as B } from "react";
3
+ import { createContext as T, useState as p, useEffect as u, useContext as C, useCallback as f, useSyncExternalStore as A, useRef as _, forwardRef as B } from "react";
4
4
  import { PaywallUI as O } from "@monetize.software/sdk";
5
- const S = A(null);
5
+ const S = T(null);
6
6
  S.displayName = "PaywallContext";
7
- const k = A(!1);
7
+ const k = T(!1);
8
8
  k.displayName = "PaywallProviderMarker";
9
9
  function j(e) {
10
10
  const n = "instance" in e ? e.instance : void 0, t = "options" in e ? e.options : void 0, [r, l] = p(
@@ -16,9 +16,9 @@ function j(e) {
16
16
  return;
17
17
  }
18
18
  if (!t) return;
19
- const a = new O(t);
20
- return l(a), () => {
21
- a.destroy(), l(null);
19
+ const s = new O(t);
20
+ return l(s), () => {
21
+ s.destroy(), l(null);
22
22
  };
23
23
  }, [n]), /* @__PURE__ */ i(k.Provider, { value: !0, children: /* @__PURE__ */ i(S.Provider, { value: r, children: e.children }) });
24
24
  }
@@ -37,7 +37,7 @@ function D() {
37
37
  },
38
38
  [e]
39
39
  ), t = f(() => e ? e.getState() : m, [e]);
40
- return T(n, t, () => m);
40
+ return A(n, t, () => m);
41
41
  }
42
42
  function F() {
43
43
  const e = o(), n = f(
@@ -45,7 +45,7 @@ function F() {
45
45
  },
46
46
  [e]
47
47
  ), t = f(() => e ? e.billing.getCachedUser() : null, [e]);
48
- return T(n, t, R);
48
+ return A(n, t, R);
49
49
  }
50
50
  function R() {
51
51
  return null;
@@ -61,7 +61,7 @@ function H(e, n) {
61
61
  }
62
62
  const x = { status: "loading", result: null };
63
63
  function N(e = {}) {
64
- const n = o(), [t, r] = p(x), l = e.skipTrial === !0, a = e.skipVisibility === !0;
64
+ const n = o(), [t, r] = p(x), l = e.skipTrial === !0, s = e.skipVisibility === !0;
65
65
  return u(() => {
66
66
  if (!n) {
67
67
  r(x);
@@ -69,18 +69,18 @@ function N(e = {}) {
69
69
  }
70
70
  const c = new AbortController();
71
71
  let d = !1;
72
- const s = () => {
73
- n.getAccess({ skipTrial: l, skipVisibility: a, signal: c.signal }).then((g) => {
74
- d || c.signal.aborted || r({ status: "ready", result: g });
72
+ const a = () => {
73
+ n.getAccess({ skipTrial: l, skipVisibility: s, signal: c.signal }).then((b) => {
74
+ d || c.signal.aborted || r({ status: "ready", result: b });
75
75
  }).catch(() => {
76
76
  });
77
77
  };
78
- s();
79
- const y = n.on("userChange", s), b = n.on("purchase_completed", s);
78
+ a();
79
+ const y = n.on("userChange", a), g = n.on("purchase_completed", a);
80
80
  return () => {
81
- d = !0, c.abort(), y(), b();
81
+ d = !0, c.abort(), y(), g();
82
82
  };
83
- }, [n, l, a]), t;
83
+ }, [n, l, s]), t;
84
84
  }
85
85
  function L() {
86
86
  const e = o(), [n, t] = p(() => ({
@@ -96,24 +96,24 @@ function L() {
96
96
  const r = e.getCachedPrices();
97
97
  t({ prices: r, loading: r === null, error: null });
98
98
  const l = new AbortController();
99
- let a = !1;
99
+ let s = !1;
100
100
  (() => {
101
- e.getPrices({ signal: l.signal }).then((s) => {
102
- a || t({ prices: s, loading: !1, error: null });
103
- }).catch((s) => {
104
- a || l.signal.aborted || t((y) => ({
101
+ e.getPrices({ signal: l.signal }).then((a) => {
102
+ s || t({ prices: a, loading: !1, error: null });
103
+ }).catch((a) => {
104
+ s || l.signal.aborted || t((y) => ({
105
105
  prices: y.prices,
106
106
  loading: !1,
107
- error: s instanceof Error ? s : new Error(String(s))
107
+ error: a instanceof Error ? a : new Error(String(a))
108
108
  }));
109
109
  });
110
110
  })();
111
111
  const d = e.on("ready", () => {
112
- const s = e.getCachedPrices();
113
- s && t({ prices: s, loading: !1, error: null });
112
+ const a = e.getCachedPrices();
113
+ a && t({ prices: a, loading: !1, error: null });
114
114
  });
115
115
  return () => {
116
- a = !0, l.abort(), d();
116
+ s = !0, l.abort(), d();
117
117
  };
118
118
  }, [e]), n;
119
119
  }
@@ -133,9 +133,9 @@ function W() {
133
133
  return;
134
134
  }
135
135
  r();
136
- const l = e.on("trial_blocked", r), a = e.on("trial_expired", r);
136
+ const l = e.on("trial_blocked", r), s = e.on("trial_expired", r);
137
137
  return () => {
138
- l(), a();
138
+ l(), s();
139
139
  };
140
140
  }, [e, r]), n;
141
141
  }
@@ -155,9 +155,9 @@ function q() {
155
155
  return;
156
156
  }
157
157
  r();
158
- const l = e.on("ready", r), a = e.on("visibility_blocked", r);
158
+ const l = e.on("ready", r), s = e.on("visibility_blocked", r);
159
159
  return () => {
160
- l(), a();
160
+ l(), s();
161
161
  };
162
162
  }, [e, r]), n;
163
163
  }
@@ -169,35 +169,36 @@ function z(e) {
169
169
  return /* @__PURE__ */ i(P, { children: e.loading ?? null });
170
170
  if (t.result.access === "granted")
171
171
  return /* @__PURE__ */ i(P, { children: e.children });
172
- const a = e.fallback;
173
- return typeof a == "function" ? /* @__PURE__ */ i(P, { children: a({
172
+ const s = e.fallback;
173
+ return typeof s == "function" ? /* @__PURE__ */ i(P, { children: s({
174
174
  result: t.result,
175
175
  open: () => n?.open()
176
- }) }) : /* @__PURE__ */ i(P, { children: a ?? null });
176
+ }) }) : /* @__PURE__ */ i(P, { children: s ?? null });
177
177
  }
178
178
  const U = B(
179
179
  function(n, t) {
180
180
  const r = o(), {
181
181
  mode: l = "paywall",
182
- identity: a,
182
+ identity: s,
183
183
  renew: c,
184
184
  skipTrial: d,
185
- skipVisibility: s,
185
+ skipVisibility: a,
186
186
  render: y,
187
- onClick: b,
188
- disabled: g,
187
+ onClick: g,
188
+ disabled: b,
189
189
  ...E
190
- } = n, h = r !== null, w = { identity: a, renew: c, skipTrial: d, skipVisibility: s }, v = () => {
190
+ } = n, h = r !== null, w = { identity: s, renew: c, skipTrial: d, skipVisibility: a }, v = () => {
191
191
  if (r)
192
192
  switch (l) {
193
193
  case "support":
194
194
  r.openSupport(w);
195
195
  return;
196
196
  case "auth":
197
- r.openAuth(w);
197
+ case "signin":
198
+ r.openSignin(w);
198
199
  return;
199
- case "anon":
200
- r.openAnonGate(w);
200
+ case "signup":
201
+ r.openSignup(w);
201
202
  return;
202
203
  default:
203
204
  r.open(w);
@@ -208,10 +209,10 @@ const U = B(
208
209
  {
209
210
  ref: t,
210
211
  type: "button",
211
- disabled: g || !h,
212
+ disabled: b || !h,
212
213
  "aria-busy": h ? void 0 : !0,
213
214
  onClick: (V) => {
214
- v(), b?.(V);
215
+ v(), g?.(V);
215
216
  },
216
217
  ...E
217
218
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/context.ts","../src/PaywallProvider.tsx","../src/hooks/usePaywall.ts","../src/hooks/usePaywallState.ts","../src/hooks/usePaywallUser.ts","../src/hooks/usePaywallEvent.ts","../src/hooks/usePaywallAccess.ts","../src/hooks/usePaywallPrices.ts","../src/hooks/usePaywallTrial.ts","../src/hooks/usePaywallVisibility.ts","../src/components/PaywallGate.tsx","../src/components/PaywallButton.tsx","../src/components/PaywallSupportButton.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\n\n/**\n * Внутренний React Context, в который PaywallProvider кладёт PaywallUI-инстанс.\n *\n * value === null до того, как Provider успел смонтировать инстанс (SSR,\n * первый render до useEffect, дев double-mount в StrictMode после cleanup).\n * Хуки должны корректно обрабатывать null — отдавать loading/null/no-op,\n * а не падать.\n *\n * defaultValue intentionally `null`, а не `undefined` — это позволяет\n * usePaywall() различать «Provider не оборачивает дерево» (undefined-симуляция\n * через sentinel-объект ниже не нужна, мы это ловим иначе) и «Provider есть,\n * но инстанс ещё не создан» (null).\n */\nexport const PaywallContext = createContext<PaywallUI | null>(null);\nPaywallContext.displayName = 'PaywallContext';\n\n/**\n * Sentinel для отслеживания: «компонент вообще находится внутри Provider'а?».\n *\n * React Context отдаёт defaultValue, когда `<Provider>` не оборачивает дерево.\n * Если defaultValue=null, а Provider тоже легально кладёт null (на SSR /\n * до mount-а) — мы не различаем эти два случая. Поэтому Provider всегда\n * оборачивает второй Context с маркером HAS_PROVIDER=true, который usePaywall\n * проверяет первым.\n */\nexport const PaywallProviderMarker = createContext<boolean>(false);\nPaywallProviderMarker.displayName = 'PaywallProviderMarker';\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { PaywallUI, type PaywallUIOptions } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from './context';\n\n/**\n * Два взаимоисключающих режима использования:\n *\n * - `options` — Provider сам конструирует `PaywallUI` в useEffect и\n * прибирает в cleanup. Самый частый кейс — обычный сайт.\n * - `instance` — хост создаёт PaywallUI сам и передаёт готовым. Нужно для\n * extension'ов (`@monetize.software/sdk-extension` поставляет structurally\n * compatible PaywallUI с RemoteBillingClient), для shared-инстанса между\n * несколькими React-деревьями и для тестов.\n *\n * Discriminated union на уровне типов — TS не даст передать оба сразу.\n */\nexport type PaywallProviderProps =\n | {\n options: PaywallUIOptions;\n instance?: never;\n children: ReactNode;\n }\n | {\n instance: PaywallUI;\n options?: never;\n children: ReactNode;\n };\n\n/**\n * Корневой Provider для всех React-биндингов SDK.\n *\n * ```tsx\n * // вариант 1: Provider сам создаёт инстанс\n * <PaywallProvider options={{ paywallId: '...', auth: true }}>\n * <App />\n * </PaywallProvider>\n *\n * // вариант 2: готовый инстанс снаружи (extension / shared)\n * const paywall = createPaywallUI({ paywallId: '...' });\n * <PaywallProvider instance={paywall}>\n * <App />\n * </PaywallProvider>\n * ```\n *\n * SSR: инстанс создаётся в useEffect, на сервере context value=null. Все\n * хуки делают graceful fallback (`null` / `{ status: 'loading' }`), так что\n * Provider можно безопасно рендерить в Next.js / Remix без `'use client'`-\n * ограничений на дерево потомков.\n *\n * StrictMode: cleanup-эффект зовёт `destroy()`, чтобы dev double-mount не\n * оставлял утечек listener'ов и подписок. Микротик-эффекты PaywallUI-\n * конструктора (`autoDetectReturn`) на первом инстансе становятся no-op\n * после destroy.\n *\n * Смена `options` между рендерами: не реактивна — Provider создаёт инстанс\n * один раз. Если хосту реально нужно пересоздать (поменялся `paywallId`),\n * следует менять `key` у Provider'а — это идиоматичный React-способ форсить\n * пересоздание. Делать «умное» сравнение опций мы намеренно не пытаемся:\n * структурный equality глубоких options всегда ломается на функциях-колбеках\n * и live-обновлениях storage'а.\n */\nexport function PaywallProvider(props: PaywallProviderProps): JSX.Element {\n const externalInstance = 'instance' in props ? props.instance : undefined;\n const options = 'options' in props ? props.options : undefined;\n\n // Внешний инстанс → синхронно кладём его в state, чтобы первый render\n // потомков уже видел реальный PaywallUI (хосту он доступен мгновенно после\n // вызова createPaywallUI). Свой инстанс → null до useEffect, потому что\n // конструктор PaywallUI трогает window/queueMicrotask и не должен крутиться\n // на сервере.\n const [paywall, setPaywall] = useState<PaywallUI | null>(\n externalInstance ?? null\n );\n\n // Сам инстанс создаём в useEffect (только клиент). Если хост даёт готовый —\n // useEffect просто sync'ит state на случай, если ref поменялся между\n // рендерами без unmount'а.\n useEffect(() => {\n if (externalInstance) {\n setPaywall(externalInstance);\n // Externally-owned lifecycle — destroy() не наш.\n return;\n }\n\n if (!options) return;\n\n const created = new PaywallUI(options);\n setPaywall(created);\n return () => {\n created.destroy();\n // null на cleanup — потомки на следующем render'е увидят «инстанс ещё\n // не готов» вместо обращения к destroyed-объекту. В обычной жизни\n // unmount Provider'а сразу размонтирует и потомков, поэтому это\n // подстраховка для редких manual-remount-сценариев и StrictMode'а.\n setPaywall(null);\n };\n // options/instance меняются по reference. Реактивная пересборка инстанса\n // на каждый ре-рендер хост-компонента — не то, что нужно (см. JSDoc выше).\n // Для пересоздания используется React `key`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [externalInstance]);\n\n return (\n <PaywallProviderMarker.Provider value={true}>\n <PaywallContext.Provider value={paywall}>\n {props.children}\n </PaywallContext.Provider>\n </PaywallProviderMarker.Provider>\n );\n}\n","import { useContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from '../context';\n\n/**\n * Достаёт PaywallUI-инстанс из ближайшего {@link PaywallProvider}.\n *\n * Бросает ошибку, если вызван вне Provider'а — это явный программный баг,\n * не runtime-флоу. На SSR / до useEffect Provider'а возвращает `null`\n * (Provider есть, но инстанс ещё не смонтирован).\n *\n * Подавляющему большинству пейволов от хоста нужны `paywall.open()`,\n * `paywall.openSupport()`, подписки на события — для всего этого\n * usePaywall() самый прямой путь:\n *\n * ```tsx\n * const paywall = usePaywall();\n * <button onClick={() => paywall?.open()}>Upgrade</button>\n * ```\n *\n * Для типичных кейсов (gating, state-driven UI) обычно удобнее\n * специализированные хуки: {@link usePaywallState}, {@link usePaywallAccess},\n * {@link usePaywallUser}.\n */\nexport function usePaywall(): PaywallUI | null {\n const hasProvider = useContext(PaywallProviderMarker);\n const paywall = useContext(PaywallContext);\n\n if (!hasProvider) {\n throw new Error(\n '[sdk-react] usePaywall() called outside <PaywallProvider>. ' +\n 'Wrap your tree with <PaywallProvider options={...}> or pass an ' +\n 'externally-created instance via <PaywallProvider instance={paywall}>.'\n );\n }\n\n return paywall;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallStateSnapshot } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Зеркалит CLOSED_STATE из PaywallUI.ts. Хранится локально, чтобы getSnapshot\n// при paywall=null отдавал стабильную ссылку (та же ссылка между рендерами →\n// useSyncExternalStore не дёргает лишний re-render). Не экспортируется\n// наружу: для public API публичная форма доступна через usePaywallState().\n//\n// Shape проверяется в contract.ts — если PaywallStateSnapshot в SDK обзаведётся\n// новым полем, TS-build sdk-react падает раньше, чем кто-то заметит расхождение.\nconst SSR_SNAPSHOT: PaywallStateSnapshot = { open: false, view: null, error: null };\n\n/**\n * Подписка на состояние модалки пейвола: открыта/закрыта, текущий view,\n * последняя ошибка.\n *\n * Реализована поверх `paywall.onStateChange` + `paywall.getState` через\n * `useSyncExternalStore` — это даёт корректную concurrent-rendering семантику\n * (никаких tearing'ов, snapshot стабилен в рамках одного React-commit'а) и\n * минимум re-render'ов (snapshot равенство по `Object.is`).\n *\n * До mount-а Provider'а или на сервере возвращает `{ open: false, view: null,\n * error: null }` — это та же форма, что PaywallUI кладёт во внутренний\n * CLOSED_STATE, так что хосту не нужно отдельно проверять «инстанс готов».\n *\n * ```tsx\n * const { open, view } = usePaywallState();\n * useEffect(() => {\n * if (open) analytics.track('paywall_seen');\n * }, [open]);\n * ```\n */\nexport function usePaywallState(): PaywallStateSnapshot {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n // immediate: 'none' — useSyncExternalStore сам читает snapshot через\n // getSnapshot. Реплей initial-state'а через subscribe был бы лишним\n // вызовом cb, не приносящим новой информации.\n return paywall.onStateChange(cb, { immediate: 'none' });\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallStateSnapshot => {\n return paywall ? paywall.getState() : SSR_SNAPSHOT;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, () => SSR_SNAPSHOT);\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallUser } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * Подписка на текущего юзера пейвола (sync snapshot + автоматический ре-рендер\n * на любой userChange — bootstrap, /me refresh, после-checkout watcher).\n *\n * Возвращает `null` до первого ответа сети или когда инстанс ещё не готов\n * (SSR / до useEffect Provider'а / Provider не оборачивает дерево с инстансом).\n *\n * Удобно для подсветки текущего плана / e-mail юзера в собственном UI без\n * необходимости держать дублирующий state и руками подписываться на\n * `paywall.on('userChange', ...)`.\n *\n * ```tsx\n * const user = usePaywallUser();\n * if (user?.has_active_subscription) {\n * return <ProBadge plan={user.active_subscription?.plan_name} />;\n * }\n * ```\n *\n * Реализация поверх `paywall.on('userChange', cb)` + `billing.getCachedUser()`.\n * `paywall.on` не делает initial replay'я, поэтому useSyncExternalStore сам\n * читает старт-snapshot через getSnapshot — без лишних cb-вызовов.\n *\n * Ссылочная стабильность: BillingClient сравнивает user shape перед update'ом\n * (`sameUser`), так что между неизменными обновлениями `getCachedUser()`\n * возвращает ===-равный объект. Это гарантирует, что useSyncExternalStore\n * не дёргает ре-рендер при no-op refresh'ах.\n */\nexport function usePaywallUser(): PaywallUser | null {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n return paywall.on('userChange', () => cb());\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallUser | null => {\n return paywall ? paywall.billing.getCachedUser() : null;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction getServerSnapshot(): PaywallUser | null {\n return null;\n}\n","import { useEffect, useRef } from 'react';\nimport type { PaywallEvent, PaywallEventHandler } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Payload-тип конкретного события достаём через `Parameters<PaywallEventHandler<E>>[0]`,\n// потому что сам `PaywallEventPayloads` в SDK объявлен локально и не экспортируется.\n// Подход через `Parameters<>` устойчив к этому: пока `PaywallEventHandler` есть в\n// public surface, payload-тип SDK мы выводим корректно — TS-сборка sdk-react\n// упадёт, если сигнатура `PaywallEventHandler` поедет.\ntype EventPayload<E extends PaywallEvent> = Parameters<PaywallEventHandler<E>>[0];\n\n/**\n * Декларативная подписка на событие PaywallUI. Обёртка над `paywall.on(event, cb)`\n * с двумя важными отличиями от ручного useEffect:\n *\n * 1. handler не нужно мемоизировать через `useCallback` — внутри храним\n * последнюю версию в `useRef`, само subscription пересоздаётся только\n * при смене `event` или инстанса paywall'а. Это убирает класс багов с\n * «забыл useCallback → подписка отписывается-переподписывается на каждый\n * рендер → события теряются».\n *\n * 2. Корректно обрабатывает `paywall === null` (SSR / до Provider mount-а):\n * подписка просто не создаётся, ждёт пока инстанс появится.\n *\n * ```tsx\n * usePaywallEvent('purchase_completed', (payload) => {\n * toast.success(`Покупка ${payload.priceId} прошла`);\n * queryClient.invalidateQueries(['user']);\n * });\n * ```\n *\n * Для self-cleaning логики (host эмит'а аналитики, инвалидаций кешей, гидрации\n * локального стейта) это самый прямой паттерн — компонент гарантированно\n * отпишется при unmount'е, и не нужно держать unsub-ref'ы вручную.\n */\nexport function usePaywallEvent<E extends PaywallEvent>(\n event: E,\n handler: PaywallEventHandler<E>\n): void {\n const paywall = usePaywall();\n const handlerRef = useRef(handler);\n\n // Обновляем ref на каждом render'е — следующее срабатывание события\n // подхватит свежий handler. Без отдельного useEffect, потому что синхронный\n // assign в render-фазе для ref'а корректен и не нарушает rules-of-hooks.\n handlerRef.current = handler;\n\n useEffect(() => {\n if (!paywall) return;\n return paywall.on(event, (payload) => {\n // Cast необходим, потому что общий вариант `PaywallEventHandler` теряет\n // narrowing по `E`. handlerRef.current типизирован под конкретный E,\n // но `on()` принимает union — рантайм-shape гарантирован SDK'шным emit'ом.\n (handlerRef.current as (p: EventPayload<E>) => void)(payload);\n });\n }, [paywall, event]);\n}\n","import { useEffect, useState } from 'react';\nimport type {\n GetAccessOptions,\n PaywallAccessResult\n} from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `loading` — первый fetch ещё в полёте (или Provider не готов).\n * `ready` — есть свежий ответ; `result` гарантированно non-null.\n *\n * Сделано discriminated union'ом, чтобы хост мог сужать тип одним if-ом:\n *\n * `if (access.status === 'ready') access.result.access === 'granted'`\n */\nexport type PaywallAccessState =\n | { status: 'loading'; result: null }\n | { status: 'ready'; result: PaywallAccessResult };\n\nconst LOADING_STATE: PaywallAccessState = { status: 'loading', result: null };\n\n/**\n * Главный хук для гейтинга фич: «нужно ли блокировать фичу для этого юзера?».\n *\n * Под капотом — `paywall.getAccess(opts)` без side-effect'ов (модалка не\n * монтируется, trial-storage не двигается). На каждый `userChange` событие\n * автоматически рефетчится — после успешной покупки `has_subscription`\n * сработает мгновенно, и хост перерендерит UI без feature-lock'а.\n *\n * Bootstrap кешируется в BillingClient, так что usePaywallAccess можно дёргать\n * в любом компоненте дерева — сетевой запрос будет ровно один (или ни одного,\n * если кеш свежий).\n *\n * ```tsx\n * const access = usePaywallAccess();\n * const paywall = usePaywall();\n *\n * if (access.status === 'loading') return <Skeleton />;\n * if (access.result.access === 'blocked') {\n * return <button onClick={() => paywall?.open()}>Upgrade</button>;\n * }\n * return <PremiumFeature />;\n * ```\n *\n * Опции `opts` десериализуются по `skipTrial`/`skipVisibility` — стабильность\n * ссылки `opts` не требуется, эффект перезапустится только при реальном\n * изменении этих флагов. `signal` мы дропаем из deps (на каждый рендер у него\n * новый ref) — отмена inflight-запроса делается локально через AbortController\n * в cleanup-эффекте.\n */\nexport function usePaywallAccess(opts: GetAccessOptions = {}): PaywallAccessState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallAccessState>(LOADING_STATE);\n\n const skipTrial = opts.skipTrial === true;\n const skipVisibility = opts.skipVisibility === true;\n\n useEffect(() => {\n if (!paywall) {\n // Инстанс ушёл (Provider unmount / StrictMode cleanup) — честно\n // вернуть loading, чтобы хост не показывал устаревший result от\n // прошлого инстанса.\n setState(LOADING_STATE);\n return;\n }\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getAccess({ skipTrial, skipVisibility, signal: ctrl.signal })\n .then((result) => {\n if (cancelled || ctrl.signal.aborted) return;\n // Каждый refresh даёт новый объект — useState увидит !== и\n // ререндерит. Это ок: для гейтинга интерес представляет именно\n // `access` поле, остальное (visibility/trial snapshot'ы) — auxiliary\n // данные, которые не должны бы менять решение хоста на тех же входах.\n setState({ status: 'ready', result });\n })\n .catch(() => {\n // getAccess() имеет собственный offline-fallback и не throw'ит на\n // failed network'е — сюда мы попадаем только при abort'е, который\n // прилетает в cleanup-эффекте. Молча игнорим.\n });\n };\n\n refresh();\n\n // userChange покрывает оба источника обновления decision'а:\n // - после-checkout watcher эмит'ит userChange когда has_subscription=true\n // - manual /me refresh из хоста (paywall.billing.getUser())\n // Дополнительно слушаем purchase_completed для symmetric'ности — на\n // некоторых платежных провайдерах userChange может задержаться, а\n // purchase_completed летит мгновенно по URL-маркеру/postMessage.\n const unsubUser = paywall.on('userChange', refresh);\n const unsubPurchase = paywall.on('purchase_completed', refresh);\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsubUser();\n unsubPurchase();\n };\n }, [paywall, skipTrial, skipVisibility]);\n\n return state;\n}\n","import { useEffect, useState } from 'react';\nimport type { PaywallPrice } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `prices` — кешированный snapshot bootstrap.prices (`null` до первого fetch'а\n * или когда инстанс ещё не готов).\n * `loading` — true пока первый запрос в полёте, после первого ответа всегда false.\n * `error` — последняя ошибка fetch'а (`null` если успешный или ещё не падал).\n *\n * Намеренно нет дискриминирующего поля типа `status: 'loading'|'ready'|'error'`\n * как в `usePaywallAccess`, потому что для прайсингов хосту обычно нужны три\n * независимые величины одновременно (показать предыдущий список + skeleton +\n * сообщение об ошибке поверх) — discriminated union тут только усложняет.\n */\nexport interface PaywallPricesState {\n prices: PaywallPrice[] | null;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Загружает и подписывается на цены пейвола. Подходит для отдельной\n * прайсинг-страницы / pricing-карточек, где host хочет показать те же цены,\n * что и в модалке, без открытия paywall'а.\n *\n * Реализация:\n * - initial sync read через `getCachedPrices()` (если bootstrap уже в кеше\n * BillingClient'а — например, после `paywall.preload()` или предыдущего\n * open'а — цены доступны мгновенно);\n * - `useEffect` дёргает `getPrices()` для гарантированной загрузки;\n * - subscription на `ready` event — рефетч bootstrap'а на новом open()\n * может принести обновлённые цены, мы обновляем snapshot.\n *\n * ```tsx\n * const { prices, loading } = usePaywallPrices();\n * if (loading && !prices) return <Skeleton />;\n * return prices?.map((p) => <PriceCard key={p.id} price={p} />);\n * ```\n */\nexport function usePaywallPrices(): PaywallPricesState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallPricesState>(() => ({\n prices: paywall?.getCachedPrices() ?? null,\n loading: true,\n error: null\n }));\n\n useEffect(() => {\n if (!paywall) {\n setState({ prices: null, loading: true, error: null });\n return;\n }\n\n // Sync-доступ через cached snapshot — если bootstrap уже загружен,\n // показываем цены немедленно (без флеша «loading → ready»).\n const cached = paywall.getCachedPrices();\n setState({ prices: cached, loading: cached === null, error: null });\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getPrices({ signal: ctrl.signal })\n .then((prices) => {\n if (cancelled) return;\n setState({ prices, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (cancelled || ctrl.signal.aborted) return;\n setState((prev) => ({\n prices: prev.prices,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error))\n }));\n });\n };\n\n refresh();\n\n // `ready` event фаерится из открытого paywall'а с финальным bootstrap'ом —\n // если хост открыл/закрыл модалку, цены могли обновиться через\n // stale-while-revalidate. Слушаем чтобы в pricing-странице цифры не\n // расходились с тем, что юзер увидит в модалке.\n const unsub = paywall.on('ready', () => {\n const fresh = paywall.getCachedPrices();\n if (fresh) setState({ prices: fresh, loading: false, error: null });\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsub();\n };\n }, [paywall]);\n\n return state;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `TrialStatus` локально не экспортируется из SDK, но мы его получаем\n// через ReturnType-инференцию по публичному методу `getTrialStatus()`. Так\n// тип всегда совпадает с тем, что реально возвращает PaywallUI, без зависимости\n// от непубличного namespace'а SDK.\ntype TrialStatus = NonNullable<ReturnType<PaywallUI['getTrialStatus']>>;\n\n/**\n * Текущий статус триала ({@link TrialStatus}) с автоматическим ре-рендером на\n * `trial_blocked` события.\n *\n * Возвращает `null`, пока триал не проверялся (хост не вызывал\n * `paywall.open()` / `paywall.getAccess()`) либо триал отключён в конфиге\n * пейвола. Сам триал-стейт живёт в storage (localStorage / chrome.storage),\n * проверяется в `paywall.open()` и в `paywall.getAccess()` — оба пути обновляют\n * in-memory snapshot, который мы здесь и читаем.\n *\n * Использовать чтобы рисовать собственный UI:\n * - «У тебя осталось 3 показа» (mode `opens`) — `status.remainingActions`;\n * - «Триал истечёт через 2 часа» (mode `time`) — `status.remainingMs`;\n * - «Триал заблокирован, оплати чтобы продолжить» — `status.blocked === true`.\n *\n * ```tsx\n * const trial = usePaywallTrial();\n * if (trial?.mode === 'opens') {\n * return <Banner>Showings left: {trial.remainingActions}</Banner>;\n * }\n * ```\n */\nexport function usePaywallTrial(): TrialStatus | null {\n const paywall = usePaywall();\n const [status, setStatus] = useState<TrialStatus | null>(() =>\n paywall?.getTrialStatus() ?? null\n );\n\n // Стабильный refresh для эффекта — отдельная функция, чтобы deps массив\n // эффекта был чистым (`[paywall]`), без useCallback-цепочек.\n const sync = useCallback(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n setStatus(paywall.getTrialStatus());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n // Sync read на mount-е — getTrialStatus() мог обновиться между прошлым\n // рендером и effect'ом (например, hook вызван после первого open()-а).\n sync();\n\n // `trial_blocked` — единственный event, после которого snapshot реально\n // меняется. `trial_expired` фаерится один раз за жизнь инстанса и не\n // меняет shape статуса (статус становится `mode: 'none'` ИЛИ переходит\n // в un-blocked-режим, что и так читается через sync()).\n const unsubBlock = paywall.on('trial_blocked', sync);\n const unsubExpired = paywall.on('trial_expired', sync);\n\n return () => {\n unsubBlock();\n unsubExpired();\n };\n }, [paywall, sync]);\n\n return status;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `VisibilityStatus` локально не экспортируется из SDK — получаем через\n// ReturnType от публичного `getVisibility()`. См. usePaywallTrial для тех же\n// соображений.\ntype VisibilityStatus = NonNullable<ReturnType<PaywallUI['getVisibility']>>;\n\n/**\n * Server-computed visibility-снимок ({@link VisibilityStatus}): попадает ли\n * юзер в monetization-scope пейвола (страна, девайс, ручной visibility-флаг).\n *\n * Возвращает `null`, пока bootstrap не загружен или сервер не отдал\n * `settings.visibility` (старый online без targeting-патча).\n *\n * Использовать чтобы:\n * - показать собственный fallback («сервис недоступен в вашей стране») вместо\n * модалки, когда `visible === false`;\n * - залогировать impression для аналитики страны/tier'а юзера;\n * - принять решение какой CTA рисовать, не дёргая open() и не дожидаясь\n * visibility_blocked event.\n *\n * ```tsx\n * const visibility = usePaywallVisibility();\n * if (visibility && !visibility.visible) {\n * return <SoftBlock reason={visibility.reason} />;\n * }\n * ```\n */\nexport function usePaywallVisibility(): VisibilityStatus | null {\n const paywall = usePaywall();\n const [visibility, setVisibility] = useState<VisibilityStatus | null>(() =>\n paywall?.getVisibility() ?? null\n );\n\n const sync = useCallback(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n setVisibility(paywall.getVisibility());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n sync();\n\n // `ready` event летит после успешного bootstrap'а — там обновляется\n // `lastVisibility` в PaywallUI. `visibility_blocked` — когда блокировка\n // реально срабатывает на gate'е. Оба меняют snapshot.\n const unsubReady = paywall.on('ready', sync);\n const unsubBlocked = paywall.on('visibility_blocked', sync);\n\n return () => {\n unsubReady();\n unsubBlocked();\n };\n }, [paywall, sync]);\n\n return visibility;\n}\n","import { useEffect, type ReactNode } from 'react';\nimport type { PaywallAccessResult } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\nimport { usePaywallAccess } from '../hooks/usePaywallAccess';\n\nexport interface PaywallGateProps {\n /** Что показать, пока `getAccess()` не вернул ответ (initial fetch / Provider mount). */\n loading?: ReactNode;\n /**\n * Fallback для `blocked` ответа — обычно CTA «Upgrade». Принимает либо\n * статичный ReactNode, либо render-функцию, получающую callback\n * `open()` — удобно, чтобы кастомная кнопка сама дёргала модалку:\n *\n * ```tsx\n * fallback={({ open }) => <MyCTA onClick={open}>Upgrade</MyCTA>}\n * ```\n *\n * Если не передан — компонент рендерит `null` для blocked (host\n * полагается на `openOnBlocked` или ловит open() сам через `usePaywall`).\n */\n fallback?: ReactNode | ((args: BlockedRenderArgs) => ReactNode);\n /**\n * Автоматически дёргать `paywall.open()` сразу как только access перешёл в\n * blocked. Удобно для feature-разделителей вида «нажми и попадёшь на\n * paywall»: компонент сам открывает модалку, не нужно писать onClick.\n *\n * По умолчанию `false` — большинство хостов хотят сначала показать\n * объясняющий CTA, а модалку открывать по клику. Включать осознанно.\n */\n openOnBlocked?: boolean;\n /** Премиум-контент. Рендерится только когда access=granted. */\n children: ReactNode;\n}\n\nexport interface BlockedRenderArgs {\n result: Extract<PaywallAccessResult, { access: 'blocked' }>;\n open: () => void;\n}\n\n/**\n * Декларативная обёртка над {@link usePaywallAccess} + {@link usePaywall}.open().\n *\n * Три состояния:\n * - `loading` (первый fetch / Provider не готов) — рендерим `props.loading`;\n * - `granted` (есть подписка / visibility / trial) — рендерим `children`;\n * - `blocked` — рендерим `fallback` (если задан) и опционально дёргаем\n * `paywall.open()` при `openOnBlocked={true}`.\n *\n * ```tsx\n * <PaywallGate\n * loading={<Skeleton />}\n * fallback={({ open }) => <button onClick={open}>Upgrade</button>}\n * >\n * <PremiumFeature />\n * </PaywallGate>\n * ```\n *\n * Для нестандартных сценариев (показать \"Try free trial\" вместо upgrade,\n * комбинировать с собственным auth-flow'ом) использовать\n * {@link usePaywallAccess} напрямую — gate решает 80% кейсов, не пытаясь\n * стать конфигурируемым на каждый чих.\n */\nexport function PaywallGate(props: PaywallGateProps): JSX.Element | null {\n const paywall = usePaywall();\n const access = usePaywallAccess();\n\n // `openOnBlocked` — side-effect, поэтому в useEffect. Зависим от access\n // через идентификатор `result.access`, а не от объекта целиком, чтобы\n // не дёргать open() на каждом refresh-е getAccess'а с тем же blocked-итогом.\n const isBlocked =\n access.status === 'ready' && access.result.access === 'blocked';\n const shouldAutoOpen = props.openOnBlocked === true && isBlocked;\n\n useEffect(() => {\n if (shouldAutoOpen && paywall) paywall.open();\n }, [shouldAutoOpen, paywall]);\n\n if (access.status === 'loading') {\n return <>{props.loading ?? null}</>;\n }\n\n if (access.result.access === 'granted') {\n return <>{props.children}</>;\n }\n\n // blocked\n const fallback = props.fallback;\n if (typeof fallback === 'function') {\n return (\n <>\n {fallback({\n result: access.result,\n open: () => paywall?.open()\n })}\n </>\n );\n }\n return <>{fallback ?? null}</>;\n}\n","import {\n forwardRef,\n type ButtonHTMLAttributes,\n type ReactElement,\n type ReactNode\n} from 'react';\nimport type { OpenOptions } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\n\n/**\n * Параметры открытия пейвола, проксируются в `paywall.open(opts)`.\n * Любые поля {@link OpenOptions} применимы: `identity`, `renew`, `skipTrial`,\n * `skipVisibility`.\n */\ntype OpenProps = OpenOptions;\n\ninterface CommonProps extends OpenProps {\n /** Что открывать: layout (default), support, auth-gate, anon-gate. */\n mode?: 'paywall' | 'support' | 'auth' | 'anon';\n /** Render-prop для полного контроля над элементом-триггером. Когда задан,\n * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */\n render?: (args: PaywallButtonRenderArgs) => ReactElement;\n}\n\nexport interface PaywallButtonRenderArgs {\n /** Открыть пейвол согласно `mode` + переданным opts. */\n open: () => void;\n /** Готов ли инстанс PaywallUI. До mount-а Provider'а / на SSR — `false`. */\n ready: boolean;\n}\n\n/**\n * Props собственно `<button>`-рендера. Любые HTML-атрибуты — `disabled`,\n * `className`, `aria-label`, `type`, и т.д. — пробрасываются на нативный\n * элемент. `onClick` объединяется с нашим open()-хендлером (мы вызываем\n * наш первым, потом ваш — чтобы хост мог prevent'ить через event.preventDefault).\n */\ntype ButtonRenderProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof OpenProps | 'children'\n> & {\n children?: ReactNode;\n};\n\nexport type PaywallButtonProps = CommonProps & ButtonRenderProps;\n\n/**\n * Сахар над `usePaywall().open()`. Кнопка по умолчанию рендерится как\n * нативный `<button>` со всеми твоими className/style/disabled, но при нужде\n * можно передать `render` для произвольного элемента (Radix-style asChild\n * паттерн через render-prop).\n *\n * ```tsx\n * // обычный кейс\n * <PaywallButton className=\"btn-primary\" renew>\n * Renew subscription\n * </PaywallButton>\n *\n * // custom-элемент\n * <PaywallButton render={({ open, ready }) => (\n * <MyFancyButton onClick={open} disabled={!ready}>Upgrade</MyFancyButton>\n * )} />\n *\n * // саппорт-форма вместо тарифов\n * <PaywallButton mode=\"support\">Need help?</PaywallButton>\n * ```\n *\n * До mount-а Provider'а или на SSR кнопка рендерится с `disabled=true`\n * (через CSS-pseudo `[aria-busy]` хост может стилизовать loading-state) —\n * клик в этот момент no-op, потому что инстанса PaywallUI ещё нет.\n */\nexport const PaywallButton = forwardRef<HTMLButtonElement, PaywallButtonProps>(\n function PaywallButton(props, ref) {\n const paywall = usePaywall();\n const {\n mode = 'paywall',\n identity,\n renew,\n skipTrial,\n skipVisibility,\n render,\n onClick,\n disabled,\n ...buttonProps\n } = props;\n\n const ready = paywall !== null;\n\n const openOpts: OpenOptions = { identity, renew, skipTrial, skipVisibility };\n\n const open = (): void => {\n if (!paywall) return;\n switch (mode) {\n case 'support':\n paywall.openSupport(openOpts);\n return;\n case 'auth':\n paywall.openAuth(openOpts);\n return;\n case 'anon':\n paywall.openAnonGate(openOpts);\n return;\n default:\n paywall.open(openOpts);\n }\n };\n\n if (render) {\n return render({ open, ready });\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled || !ready}\n aria-busy={!ready ? true : undefined}\n onClick={(event) => {\n // Наш handler первым — host через event.preventDefault() ничего\n // не остановит, потому что open() уже стрельнул. Это намеренно:\n // открытие пейвола не должно зависеть от того, забыл ли хост\n // вернуть `false` из своего analytics-handler'а. Если нужен\n // префлайт-чек — паттерн через `render`-prop, там полный контроль.\n open();\n onClick?.(event);\n }}\n {...buttonProps}\n />\n );\n }\n);\n","import { forwardRef } from 'react';\nimport { PaywallButton, type PaywallButtonProps } from './PaywallButton';\n\nexport type PaywallSupportButtonProps = Omit<PaywallButtonProps, 'mode'>;\n\n/**\n * Сахар над `<PaywallButton mode=\"support\">`. Самостоятельная компонента, а\n * не пресет prop'а, для discoverability — название говорит за себя, и в\n * больших layout-ах легче видеть, где саппорт, а где основной upgrade-CTA.\n *\n * ```tsx\n * <PaywallSupportButton className=\"link\">Help</PaywallSupportButton>\n * ```\n */\nexport const PaywallSupportButton = forwardRef<\n HTMLButtonElement,\n PaywallSupportButtonProps\n>(function PaywallSupportButton(props, ref) {\n return <PaywallButton {...props} mode=\"support\" ref={ref} />;\n});\n"],"names":["PaywallContext","createContext","PaywallProviderMarker","PaywallProvider","props","externalInstance","options","paywall","setPaywall","useState","useEffect","created","PaywallUI","jsx","usePaywall","hasProvider","useContext","SSR_SNAPSHOT","usePaywallState","subscribe","useCallback","cb","getSnapshot","useSyncExternalStore","usePaywallUser","getServerSnapshot","usePaywallEvent","event","handler","handlerRef","useRef","payload","LOADING_STATE","usePaywallAccess","opts","state","setState","skipTrial","skipVisibility","ctrl","cancelled","refresh","result","unsubUser","unsubPurchase","usePaywallPrices","cached","prices","error","prev","unsub","fresh","usePaywallTrial","status","setStatus","sync","unsubBlock","unsubExpired","usePaywallVisibility","visibility","setVisibility","unsubReady","unsubBlocked","PaywallGate","access","isBlocked","shouldAutoOpen","Fragment","fallback","PaywallButton","forwardRef","ref","mode","identity","renew","render","onClick","disabled","buttonProps","ready","openOpts","open","PaywallSupportButton"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/context.ts","../src/PaywallProvider.tsx","../src/hooks/usePaywall.ts","../src/hooks/usePaywallState.ts","../src/hooks/usePaywallUser.ts","../src/hooks/usePaywallEvent.ts","../src/hooks/usePaywallAccess.ts","../src/hooks/usePaywallPrices.ts","../src/hooks/usePaywallTrial.ts","../src/hooks/usePaywallVisibility.ts","../src/components/PaywallGate.tsx","../src/components/PaywallButton.tsx","../src/components/PaywallSupportButton.tsx"],"sourcesContent":["import { createContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\n\n/**\n * Внутренний React Context, в который PaywallProvider кладёт PaywallUI-инстанс.\n *\n * value === null до того, как Provider успел смонтировать инстанс (SSR,\n * первый render до useEffect, дев double-mount в StrictMode после cleanup).\n * Хуки должны корректно обрабатывать null — отдавать loading/null/no-op,\n * а не падать.\n *\n * defaultValue intentionally `null`, а не `undefined` — это позволяет\n * usePaywall() различать «Provider не оборачивает дерево» (undefined-симуляция\n * через sentinel-объект ниже не нужна, мы это ловим иначе) и «Provider есть,\n * но инстанс ещё не создан» (null).\n */\nexport const PaywallContext = createContext<PaywallUI | null>(null);\nPaywallContext.displayName = 'PaywallContext';\n\n/**\n * Sentinel для отслеживания: «компонент вообще находится внутри Provider'а?».\n *\n * React Context отдаёт defaultValue, когда `<Provider>` не оборачивает дерево.\n * Если defaultValue=null, а Provider тоже легально кладёт null (на SSR /\n * до mount-а) — мы не различаем эти два случая. Поэтому Provider всегда\n * оборачивает второй Context с маркером HAS_PROVIDER=true, который usePaywall\n * проверяет первым.\n */\nexport const PaywallProviderMarker = createContext<boolean>(false);\nPaywallProviderMarker.displayName = 'PaywallProviderMarker';\n","import { useEffect, useState, type ReactNode } from 'react';\nimport { PaywallUI, type PaywallUIOptions } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from './context';\n\n/**\n * Два взаимоисключающих режима использования:\n *\n * - `options` — Provider сам конструирует `PaywallUI` в useEffect и\n * прибирает в cleanup. Самый частый кейс — обычный сайт.\n * - `instance` — хост создаёт PaywallUI сам и передаёт готовым. Нужно для\n * extension'ов (`@monetize.software/sdk-extension` поставляет structurally\n * compatible PaywallUI с RemoteBillingClient), для shared-инстанса между\n * несколькими React-деревьями и для тестов.\n *\n * Discriminated union на уровне типов — TS не даст передать оба сразу.\n */\nexport type PaywallProviderProps =\n | {\n options: PaywallUIOptions;\n instance?: never;\n children: ReactNode;\n }\n | {\n instance: PaywallUI;\n options?: never;\n children: ReactNode;\n };\n\n/**\n * Корневой Provider для всех React-биндингов SDK.\n *\n * ```tsx\n * // вариант 1: Provider сам создаёт инстанс\n * <PaywallProvider options={{ paywallId: '...', auth: true }}>\n * <App />\n * </PaywallProvider>\n *\n * // вариант 2: готовый инстанс снаружи (extension / shared)\n * const paywall = createPaywallUI({ paywallId: '...' });\n * <PaywallProvider instance={paywall}>\n * <App />\n * </PaywallProvider>\n * ```\n *\n * SSR: инстанс создаётся в useEffect, на сервере context value=null. Все\n * хуки делают graceful fallback (`null` / `{ status: 'loading' }`), так что\n * Provider можно безопасно рендерить в Next.js / Remix без `'use client'`-\n * ограничений на дерево потомков.\n *\n * StrictMode: cleanup-эффект зовёт `destroy()`, чтобы dev double-mount не\n * оставлял утечек listener'ов и подписок. Микротик-эффекты PaywallUI-\n * конструктора (`autoDetectReturn`) на первом инстансе становятся no-op\n * после destroy.\n *\n * Смена `options` между рендерами: не реактивна — Provider создаёт инстанс\n * один раз. Если хосту реально нужно пересоздать (поменялся `paywallId`),\n * следует менять `key` у Provider'а — это идиоматичный React-способ форсить\n * пересоздание. Делать «умное» сравнение опций мы намеренно не пытаемся:\n * структурный equality глубоких options всегда ломается на функциях-колбеках\n * и live-обновлениях storage'а.\n */\nexport function PaywallProvider(props: PaywallProviderProps): JSX.Element {\n const externalInstance = 'instance' in props ? props.instance : undefined;\n const options = 'options' in props ? props.options : undefined;\n\n // Внешний инстанс → синхронно кладём его в state, чтобы первый render\n // потомков уже видел реальный PaywallUI (хосту он доступен мгновенно после\n // вызова createPaywallUI). Свой инстанс → null до useEffect, потому что\n // конструктор PaywallUI трогает window/queueMicrotask и не должен крутиться\n // на сервере.\n const [paywall, setPaywall] = useState<PaywallUI | null>(\n externalInstance ?? null\n );\n\n // Сам инстанс создаём в useEffect (только клиент). Если хост даёт готовый —\n // useEffect просто sync'ит state на случай, если ref поменялся между\n // рендерами без unmount'а.\n useEffect(() => {\n if (externalInstance) {\n setPaywall(externalInstance);\n // Externally-owned lifecycle — destroy() не наш.\n return;\n }\n\n if (!options) return;\n\n const created = new PaywallUI(options);\n setPaywall(created);\n return () => {\n created.destroy();\n // null на cleanup — потомки на следующем render'е увидят «инстанс ещё\n // не готов» вместо обращения к destroyed-объекту. В обычной жизни\n // unmount Provider'а сразу размонтирует и потомков, поэтому это\n // подстраховка для редких manual-remount-сценариев и StrictMode'а.\n setPaywall(null);\n };\n // options/instance меняются по reference. Реактивная пересборка инстанса\n // на каждый ре-рендер хост-компонента — не то, что нужно (см. JSDoc выше).\n // Для пересоздания используется React `key`.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [externalInstance]);\n\n return (\n <PaywallProviderMarker.Provider value={true}>\n <PaywallContext.Provider value={paywall}>\n {props.children}\n </PaywallContext.Provider>\n </PaywallProviderMarker.Provider>\n );\n}\n","import { useContext } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { PaywallContext, PaywallProviderMarker } from '../context';\n\n/**\n * Достаёт PaywallUI-инстанс из ближайшего {@link PaywallProvider}.\n *\n * Бросает ошибку, если вызван вне Provider'а — это явный программный баг,\n * не runtime-флоу. На SSR / до useEffect Provider'а возвращает `null`\n * (Provider есть, но инстанс ещё не смонтирован).\n *\n * Подавляющему большинству пейволов от хоста нужны `paywall.open()`,\n * `paywall.openSupport()`, подписки на события — для всего этого\n * usePaywall() самый прямой путь:\n *\n * ```tsx\n * const paywall = usePaywall();\n * <button onClick={() => paywall?.open()}>Upgrade</button>\n * ```\n *\n * Для типичных кейсов (gating, state-driven UI) обычно удобнее\n * специализированные хуки: {@link usePaywallState}, {@link usePaywallAccess},\n * {@link usePaywallUser}.\n */\nexport function usePaywall(): PaywallUI | null {\n const hasProvider = useContext(PaywallProviderMarker);\n const paywall = useContext(PaywallContext);\n\n if (!hasProvider) {\n throw new Error(\n '[sdk-react] usePaywall() called outside <PaywallProvider>. ' +\n 'Wrap your tree with <PaywallProvider options={...}> or pass an ' +\n 'externally-created instance via <PaywallProvider instance={paywall}>.'\n );\n }\n\n return paywall;\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallStateSnapshot } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Зеркалит CLOSED_STATE из PaywallUI.ts. Хранится локально, чтобы getSnapshot\n// при paywall=null отдавал стабильную ссылку (та же ссылка между рендерами →\n// useSyncExternalStore не дёргает лишний re-render). Не экспортируется\n// наружу: для public API публичная форма доступна через usePaywallState().\n//\n// Shape проверяется в contract.ts — если PaywallStateSnapshot в SDK обзаведётся\n// новым полем, TS-build sdk-react падает раньше, чем кто-то заметит расхождение.\nconst SSR_SNAPSHOT: PaywallStateSnapshot = { open: false, view: null, error: null };\n\n/**\n * Подписка на состояние модалки пейвола: открыта/закрыта, текущий view,\n * последняя ошибка.\n *\n * Реализована поверх `paywall.onStateChange` + `paywall.getState` через\n * `useSyncExternalStore` — это даёт корректную concurrent-rendering семантику\n * (никаких tearing'ов, snapshot стабилен в рамках одного React-commit'а) и\n * минимум re-render'ов (snapshot равенство по `Object.is`).\n *\n * До mount-а Provider'а или на сервере возвращает `{ open: false, view: null,\n * error: null }` — это та же форма, что PaywallUI кладёт во внутренний\n * CLOSED_STATE, так что хосту не нужно отдельно проверять «инстанс готов».\n *\n * ```tsx\n * const { open, view } = usePaywallState();\n * useEffect(() => {\n * if (open) analytics.track('paywall_seen');\n * }, [open]);\n * ```\n */\nexport function usePaywallState(): PaywallStateSnapshot {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n // immediate: 'none' — useSyncExternalStore сам читает snapshot через\n // getSnapshot. Реплей initial-state'а через subscribe был бы лишним\n // вызовом cb, не приносящим новой информации.\n return paywall.onStateChange(cb, { immediate: 'none' });\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallStateSnapshot => {\n return paywall ? paywall.getState() : SSR_SNAPSHOT;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, () => SSR_SNAPSHOT);\n}\n","import { useCallback, useSyncExternalStore } from 'react';\nimport type { PaywallUser } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * Подписка на текущего юзера пейвола (sync snapshot + автоматический ре-рендер\n * на любой userChange — bootstrap, /me refresh, после-checkout watcher).\n *\n * Возвращает `null` до первого ответа сети или когда инстанс ещё не готов\n * (SSR / до useEffect Provider'а / Provider не оборачивает дерево с инстансом).\n *\n * Удобно для подсветки текущего плана / e-mail юзера в собственном UI без\n * необходимости держать дублирующий state и руками подписываться на\n * `paywall.on('userChange', ...)`.\n *\n * ```tsx\n * const user = usePaywallUser();\n * if (user?.has_active_subscription) {\n * return <ProBadge plan={user.active_subscription?.plan_name} />;\n * }\n * ```\n *\n * Реализация поверх `paywall.on('userChange', cb)` + `billing.getCachedUser()`.\n * `paywall.on` не делает initial replay'я, поэтому useSyncExternalStore сам\n * читает старт-snapshot через getSnapshot — без лишних cb-вызовов.\n *\n * Ссылочная стабильность: BillingClient сравнивает user shape перед update'ом\n * (`sameUser`), так что между неизменными обновлениями `getCachedUser()`\n * возвращает ===-равный объект. Это гарантирует, что useSyncExternalStore\n * не дёргает ре-рендер при no-op refresh'ах.\n */\nexport function usePaywallUser(): PaywallUser | null {\n const paywall = usePaywall();\n\n const subscribe = useCallback(\n (cb: () => void): (() => void) => {\n if (!paywall) return () => {};\n return paywall.on('userChange', () => cb());\n },\n [paywall]\n );\n\n const getSnapshot = useCallback((): PaywallUser | null => {\n return paywall ? paywall.billing.getCachedUser() : null;\n }, [paywall]);\n\n return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);\n}\n\nfunction getServerSnapshot(): PaywallUser | null {\n return null;\n}\n","import { useEffect, useRef } from 'react';\nimport type { PaywallEvent, PaywallEventHandler } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// Payload-тип конкретного события достаём через `Parameters<PaywallEventHandler<E>>[0]`,\n// потому что сам `PaywallEventPayloads` в SDK объявлен локально и не экспортируется.\n// Подход через `Parameters<>` устойчив к этому: пока `PaywallEventHandler` есть в\n// public surface, payload-тип SDK мы выводим корректно — TS-сборка sdk-react\n// упадёт, если сигнатура `PaywallEventHandler` поедет.\ntype EventPayload<E extends PaywallEvent> = Parameters<PaywallEventHandler<E>>[0];\n\n/**\n * Декларативная подписка на событие PaywallUI. Обёртка над `paywall.on(event, cb)`\n * с двумя важными отличиями от ручного useEffect:\n *\n * 1. handler не нужно мемоизировать через `useCallback` — внутри храним\n * последнюю версию в `useRef`, само subscription пересоздаётся только\n * при смене `event` или инстанса paywall'а. Это убирает класс багов с\n * «забыл useCallback → подписка отписывается-переподписывается на каждый\n * рендер → события теряются».\n *\n * 2. Корректно обрабатывает `paywall === null` (SSR / до Provider mount-а):\n * подписка просто не создаётся, ждёт пока инстанс появится.\n *\n * ```tsx\n * usePaywallEvent('purchase_completed', (payload) => {\n * toast.success(`Покупка ${payload.priceId} прошла`);\n * queryClient.invalidateQueries(['user']);\n * });\n * ```\n *\n * Для self-cleaning логики (host эмит'а аналитики, инвалидаций кешей, гидрации\n * локального стейта) это самый прямой паттерн — компонент гарантированно\n * отпишется при unmount'е, и не нужно держать unsub-ref'ы вручную.\n */\nexport function usePaywallEvent<E extends PaywallEvent>(\n event: E,\n handler: PaywallEventHandler<E>\n): void {\n const paywall = usePaywall();\n const handlerRef = useRef(handler);\n\n // Обновляем ref на каждом render'е — следующее срабатывание события\n // подхватит свежий handler. Без отдельного useEffect, потому что синхронный\n // assign в render-фазе для ref'а корректен и не нарушает rules-of-hooks.\n handlerRef.current = handler;\n\n useEffect(() => {\n if (!paywall) return;\n return paywall.on(event, (payload) => {\n // Cast необходим, потому что общий вариант `PaywallEventHandler` теряет\n // narrowing по `E`. handlerRef.current типизирован под конкретный E,\n // но `on()` принимает union — рантайм-shape гарантирован SDK'шным emit'ом.\n (handlerRef.current as (p: EventPayload<E>) => void)(payload);\n });\n }, [paywall, event]);\n}\n","import { useEffect, useState } from 'react';\nimport type {\n GetAccessOptions,\n PaywallAccessResult\n} from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `loading` — первый fetch ещё в полёте (или Provider не готов).\n * `ready` — есть свежий ответ; `result` гарантированно non-null.\n *\n * Сделано discriminated union'ом, чтобы хост мог сужать тип одним if-ом:\n *\n * `if (access.status === 'ready') access.result.access === 'granted'`\n */\nexport type PaywallAccessState =\n | { status: 'loading'; result: null }\n | { status: 'ready'; result: PaywallAccessResult };\n\nconst LOADING_STATE: PaywallAccessState = { status: 'loading', result: null };\n\n/**\n * Главный хук для гейтинга фич: «нужно ли блокировать фичу для этого юзера?».\n *\n * Под капотом — `paywall.getAccess(opts)` без side-effect'ов (модалка не\n * монтируется, trial-storage не двигается). На каждый `userChange` событие\n * автоматически рефетчится — после успешной покупки `has_subscription`\n * сработает мгновенно, и хост перерендерит UI без feature-lock'а.\n *\n * Bootstrap кешируется в BillingClient, так что usePaywallAccess можно дёргать\n * в любом компоненте дерева — сетевой запрос будет ровно один (или ни одного,\n * если кеш свежий).\n *\n * ```tsx\n * const access = usePaywallAccess();\n * const paywall = usePaywall();\n *\n * if (access.status === 'loading') return <Skeleton />;\n * if (access.result.access === 'blocked') {\n * return <button onClick={() => paywall?.open()}>Upgrade</button>;\n * }\n * return <PremiumFeature />;\n * ```\n *\n * Опции `opts` десериализуются по `skipTrial`/`skipVisibility` — стабильность\n * ссылки `opts` не требуется, эффект перезапустится только при реальном\n * изменении этих флагов. `signal` мы дропаем из deps (на каждый рендер у него\n * новый ref) — отмена inflight-запроса делается локально через AbortController\n * в cleanup-эффекте.\n */\nexport function usePaywallAccess(opts: GetAccessOptions = {}): PaywallAccessState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallAccessState>(LOADING_STATE);\n\n const skipTrial = opts.skipTrial === true;\n const skipVisibility = opts.skipVisibility === true;\n\n useEffect(() => {\n if (!paywall) {\n // Инстанс ушёл (Provider unmount / StrictMode cleanup) — честно\n // вернуть loading, чтобы хост не показывал устаревший result от\n // прошлого инстанса.\n setState(LOADING_STATE);\n return;\n }\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getAccess({ skipTrial, skipVisibility, signal: ctrl.signal })\n .then((result) => {\n if (cancelled || ctrl.signal.aborted) return;\n // Каждый refresh даёт новый объект — useState увидит !== и\n // ререндерит. Это ок: для гейтинга интерес представляет именно\n // `access` поле, остальное (visibility/trial snapshot'ы) — auxiliary\n // данные, которые не должны бы менять решение хоста на тех же входах.\n setState({ status: 'ready', result });\n })\n .catch(() => {\n // getAccess() имеет собственный offline-fallback и не throw'ит на\n // failed network'е — сюда мы попадаем только при abort'е, который\n // прилетает в cleanup-эффекте. Молча игнорим.\n });\n };\n\n refresh();\n\n // userChange покрывает оба источника обновления decision'а:\n // - после-checkout watcher эмит'ит userChange когда has_subscription=true\n // - manual /me refresh из хоста (paywall.billing.getUser())\n // Дополнительно слушаем purchase_completed для symmetric'ности — на\n // некоторых платежных провайдерах userChange может задержаться, а\n // purchase_completed летит мгновенно по URL-маркеру/postMessage.\n const unsubUser = paywall.on('userChange', refresh);\n const unsubPurchase = paywall.on('purchase_completed', refresh);\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsubUser();\n unsubPurchase();\n };\n }, [paywall, skipTrial, skipVisibility]);\n\n return state;\n}\n","import { useEffect, useState } from 'react';\nimport type { PaywallPrice } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n/**\n * `prices` — кешированный snapshot bootstrap.prices (`null` до первого fetch'а\n * или когда инстанс ещё не готов).\n * `loading` — true пока первый запрос в полёте, после первого ответа всегда false.\n * `error` — последняя ошибка fetch'а (`null` если успешный или ещё не падал).\n *\n * Намеренно нет дискриминирующего поля типа `status: 'loading'|'ready'|'error'`\n * как в `usePaywallAccess`, потому что для прайсингов хосту обычно нужны три\n * независимые величины одновременно (показать предыдущий список + skeleton +\n * сообщение об ошибке поверх) — discriminated union тут только усложняет.\n */\nexport interface PaywallPricesState {\n prices: PaywallPrice[] | null;\n loading: boolean;\n error: Error | null;\n}\n\n/**\n * Загружает и подписывается на цены пейвола. Подходит для отдельной\n * прайсинг-страницы / pricing-карточек, где host хочет показать те же цены,\n * что и в модалке, без открытия paywall'а.\n *\n * Реализация:\n * - initial sync read через `getCachedPrices()` (если bootstrap уже в кеше\n * BillingClient'а — например, после `paywall.preload()` или предыдущего\n * open'а — цены доступны мгновенно);\n * - `useEffect` дёргает `getPrices()` для гарантированной загрузки;\n * - subscription на `ready` event — рефетч bootstrap'а на новом open()\n * может принести обновлённые цены, мы обновляем snapshot.\n *\n * ```tsx\n * const { prices, loading } = usePaywallPrices();\n * if (loading && !prices) return <Skeleton />;\n * return prices?.map((p) => <PriceCard key={p.id} price={p} />);\n * ```\n */\nexport function usePaywallPrices(): PaywallPricesState {\n const paywall = usePaywall();\n const [state, setState] = useState<PaywallPricesState>(() => ({\n prices: paywall?.getCachedPrices() ?? null,\n loading: true,\n error: null\n }));\n\n useEffect(() => {\n if (!paywall) {\n setState({ prices: null, loading: true, error: null });\n return;\n }\n\n // Sync-доступ через cached snapshot — если bootstrap уже загружен,\n // показываем цены немедленно (без флеша «loading → ready»).\n const cached = paywall.getCachedPrices();\n setState({ prices: cached, loading: cached === null, error: null });\n\n const ctrl = new AbortController();\n let cancelled = false;\n\n const refresh = () => {\n paywall\n .getPrices({ signal: ctrl.signal })\n .then((prices) => {\n if (cancelled) return;\n setState({ prices, loading: false, error: null });\n })\n .catch((error: unknown) => {\n if (cancelled || ctrl.signal.aborted) return;\n setState((prev) => ({\n prices: prev.prices,\n loading: false,\n error: error instanceof Error ? error : new Error(String(error))\n }));\n });\n };\n\n refresh();\n\n // `ready` event фаерится из открытого paywall'а с финальным bootstrap'ом —\n // если хост открыл/закрыл модалку, цены могли обновиться через\n // stale-while-revalidate. Слушаем чтобы в pricing-странице цифры не\n // расходились с тем, что юзер увидит в модалке.\n const unsub = paywall.on('ready', () => {\n const fresh = paywall.getCachedPrices();\n if (fresh) setState({ prices: fresh, loading: false, error: null });\n });\n\n return () => {\n cancelled = true;\n ctrl.abort();\n unsub();\n };\n }, [paywall]);\n\n return state;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `TrialStatus` локально не экспортируется из SDK, но мы его получаем\n// через ReturnType-инференцию по публичному методу `getTrialStatus()`. Так\n// тип всегда совпадает с тем, что реально возвращает PaywallUI, без зависимости\n// от непубличного namespace'а SDK.\ntype TrialStatus = NonNullable<ReturnType<PaywallUI['getTrialStatus']>>;\n\n/**\n * Текущий статус триала ({@link TrialStatus}) с автоматическим ре-рендером на\n * `trial_blocked` события.\n *\n * Возвращает `null`, пока триал не проверялся (хост не вызывал\n * `paywall.open()` / `paywall.getAccess()`) либо триал отключён в конфиге\n * пейвола. Сам триал-стейт живёт в storage (localStorage / chrome.storage),\n * проверяется в `paywall.open()` и в `paywall.getAccess()` — оба пути обновляют\n * in-memory snapshot, который мы здесь и читаем.\n *\n * Использовать чтобы рисовать собственный UI:\n * - «У тебя осталось 3 показа» (mode `opens`) — `status.remainingActions`;\n * - «Триал истечёт через 2 часа» (mode `time`) — `status.remainingMs`;\n * - «Триал заблокирован, оплати чтобы продолжить» — `status.blocked === true`.\n *\n * ```tsx\n * const trial = usePaywallTrial();\n * if (trial?.mode === 'opens') {\n * return <Banner>Showings left: {trial.remainingActions}</Banner>;\n * }\n * ```\n */\nexport function usePaywallTrial(): TrialStatus | null {\n const paywall = usePaywall();\n const [status, setStatus] = useState<TrialStatus | null>(() =>\n paywall?.getTrialStatus() ?? null\n );\n\n // Стабильный refresh для эффекта — отдельная функция, чтобы deps массив\n // эффекта был чистым (`[paywall]`), без useCallback-цепочек.\n const sync = useCallback(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n setStatus(paywall.getTrialStatus());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setStatus(null);\n return;\n }\n // Sync read на mount-е — getTrialStatus() мог обновиться между прошлым\n // рендером и effect'ом (например, hook вызван после первого open()-а).\n sync();\n\n // `trial_blocked` — единственный event, после которого snapshot реально\n // меняется. `trial_expired` фаерится один раз за жизнь инстанса и не\n // меняет shape статуса (статус становится `mode: 'none'` ИЛИ переходит\n // в un-blocked-режим, что и так читается через sync()).\n const unsubBlock = paywall.on('trial_blocked', sync);\n const unsubExpired = paywall.on('trial_expired', sync);\n\n return () => {\n unsubBlock();\n unsubExpired();\n };\n }, [paywall, sync]);\n\n return status;\n}\n","import { useCallback, useEffect, useState } from 'react';\nimport type { PaywallUI } from '@monetize.software/sdk';\nimport { usePaywall } from './usePaywall';\n\n// `VisibilityStatus` локально не экспортируется из SDK — получаем через\n// ReturnType от публичного `getVisibility()`. См. usePaywallTrial для тех же\n// соображений.\ntype VisibilityStatus = NonNullable<ReturnType<PaywallUI['getVisibility']>>;\n\n/**\n * Server-computed visibility-снимок ({@link VisibilityStatus}): попадает ли\n * юзер в monetization-scope пейвола (страна, девайс, ручной visibility-флаг).\n *\n * Возвращает `null`, пока bootstrap не загружен или сервер не отдал\n * `settings.visibility` (старый online без targeting-патча).\n *\n * Использовать чтобы:\n * - показать собственный fallback («сервис недоступен в вашей стране») вместо\n * модалки, когда `visible === false`;\n * - залогировать impression для аналитики страны/tier'а юзера;\n * - принять решение какой CTA рисовать, не дёргая open() и не дожидаясь\n * visibility_blocked event.\n *\n * ```tsx\n * const visibility = usePaywallVisibility();\n * if (visibility && !visibility.visible) {\n * return <SoftBlock reason={visibility.reason} />;\n * }\n * ```\n */\nexport function usePaywallVisibility(): VisibilityStatus | null {\n const paywall = usePaywall();\n const [visibility, setVisibility] = useState<VisibilityStatus | null>(() =>\n paywall?.getVisibility() ?? null\n );\n\n const sync = useCallback(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n setVisibility(paywall.getVisibility());\n }, [paywall]);\n\n useEffect(() => {\n if (!paywall) {\n setVisibility(null);\n return;\n }\n sync();\n\n // `ready` event летит после успешного bootstrap'а — там обновляется\n // `lastVisibility` в PaywallUI. `visibility_blocked` — когда блокировка\n // реально срабатывает на gate'е. Оба меняют snapshot.\n const unsubReady = paywall.on('ready', sync);\n const unsubBlocked = paywall.on('visibility_blocked', sync);\n\n return () => {\n unsubReady();\n unsubBlocked();\n };\n }, [paywall, sync]);\n\n return visibility;\n}\n","import { useEffect, type ReactNode } from 'react';\nimport type { PaywallAccessResult } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\nimport { usePaywallAccess } from '../hooks/usePaywallAccess';\n\nexport interface PaywallGateProps {\n /** Что показать, пока `getAccess()` не вернул ответ (initial fetch / Provider mount). */\n loading?: ReactNode;\n /**\n * Fallback для `blocked` ответа — обычно CTA «Upgrade». Принимает либо\n * статичный ReactNode, либо render-функцию, получающую callback\n * `open()` — удобно, чтобы кастомная кнопка сама дёргала модалку:\n *\n * ```tsx\n * fallback={({ open }) => <MyCTA onClick={open}>Upgrade</MyCTA>}\n * ```\n *\n * Если не передан — компонент рендерит `null` для blocked (host\n * полагается на `openOnBlocked` или ловит open() сам через `usePaywall`).\n */\n fallback?: ReactNode | ((args: BlockedRenderArgs) => ReactNode);\n /**\n * Автоматически дёргать `paywall.open()` сразу как только access перешёл в\n * blocked. Удобно для feature-разделителей вида «нажми и попадёшь на\n * paywall»: компонент сам открывает модалку, не нужно писать onClick.\n *\n * По умолчанию `false` — большинство хостов хотят сначала показать\n * объясняющий CTA, а модалку открывать по клику. Включать осознанно.\n */\n openOnBlocked?: boolean;\n /** Премиум-контент. Рендерится только когда access=granted. */\n children: ReactNode;\n}\n\nexport interface BlockedRenderArgs {\n result: Extract<PaywallAccessResult, { access: 'blocked' }>;\n open: () => void;\n}\n\n/**\n * Декларативная обёртка над {@link usePaywallAccess} + {@link usePaywall}.open().\n *\n * Три состояния:\n * - `loading` (первый fetch / Provider не готов) — рендерим `props.loading`;\n * - `granted` (есть подписка / visibility / trial) — рендерим `children`;\n * - `blocked` — рендерим `fallback` (если задан) и опционально дёргаем\n * `paywall.open()` при `openOnBlocked={true}`.\n *\n * ```tsx\n * <PaywallGate\n * loading={<Skeleton />}\n * fallback={({ open }) => <button onClick={open}>Upgrade</button>}\n * >\n * <PremiumFeature />\n * </PaywallGate>\n * ```\n *\n * Для нестандартных сценариев (показать \"Try free trial\" вместо upgrade,\n * комбинировать с собственным auth-flow'ом) использовать\n * {@link usePaywallAccess} напрямую — gate решает 80% кейсов, не пытаясь\n * стать конфигурируемым на каждый чих.\n */\nexport function PaywallGate(props: PaywallGateProps): JSX.Element | null {\n const paywall = usePaywall();\n const access = usePaywallAccess();\n\n // `openOnBlocked` — side-effect, поэтому в useEffect. Зависим от access\n // через идентификатор `result.access`, а не от объекта целиком, чтобы\n // не дёргать open() на каждом refresh-е getAccess'а с тем же blocked-итогом.\n const isBlocked =\n access.status === 'ready' && access.result.access === 'blocked';\n const shouldAutoOpen = props.openOnBlocked === true && isBlocked;\n\n useEffect(() => {\n if (shouldAutoOpen && paywall) paywall.open();\n }, [shouldAutoOpen, paywall]);\n\n if (access.status === 'loading') {\n return <>{props.loading ?? null}</>;\n }\n\n if (access.result.access === 'granted') {\n return <>{props.children}</>;\n }\n\n // blocked\n const fallback = props.fallback;\n if (typeof fallback === 'function') {\n return (\n <>\n {fallback({\n result: access.result,\n open: () => paywall?.open()\n })}\n </>\n );\n }\n return <>{fallback ?? null}</>;\n}\n","import {\n forwardRef,\n type ButtonHTMLAttributes,\n type ReactElement,\n type ReactNode\n} from 'react';\nimport type { OpenOptions } from '@monetize.software/sdk';\nimport { usePaywall } from '../hooks/usePaywall';\n\n/**\n * Параметры открытия пейвола, проксируются в `paywall.open(opts)`.\n * Любые поля {@link OpenOptions} применимы: `identity`, `renew`, `skipTrial`,\n * `skipVisibility`.\n */\ntype OpenProps = OpenOptions;\n\ninterface CommonProps extends OpenProps {\n /** Что открывать: layout (default), support, auth-gate (signin),\n * signup-форма. 'auth' эквивалентен 'signin' (исторически — openAuth\n * дефолтит в signin-mode). Для анонимного signin используй\n * `usePaywall().signInAnonymously()` напрямую — headless без модалки. */\n mode?: 'paywall' | 'support' | 'auth' | 'signin' | 'signup';\n /** Render-prop для полного контроля над элементом-триггером. Когда задан,\n * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */\n render?: (args: PaywallButtonRenderArgs) => ReactElement;\n}\n\nexport interface PaywallButtonRenderArgs {\n /** Открыть пейвол согласно `mode` + переданным opts. */\n open: () => void;\n /** Готов ли инстанс PaywallUI. До mount-а Provider'а / на SSR — `false`. */\n ready: boolean;\n}\n\n/**\n * Props собственно `<button>`-рендера. Любые HTML-атрибуты — `disabled`,\n * `className`, `aria-label`, `type`, и т.д. — пробрасываются на нативный\n * элемент. `onClick` объединяется с нашим open()-хендлером (мы вызываем\n * наш первым, потом ваш — чтобы хост мог prevent'ить через event.preventDefault).\n */\ntype ButtonRenderProps = Omit<\n ButtonHTMLAttributes<HTMLButtonElement>,\n keyof OpenProps | 'children'\n> & {\n children?: ReactNode;\n};\n\nexport type PaywallButtonProps = CommonProps & ButtonRenderProps;\n\n/**\n * Сахар над `usePaywall().open()`. Кнопка по умолчанию рендерится как\n * нативный `<button>` со всеми твоими className/style/disabled, но при нужде\n * можно передать `render` для произвольного элемента (Radix-style asChild\n * паттерн через render-prop).\n *\n * ```tsx\n * // обычный кейс\n * <PaywallButton className=\"btn-primary\" renew>\n * Renew subscription\n * </PaywallButton>\n *\n * // custom-элемент\n * <PaywallButton render={({ open, ready }) => (\n * <MyFancyButton onClick={open} disabled={!ready}>Upgrade</MyFancyButton>\n * )} />\n *\n * // саппорт-форма вместо тарифов\n * <PaywallButton mode=\"support\">Need help?</PaywallButton>\n * ```\n *\n * До mount-а Provider'а или на SSR кнопка рендерится с `disabled=true`\n * (через CSS-pseudo `[aria-busy]` хост может стилизовать loading-state) —\n * клик в этот момент no-op, потому что инстанса PaywallUI ещё нет.\n */\nexport const PaywallButton = forwardRef<HTMLButtonElement, PaywallButtonProps>(\n function PaywallButton(props, ref) {\n const paywall = usePaywall();\n const {\n mode = 'paywall',\n identity,\n renew,\n skipTrial,\n skipVisibility,\n render,\n onClick,\n disabled,\n ...buttonProps\n } = props;\n\n const ready = paywall !== null;\n\n const openOpts: OpenOptions = { identity, renew, skipTrial, skipVisibility };\n\n const open = (): void => {\n if (!paywall) return;\n switch (mode) {\n case 'support':\n paywall.openSupport(openOpts);\n return;\n case 'auth':\n case 'signin':\n paywall.openSignin(openOpts);\n return;\n case 'signup':\n paywall.openSignup(openOpts);\n return;\n default:\n paywall.open(openOpts);\n }\n };\n\n if (render) {\n return render({ open, ready });\n }\n\n return (\n <button\n ref={ref}\n type=\"button\"\n disabled={disabled || !ready}\n aria-busy={!ready ? true : undefined}\n onClick={(event) => {\n // Наш handler первым — host через event.preventDefault() ничего\n // не остановит, потому что open() уже стрельнул. Это намеренно:\n // открытие пейвола не должно зависеть от того, забыл ли хост\n // вернуть `false` из своего analytics-handler'а. Если нужен\n // префлайт-чек — паттерн через `render`-prop, там полный контроль.\n open();\n onClick?.(event);\n }}\n {...buttonProps}\n />\n );\n }\n);\n","import { forwardRef } from 'react';\nimport { PaywallButton, type PaywallButtonProps } from './PaywallButton';\n\nexport type PaywallSupportButtonProps = Omit<PaywallButtonProps, 'mode'>;\n\n/**\n * Сахар над `<PaywallButton mode=\"support\">`. Самостоятельная компонента, а\n * не пресет prop'а, для discoverability — название говорит за себя, и в\n * больших layout-ах легче видеть, где саппорт, а где основной upgrade-CTA.\n *\n * ```tsx\n * <PaywallSupportButton className=\"link\">Help</PaywallSupportButton>\n * ```\n */\nexport const PaywallSupportButton = forwardRef<\n HTMLButtonElement,\n PaywallSupportButtonProps\n>(function PaywallSupportButton(props, ref) {\n return <PaywallButton {...props} mode=\"support\" ref={ref} />;\n});\n"],"names":["PaywallContext","createContext","PaywallProviderMarker","PaywallProvider","props","externalInstance","options","paywall","setPaywall","useState","useEffect","created","PaywallUI","jsx","usePaywall","hasProvider","useContext","SSR_SNAPSHOT","usePaywallState","subscribe","useCallback","cb","getSnapshot","useSyncExternalStore","usePaywallUser","getServerSnapshot","usePaywallEvent","event","handler","handlerRef","useRef","payload","LOADING_STATE","usePaywallAccess","opts","state","setState","skipTrial","skipVisibility","ctrl","cancelled","refresh","result","unsubUser","unsubPurchase","usePaywallPrices","cached","prices","error","prev","unsub","fresh","usePaywallTrial","status","setStatus","sync","unsubBlock","unsubExpired","usePaywallVisibility","visibility","setVisibility","unsubReady","unsubBlocked","PaywallGate","access","isBlocked","shouldAutoOpen","Fragment","fallback","PaywallButton","forwardRef","ref","mode","identity","renew","render","onClick","disabled","buttonProps","ready","openOpts","open","PaywallSupportButton"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monetize.software/sdk-react",
3
- "version": "3.0.0-alpha.4",
3
+ "version": "3.0.0-alpha.6",
4
4
  "description": "React bindings for @monetize.software/sdk — Provider, hooks and declarative components. Works with the web SDK and the extension SDK (any drop-in compatible PaywallUI).",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -26,7 +26,7 @@
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=18",
29
- "@monetize.software/sdk": "3.0.0-alpha.6"
29
+ "@monetize.software/sdk": "3.0.0-alpha.8"
30
30
  },
31
31
  "peerDependenciesMeta": {
32
32
  "@monetize.software/sdk": {
@@ -48,7 +48,7 @@
48
48
  "vite": "^6.0.5",
49
49
  "vite-plugin-dts": "^4.3.0",
50
50
  "vitest": "^2.1.8",
51
- "@monetize.software/sdk": "3.0.0-alpha.6"
51
+ "@monetize.software/sdk": "3.0.0-alpha.8"
52
52
  },
53
53
  "scripts": {
54
54
  "dev": "vite",