@monetize.software/sdk-react 3.0.0-alpha.2 → 3.0.0-alpha.20

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>
@@ -36,54 +46,64 @@ function App() {
36
46
  }
37
47
 
38
48
  function UpgradeCTA() {
39
- const user = usePaywallUser();
40
- return <p>Привет, {user?.email ?? 'гость'}! Открой полный доступ.</p>;
49
+ const account = usePaywallUser();
50
+ if (account.status === 'loading') return <p>…</p>;
51
+ if (account.status === 'guest') return <p>Hi guest! Unlock full access.</p>;
52
+ return <p>Hi, {account.user?.email ?? 'there'}! Unlock full access.</p>;
41
53
  }
42
54
  ```
43
55
 
56
+ `apiOrigin` must match the `custom_domain` configured for your paywall in the
57
+ platform.
58
+
44
59
  ## Provider
45
60
 
46
- `<PaywallProvider>` принимает один из двух пропсов:
61
+ `<PaywallProvider>` accepts one of two props:
47
62
 
48
63
  ```tsx
49
- // Вариант 1 — Provider сам создаёт инстанс
64
+ // Option 1 — Provider creates the instance itself
50
65
  <PaywallProvider options={{ paywallId, apiOrigin, auth: true }}>
51
66
 
52
- // Вариант 2 — готовый инстанс снаружи (extension / shared / тесты)
67
+ // Option 2 — host supplies a ready instance (extension / shared singleton / tests)
53
68
  import { createPaywallUI } from '@monetize.software/sdk-extension';
54
- const paywall = createPaywallUI({ paywallId });
69
+ const paywall = createPaywallUI({ paywallId, apiOrigin });
55
70
 
56
71
  <PaywallProvider instance={paywall}>
57
72
  ```
58
73
 
59
- Если `paywallId` динамически меняется, перемонтируй Provider через `<PaywallProvider key={paywallId} options={...}>` — реактивная пересборка опций намеренно не делается.
74
+ If `paywallId` changes dynamically, remount the Provider via
75
+ `<PaywallProvider key={paywallId} options={...}>` — reactive option rebuilds are
76
+ intentionally not performed.
60
77
 
61
- ## Хуки
78
+ ## Hooks
62
79
 
63
- | Хук | Возвращает | Когда триггерит rerender |
80
+ | Hook | Returns | When it triggers a rerender |
64
81
  |---|---|---|
65
- | `usePaywall()` | `PaywallUI \| null` | смена инстанса (редко) |
66
- | `usePaywallState()` | `{ open, view, error }` | любое изменение state-машины |
67
- | `usePaywallUser()` | `PaywallUser \| null` | event `userChange` |
82
+ | `usePaywall()` | `PaywallUI \| null` | instance change (rare) |
83
+ | `usePaywallState()` | `{ open, view, error }` | any state-machine change |
84
+ | `usePaywallUser()` | `PaywallUserState` (`loading` \| `guest` \| `signed_in`) | `userChange` / `authChange` |
68
85
  | `usePaywallAccess(opts?)` | `{ status, result }` | `userChange` / `purchase_completed` |
69
86
  | `usePaywallPrices()` | `{ prices, loading, error }` | bootstrap refresh |
87
+ | `usePaywallOffer(priceId)` | `ResolvedOffer \| null` | `ready` + 1Hz tick while countdown is live |
88
+ | `usePaywallOffers()` | `PaywallOffer[] \| null` | `ready` (bootstrap refresh) |
70
89
  | `usePaywallTrial()` | `TrialStatus \| null` | `trial_blocked` / `trial_expired` |
71
90
  | `usePaywallVisibility()` | `VisibilityStatus \| null` | `ready` / `visibility_blocked` |
72
- | `usePaywallEvent(event, handler)` | — | подписка с stable-handler-ref |
91
+ | `usePaywallEvent(event, handler)` | — | subscribes with a stable handler ref |
73
92
 
74
- Все хуки безопасны до mount-а Provider (отдают `null` / loading) — можно использовать в SSR без `'use client'`-обёрток на ветке дерева.
93
+ All hooks are safe before the Provider mounts (they return `null` / loading) —
94
+ you can use them in SSR without `'use client'` wrappers on the consuming subtree.
75
95
 
76
- ## Компоненты
96
+ ## Components
77
97
 
78
98
  ### `<PaywallGate>`
79
99
 
80
- Декларативный гейт: loading → fallback → children.
100
+ Declarative gate: loading → fallback → children.
81
101
 
82
102
  ```tsx
83
103
  <PaywallGate
84
104
  loading={<Skeleton />}
85
105
  fallback={({ open }) => <button onClick={open}>Upgrade</button>}
86
- openOnBlocked={false} // если true — автоматом дёргает paywall.open()
106
+ openOnBlocked={false} // if true — calls paywall.open() automatically
87
107
  >
88
108
  <PremiumFeature />
89
109
  </PaywallGate>
@@ -91,7 +111,9 @@ const paywall = createPaywallUI({ paywallId });
91
111
 
92
112
  ### `<PaywallButton>` / `<PaywallSupportButton>`
93
113
 
94
- Сахар над `paywall.open()`. По умолчанию рендерится как нативный `<button>` со всеми твоими `className`/`disabled`/`aria-*`. Для кастомного элемента — render-prop:
114
+ Sugar over `paywall.open()`. By default renders a native `<button>` with all
115
+ your `className`/`disabled`/`aria-*` props forwarded. For a custom element use
116
+ the render prop:
95
117
 
96
118
  ```tsx
97
119
  <PaywallButton render={({ open, ready }) => (
@@ -99,58 +121,72 @@ const paywall = createPaywallUI({ paywallId });
99
121
  )} />
100
122
  ```
101
123
 
102
- `mode` переключает между `open()` / `openSupport()` / `openAuth()` / `openAnonGate()`:
124
+ `mode` switches between `open()` / `openSupport()` / `openSignin()` / `openSignup()`:
103
125
 
104
126
  ```tsx
105
127
  <PaywallButton mode="support">Need help?</PaywallButton>
106
- <PaywallButton mode="auth">Sign in</PaywallButton>
128
+ <PaywallButton mode="signin">Sign in</PaywallButton>
129
+ <PaywallButton mode="signup">Create account</PaywallButton>
107
130
  ```
108
131
 
132
+ `mode="auth"` оставлен как алиас для `signin` (back-compat).
133
+
134
+ Для анонимного signin'а используй `usePaywall().signInAnonymously()` напрямую — он headless (без модалки), хост сам управляет loading-стейтом кнопки.
135
+
109
136
  ## SSR / Next.js
110
137
 
111
138
  ```tsx
112
- 'use client'; // на Provider, не на дерево потомков
139
+ 'use client'; // on the Provider, not on the consumer subtree
113
140
 
114
141
  import { PaywallProvider } from '@monetize.software/sdk-react';
115
142
 
116
143
  export function PaywallProviders({ children }) {
117
144
  return (
118
- <PaywallProvider options={{ paywallId: process.env.NEXT_PUBLIC_PAYWALL_ID! }}>
145
+ <PaywallProvider
146
+ options={{
147
+ paywallId: process.env.NEXT_PUBLIC_PAYWALL_ID!,
148
+ apiOrigin: process.env.NEXT_PUBLIC_PAYWALL_ORIGIN!
149
+ }}
150
+ >
119
151
  {children}
120
152
  </PaywallProvider>
121
153
  );
122
154
  }
123
155
  ```
124
156
 
125
- Хуки можно вызывать из server components только при типизированных-null-сценариях (всё равно вернётся `null`/`loading`). Рекомендация — выносить хук-логику в client component.
157
+ Hooks can be called from server components in typed-null scenarios (they'll
158
+ return `null` / loading anyway). The recommendation is to keep hook logic in a
159
+ client component.
126
160
 
127
- ## Защита от изменений в SDK
161
+ ## SDK contract guard
128
162
 
129
- `pnpm typecheck` проверяет [`src/contract.ts`](src/contract.ts) — там перечислены все точки опоры на public API SDK (методы PaywallUI, поля snapshot'ов, имена событий). Любое разъезжание в `../sdk` ловится здесь раньше, чем в проде.
163
+ `pnpm typecheck` validates [`src/contract.ts`](src/contract.ts) — it lists every
164
+ point of contact with the public SDK API (`PaywallUI` methods, snapshot fields,
165
+ event names). Any drift in `../sdk` is caught here before it hits production.
130
166
 
131
- После изменений в SDK обнови dist для типов:
167
+ After SDK changes, refresh the dist for type resolution:
132
168
 
133
169
  ```bash
134
170
  cd ../sdk && pnpm build
135
171
  cd ../sdk-react && pnpm typecheck
136
172
  ```
137
173
 
138
- ## Разработка
174
+ ## Development
139
175
 
140
176
  ```bash
141
177
  pnpm install
142
178
  pnpm dev # → http://localhost:5080/demo/
143
- pnpm typecheck # TS-валидация + контракт
179
+ pnpm typecheck # TS validation + contract guard
144
180
  pnpm test # vitest + @testing-library/react
145
- pnpm test:e2e # playwright против демо
181
+ pnpm test:e2e # playwright against the demo
146
182
  pnpm build # ESM + CJS + d.ts → dist/
147
183
  ```
148
184
 
149
185
  ## API reference
150
186
 
151
- Полные JSDoc-комментарии на каждый публичный экспорт смотри в исходниках:
187
+ Full JSDoc comments on every public export are inline in the sources:
152
188
 
153
189
  - [`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
190
+ - [`src/hooks/`](src/hooks/) — all hooks
191
+ - [`src/components/`](src/components/) — declarative components
192
+ - [`src/contract.ts`](src/contract.ts) — SDK contact points
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { PaywallUI, PaywallUIOptions } from '../../sdk/src';
2
+ import { PaywallUI, PaywallUIOptions } from '@monetize.software/sdk';
3
3
  /**
4
4
  * Два взаимоисключающих режима использования:
5
5
  *
@@ -1,5 +1,5 @@
1
1
  import { ButtonHTMLAttributes, ReactElement, ReactNode } from 'react';
2
- import { OpenOptions } from '../../../sdk/src';
2
+ import { OpenOptions } from '@monetize.software/sdk';
3
3
  /**
4
4
  * Параметры открытия пейвола, проксируются в `paywall.open(opts)`.
5
5
  * Любые поля {@link OpenOptions} применимы: `identity`, `renew`, `skipTrial`,
@@ -7,8 +7,18 @@ 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';
15
+ /** Direct-checkout: при заданном `priceId` клик вызывает
16
+ * `paywall.checkout(priceId, opts)` минуя layout с тарифами. `mode`
17
+ * при этом игнорируется. Layout-flow (`mode='paywall'`, дефолт) и
18
+ * direct-checkout — взаимоисключающие: либо юзер выбирает план в
19
+ * модалке, либо хост уже выбрал и ведёт в checkout. См.
20
+ * `PaywallUI.checkout()` про preauth/already-paid поведение. */
21
+ priceId?: string;
12
22
  /** Render-prop для полного контроля над элементом-триггером. Когда задан,
13
23
  * все обычные `<button>`-пропсы (children, type, и т.д.) игнорируются. */
14
24
  render?: (args: PaywallButtonRenderArgs) => ReactElement;
@@ -18,6 +28,11 @@ export interface PaywallButtonRenderArgs {
18
28
  open: () => void;
19
29
  /** Готов ли инстанс PaywallUI. До mount-а Provider'а / на SSR — `false`. */
20
30
  ready: boolean;
31
+ /** Direct-checkout в процессе headless bootstrap+createCheckout (только когда
32
+ * у кнопки задан `priceId`). Render-prop может показать спиннер прямо на
33
+ * своей кнопке и задизейблить её, чтобы юзер не кликал ещё раз. Для
34
+ * не-priceId-режимов всегда false. */
35
+ processing: boolean;
21
36
  }
22
37
  /**
23
38
  * Props собственно `<button>`-рендера. Любые HTML-атрибуты — `disabled`,
@@ -48,6 +63,12 @@ export type PaywallButtonProps = CommonProps & ButtonRenderProps;
48
63
  *
49
64
  * // саппорт-форма вместо тарифов
50
65
  * <PaywallButton mode="support">Need help?</PaywallButton>
66
+ *
67
+ * // direct-checkout: хост уже выбрал план в своём UI (pricing-карточки),
68
+ * // клик ведёт прямо в провайдера, минуя layout с тарифами.
69
+ * <PaywallButton priceId={price.id} className="btn-primary">
70
+ * Get this plan
71
+ * </PaywallButton>
51
72
  * ```
52
73
  *
53
74
  * До mount-а Provider'а или на SSR кнопка рендерится с `disabled=true`
@@ -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;AAI1D;;;;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;;;;;qEAKiE;IACjE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;+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;IACf;;;2CAGuC;IACvC,UAAU,EAAE,OAAO,CAAC;CACrB;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,aAAa;eApCb,SAAS;qDAiHrB,CAAC"}
@@ -1,5 +1,5 @@
1
1
  import { ReactNode } from 'react';
2
- import { PaywallAccessResult } from '../../../sdk/src';
2
+ import { PaywallAccessResult } from '@monetize.software/sdk';
3
3
  export interface PaywallGateProps {
4
4
  /** Что показать, пока `getAccess()` не вернул ответ (initial fetch / Provider mount). */
5
5
  loading?: ReactNode;
package/dist/context.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { PaywallUI } from '../../sdk/src';
1
+ import { PaywallUI } from '@monetize.software/sdk';
2
2
  /**
3
3
  * Внутренний React Context, в который PaywallProvider кладёт PaywallUI-инстанс.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { PaywallUI } from '../../../sdk/src';
1
+ import { PaywallUI } from '@monetize.software/sdk';
2
2
  /**
3
3
  * Достаёт PaywallUI-инстанс из ближайшего {@link PaywallProvider}.
4
4
  *
@@ -1,4 +1,4 @@
1
- import { GetAccessOptions, PaywallAccessResult } from '../../../sdk/src';
1
+ import { GetAccessOptions, PaywallAccessResult } from '@monetize.software/sdk';
2
2
  /**
3
3
  * `loading` — первый fetch ещё в полёте (или Provider не готов).
4
4
  * `ready` — есть свежий ответ; `result` гарантированно non-null.
@@ -1,4 +1,4 @@
1
- import { PaywallEvent, PaywallEventHandler } from '../../../sdk/src';
1
+ import { PaywallEvent, PaywallEventHandler } from '@monetize.software/sdk';
2
2
  /**
3
3
  * Декларативная подписка на событие PaywallUI. Обёртка над `paywall.on(event, cb)`
4
4
  * с двумя важными отличиями от ручного useEffect:
@@ -0,0 +1,42 @@
1
+ import { ResolvedOffer } from '@monetize.software/sdk';
2
+ /**
3
+ * Reactive resolved offer for a given price.
4
+ *
5
+ * Returns the same shape as `paywall.getOfferForPrice(priceId)`, plus a live
6
+ * countdown — re-renders every second while there's a positive `remainingMs`,
7
+ * stops ticking when the offer expires (one final re-render brings the value
8
+ * to `null`). Re-fetches on `ready` event too, so a bootstrap refresh that
9
+ * brings new offers reflects immediately.
10
+ *
11
+ * Returns `null` when:
12
+ * - Provider isn't mounted yet (SSR / pre-mount);
13
+ * - bootstrap hasn't loaded the offers list;
14
+ * - no offer targets this price (no targeted match, no global offer);
15
+ * - the matching offer is a `duration_minutes`-only timer that hasn't been
16
+ * started yet (i.e. the paywall hasn't been opened by this user). The
17
+ * renderer writes the start on first paywall view — refreshing the page
18
+ * after the first view will then surface the countdown here.
19
+ * - the matching offer has already expired.
20
+ *
21
+ * ```tsx
22
+ * const offer = usePaywallOffer(price.id);
23
+ *
24
+ * if (!offer) return <span>{formatAmount(price.amount)}</span>;
25
+ *
26
+ * const discounted = price.amount * (1 - offer.discountPercent / 100);
27
+ * return (
28
+ * <>
29
+ * <s>{formatAmount(price.amount)}</s>
30
+ * <strong>{formatAmount(discounted)}</strong>
31
+ * <Badge>-{offer.discountPercent}%</Badge>
32
+ * {offer.remainingMs !== null && <Countdown ms={offer.remainingMs} />}
33
+ * </>
34
+ * );
35
+ * ```
36
+ *
37
+ * Implementation: a single `setInterval(1000)` ticks while there's a live
38
+ * countdown. The PaywallUI handle is read on each tick, so a Provider re-mount
39
+ * doesn't leave a stale closure.
40
+ */
41
+ export declare function usePaywallOffer(priceId: string): ResolvedOffer | null;
42
+ //# sourceMappingURL=usePaywallOffer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePaywallOffer.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallOffer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAG5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAoDrE"}
@@ -0,0 +1,16 @@
1
+ import { PaywallOffer } from '@monetize.software/sdk';
2
+ /**
3
+ * Cached offers list, refreshed on every `ready` event (= bootstrap refresh).
4
+ *
5
+ * Returns `null` before bootstrap loads, then the server-filtered offer list
6
+ * (server-side targeting on countries / emails / mode is already applied).
7
+ * Client code is still responsible for `price_id` matching (use
8
+ * `findApplicableOffer` from `@monetize.software/sdk` or the higher-level
9
+ * `usePaywallOffer(priceId)` hook).
10
+ *
11
+ * Mostly useful for hosts that want to iterate offers manually (e.g. render
12
+ * a global "Limited offer" banner above the page). For per-price strike-
13
+ * through + countdown, `usePaywallOffer(priceId)` is the right primitive.
14
+ */
15
+ export declare function usePaywallOffers(): PaywallOffer[] | null;
16
+ //# sourceMappingURL=usePaywallOffers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePaywallOffers.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallOffers.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAG3D;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,YAAY,EAAE,GAAG,IAAI,CAgBxD"}
@@ -1,4 +1,4 @@
1
- import { PaywallPrice } from '../../../sdk/src';
1
+ import { PaywallPrice } from '@monetize.software/sdk';
2
2
  /**
3
3
  * `prices` — кешированный snapshot bootstrap.prices (`null` до первого fetch'а
4
4
  * или когда инстанс ещё не готов).
@@ -1,4 +1,4 @@
1
- import { PaywallStateSnapshot } from '../../../sdk/src';
1
+ import { PaywallStateSnapshot } from '@monetize.software/sdk';
2
2
  /**
3
3
  * Подписка на состояние модалки пейвола: открыта/закрыта, текущий view,
4
4
  * последняя ошибка.
@@ -1 +1 @@
1
- {"version":3,"file":"usePaywallState.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAYnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,IAAI,oBAAoB,CAmBtD"}
1
+ {"version":3,"file":"usePaywallState.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallState.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AAiBnE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,IAAI,oBAAoB,CAmBtD"}
@@ -1,4 +1,4 @@
1
- import { PaywallUI } from '../../../sdk/src';
1
+ import { PaywallUI } from '@monetize.software/sdk';
2
2
  type TrialStatus = NonNullable<ReturnType<PaywallUI['getTrialStatus']>>;
3
3
  /**
4
4
  * Текущий статус триала ({@link TrialStatus}) с автоматическим ре-рендером на
@@ -1,30 +1,50 @@
1
- import { PaywallUser } from '../../../sdk/src';
1
+ import { AuthSession, PaywallUser } from '@monetize.software/sdk';
2
2
  /**
3
- * Подписка на текущего юзера пейвола (sync snapshot + автоматический ре-рендер
4
- * на любой userChange — bootstrap, /me refresh, после-checkout watcher).
3
+ * Состояние «кто такой текущий пользователь» с точки зрения хоста.
5
4
  *
6
- * Возвращает `null` до первого ответа сети или когда инстанс ещё не готов
7
- * (SSR / до useEffect Provider / Provider не оборачивает дерево с инстансом).
5
+ * Discriminated union намеренно совмещает три источника: готовность инстанса
6
+ * PaywallUI (Provider mount), наличие session у managed-auth и `getCachedUser()`
7
+ * от bootstrap'а. Это убирает у хоста нужду различать «пейвол ещё грузится»
8
+ * vs «никого нет» вручную — типы сужают каждый случай.
8
9
  *
9
- * Удобно для подсветки текущего плана / e-mail юзера в собственном UI без
10
- * необходимости держать дублирующий state и руками подписываться на
11
- * `paywall.on('userChange', ...)`.
10
+ * - `loading` Provider ещё не смонтировал PaywallUI (SSR / pre-mount /
11
+ * dev-double-mount cleanup). На этом этапе показывать skeleton.
12
+ * - `guest` — у пейвола нет identity:
13
+ * • managed-auth: `auth.getCachedSession()` вернул null;
14
+ * • hybrid (без managed-auth): bootstrap прошёл, но user-snapshot пуст.
15
+ * В этом состоянии валидно показать CTA «Sign in» / `<PaywallButton mode="signin">`.
16
+ * - `signed_in` — есть identity. `user` — последний снимок из BillingClient
17
+ * (может быть `null`, пока /me-refresh после signIn в полёте — UI должен
18
+ * показать skeleton, не «sign-in» CTA). `session` — managed-auth session
19
+ * или `null` для hybrid-режима.
12
20
  *
21
+ * Хост обычно делает три проверки подряд:
13
22
  * ```tsx
14
- * const user = usePaywallUser();
15
- * if (user?.has_active_subscription) {
16
- * return <ProBadge plan={user.active_subscription?.plan_name} />;
17
- * }
23
+ * const account = usePaywallUser();
24
+ * if (account.status === 'loading') return <Skeleton />;
25
+ * if (account.status === 'guest') return <SignInCTA />;
26
+ * // account.user может быть null, пока /me грузится — показать skeleton тут же.
27
+ * if (!account.user) return <Skeleton />;
28
+ * return <Profile user={account.user} />;
18
29
  * ```
19
30
  *
20
- * Реализация поверх `paywall.on('userChange', cb)` + `billing.getCachedUser()`.
21
- * `paywall.on` не делает initial replay'я, поэтому useSyncExternalStore сам
22
- * читает старт-snapshot через getSnapshot без лишних cb-вызовов.
23
- *
24
- * Ссылочная стабильность: BillingClient сравнивает user shape перед update'ом
25
- * (`sameUser`), так что между неизменными обновлениями `getCachedUser()`
26
- * возвращает ===-равный объект. Это гарантирует, что useSyncExternalStore
27
- * не дёргает ре-рендер при no-op refresh'ах.
31
+ * Реализация подписана и на `userChange`, и на `authChange` любой источник
32
+ * меняющий status триггерит rerender. Snapshot reference закеширован через
33
+ * useRef, чтобы useSyncExternalStore не словил infinite-loop на новых
34
+ * объектах при каждом getSnapshot.
28
35
  */
29
- export declare function usePaywallUser(): PaywallUser | null;
36
+ export type PaywallUserState = {
37
+ status: 'loading';
38
+ user: null;
39
+ session: null;
40
+ } | {
41
+ status: 'guest';
42
+ user: null;
43
+ session: null;
44
+ } | {
45
+ status: 'signed_in';
46
+ user: PaywallUser | null;
47
+ session: AuthSession | null;
48
+ };
49
+ export declare function usePaywallUser(): PaywallUserState;
30
50
  //# sourceMappingURL=usePaywallUser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"usePaywallUser.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallUser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAgBnD"}
1
+ {"version":3,"file":"usePaywallUser.d.ts","sourceRoot":"","sources":["../../src/hooks/usePaywallUser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGvE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAChD;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,IAAI,CAAA;CAAE,GAC9C;IACE,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,WAAW,GAAG,IAAI,CAAC;IACzB,OAAO,EAAE,WAAW,GAAG,IAAI,CAAC;CAC7B,CAAC;AAKN,wBAAgB,cAAc,IAAI,gBAAgB,CA8EjD"}
@@ -1,4 +1,4 @@
1
- import { PaywallUI } from '../../../sdk/src';
1
+ import { PaywallUI } from '@monetize.software/sdk';
2
2
  type VisibilityStatus = NonNullable<ReturnType<PaywallUI['getVisibility']>>;
3
3
  /**
4
4
  * Server-computed visibility-снимок ({@link VisibilityStatus}): попадает ли
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 f=require("react/jsx-runtime"),s=require("react"),B=require("@monetize.software/sdk"),S=s.createContext(null);S.displayName="PaywallContext";const k=s.createContext(!1);k.displayName="PaywallProviderMarker";function _(e){const t="instance"in e?e.instance:void 0,n="options"in e?e.options:void 0,[r,l]=s.useState(t??null);return s.useEffect(()=>{if(t){l(t);return}if(!n)return;const a=new B.PaywallUI(n);return l(a),()=>{a.destroy(),l(null)}},[t]),f.jsx(k.Provider,{value:!0,children:f.jsx(S.Provider,{value:r,children:e.children})})}function y(){const e=s.useContext(k),t=s.useContext(S);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 t}const C={open:!1,view:null,error:null,processing:!1};function E(){const e=y(),t=s.useCallback(r=>e?e.onStateChange(r,{immediate:"none"}):()=>{},[e]),n=s.useCallback(()=>e?e.getState():C,[e]);return s.useSyncExternalStore(t,n,()=>C)}const P={status:"loading",user:null,session:null},g={status:"guest",user:null,session:null};function V(){const e=y(),t=s.useRef(P),n=s.useCallback(l=>{if(!e)return()=>{};const a=e.on("userChange",()=>l()),u=e.auth?e.on("authChange",()=>l()):null;return()=>{a(),u?.()}},[e]),r=s.useCallback(()=>{if(!e)return t.current=P,P;const l=e.billing.getCachedUser();if(e.auth){const a=e.auth.getCachedSession();if(!a)return t.current=g,g;const u=t.current;if(u.status==="signed_in"&&u.user===l&&u.session===a)return u;const i={status:"signed_in",user:l,session:a};return t.current=i,i}if(l){const a=t.current;if(a.status==="signed_in"&&a.user===l&&a.session===null)return a;const u={status:"signed_in",user:l,session:null};return t.current=u,u}return t.current=g,g},[e]);return s.useSyncExternalStore(n,r,U)}function U(){return P}function F(e,t){const n=y(),r=s.useRef(t);r.current=t,s.useEffect(()=>{if(n)return n.on(e,l=>{r.current(l)})},[n,e])}const x={status:"loading",result:null};function m(e={}){const t=y(),[n,r]=s.useState(x),l=e.skipTrial===!0,a=e.skipVisibility===!0;return s.useEffect(()=>{if(!t){r(x);return}const u=new AbortController;let i=!1;const o=()=>{t.getAccess({skipTrial:l,skipVisibility:a,signal:u.signal}).then(p=>{i||u.signal.aborted||r({status:"ready",result:p})}).catch(()=>{})};o();const c=t.on("userChange",o),d=t.on("purchase_completed",o);return()=>{i=!0,u.abort(),c(),d()}},[t,l,a]),n}function I(){const e=y(),[t,n]=s.useState(()=>({prices:e?.getCachedPrices()??null,loading:!0,error:null}));return s.useEffect(()=>{if(!e){n({prices:null,loading:!0,error:null});return}const r=e.getCachedPrices();n({prices:r,loading:r===null,error:null});const l=new AbortController;let a=!1;(()=>{e.getPrices({signal:l.signal}).then(o=>{a||n({prices:o,loading:!1,error:null})}).catch(o=>{a||l.signal.aborted||n(c=>({prices:c.prices,loading:!1,error:o instanceof Error?o:new Error(String(o))}))})})();const i=e.on("ready",()=>{const o=e.getCachedPrices();o&&n({prices:o,loading:!1,error:null})});return()=>{a=!0,l.abort(),i()}},[e]),t}function M(e){const t=y(),[n,r]=s.useState(()=>t?t.getOfferForPrice(e):null),l=s.useRef(e);return l.current=e,s.useEffect(()=>{if(!t){r(null);return}let a=!1;const u=()=>{if(a)return;const d=t.getOfferForPrice(l.current);return r(d),d},i=u(),o=t.on("ready",()=>u());let c=null;return i&&i.remainingMs!==null&&(c=setInterval(()=>{const d=u();(!d||d.remainingMs===null||d.remainingMs<=0)&&(c&&clearInterval(c),c=null)},1e3)),()=>{a=!0,o(),c&&clearInterval(c)}},[t,e]),n}function G(){const e=y(),t=s.useCallback(r=>e?e.on("ready",()=>r()):()=>{},[e]),n=s.useCallback(()=>e?e.getCachedOffers():null,[e]);return s.useSyncExternalStore(t,n,N)}function N(){return null}function q(){const e=y(),[t,n]=s.useState(()=>e?.getTrialStatus()??null),r=s.useCallback(()=>{if(!e){n(null);return}n(e.getTrialStatus())},[e]);return s.useEffect(()=>{if(!e){n(null);return}r();const l=e.on("trial_blocked",r),a=e.on("trial_expired",r);return()=>{l(),a()}},[e,r]),t}function D(){const e=y(),[t,n]=s.useState(()=>e?.getVisibility()??null),r=s.useCallback(()=>{if(!e){n(null);return}n(e.getVisibility())},[e]);return s.useEffect(()=>{if(!e){n(null);return}r();const l=e.on("ready",r),a=e.on("visibility_blocked",r);return()=>{l(),a()}},[e,r]),t}function L(e){const t=y(),n=m(),r=n.status==="ready"&&n.result.access==="blocked",l=e.openOnBlocked===!0&&r;if(s.useEffect(()=>{l&&t&&t.open()},[l,t]),n.status==="loading")return f.jsx(f.Fragment,{children:e.loading??null});if(n.result.access==="granted")return f.jsx(f.Fragment,{children:e.children});const a=e.fallback;return typeof a=="function"?f.jsx(f.Fragment,{children:a({result:n.result,open:()=>t?.open()})}):f.jsx(f.Fragment,{children:a??null})}const O=s.forwardRef(function(t,n){const r=y(),l=E(),{mode:a="paywall",priceId:u,identity:i,renew:o,skipTrial:c,skipVisibility:d,render:p,onClick:R,disabled:T,...A}=t,b=r!==null,h=!!u&&l.processing,w={identity:i,renew:o,skipTrial:c,skipVisibility:d},v=()=>{if(r){if(u){r.checkout(u,w);return}switch(a){case"support":r.openSupport(w);return;case"auth":case"signin":r.openSignin(w);return;case"signup":r.openSignup(w);return;default:r.open(w)}}};return p?p({open:v,ready:b,processing:h}):f.jsx("button",{ref:n,type:"button",disabled:T||!b||h,"aria-busy":!b||h?!0:void 0,onClick:j=>{v(),R?.(j)},...A})}),H=s.forwardRef(function(t,n){return f.jsx(O,{...t,mode:"support",ref:n})});exports.PaywallButton=O;exports.PaywallGate=L;exports.PaywallProvider=_;exports.PaywallSupportButton=H;exports.usePaywall=y;exports.usePaywallAccess=m;exports.usePaywallEvent=F;exports.usePaywallOffer=M;exports.usePaywallOffers=G;exports.usePaywallPrices=I;exports.usePaywallState=E;exports.usePaywallTrial=q;exports.usePaywallUser=V;exports.usePaywallVisibility=D;
2
2
  //# sourceMappingURL=index.cjs.map