@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 +76 -40
- package/dist/PaywallProvider.d.ts +1 -1
- package/dist/components/PaywallButton.d.ts +24 -3
- package/dist/components/PaywallButton.d.ts.map +1 -1
- package/dist/components/PaywallGate.d.ts +1 -1
- package/dist/context.d.ts +1 -1
- package/dist/hooks/usePaywall.d.ts +1 -1
- package/dist/hooks/usePaywallAccess.d.ts +1 -1
- package/dist/hooks/usePaywallEvent.d.ts +1 -1
- package/dist/hooks/usePaywallOffer.d.ts +42 -0
- package/dist/hooks/usePaywallOffer.d.ts.map +1 -0
- package/dist/hooks/usePaywallOffers.d.ts +16 -0
- package/dist/hooks/usePaywallOffers.d.ts.map +1 -0
- package/dist/hooks/usePaywallPrices.d.ts +1 -1
- package/dist/hooks/usePaywallState.d.ts +1 -1
- package/dist/hooks/usePaywallState.d.ts.map +1 -1
- package/dist/hooks/usePaywallTrial.d.ts +1 -1
- package/dist/hooks/usePaywallUser.d.ts +41 -21
- package/dist/hooks/usePaywallUser.d.ts.map +1 -1
- package/dist/hooks/usePaywallVisibility.d.ts +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +228 -146
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
# @monetize.software/sdk-react
|
|
2
2
|
|
|
3
|
-
React bindings
|
|
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 (
|
|
6
|
-
- **React**: >= 18,
|
|
7
|
-
- **SSR**:
|
|
8
|
-
|
|
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
|
|
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
|
|
40
|
-
|
|
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
|
-
//
|
|
64
|
+
// Option 1 — Provider creates the instance itself
|
|
50
65
|
<PaywallProvider options={{ paywallId, apiOrigin, auth: true }}>
|
|
51
66
|
|
|
52
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
|
80
|
+
| Hook | Returns | When it triggers a rerender |
|
|
64
81
|
|---|---|---|
|
|
65
|
-
| `usePaywall()` | `PaywallUI \| null` |
|
|
66
|
-
| `usePaywallState()` | `{ open, view, error }` |
|
|
67
|
-
| `usePaywallUser()` | `
|
|
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)` | — |
|
|
91
|
+
| `usePaywallEvent(event, handler)` | — | subscribes with a stable handler ref |
|
|
73
92
|
|
|
74
|
-
|
|
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
|
-
|
|
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} //
|
|
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
|
-
|
|
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`
|
|
124
|
+
`mode` switches between `open()` / `openSupport()` / `openSignin()` / `openSignup()`:
|
|
103
125
|
|
|
104
126
|
```tsx
|
|
105
127
|
<PaywallButton mode="support">Need help?</PaywallButton>
|
|
106
|
-
<PaywallButton mode="
|
|
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'; //
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
161
|
+
## SDK contract guard
|
|
128
162
|
|
|
129
|
-
`pnpm typecheck`
|
|
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
|
-
|
|
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
|
-
|
|
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) —
|
|
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 { ButtonHTMLAttributes, ReactElement, ReactNode } from 'react';
|
|
2
|
-
import { OpenOptions } from '
|
|
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,
|
|
11
|
-
|
|
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;
|
|
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 '
|
|
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 { GetAccessOptions, PaywallAccessResult } from '
|
|
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 '
|
|
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 +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;
|
|
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,30 +1,50 @@
|
|
|
1
|
-
import { PaywallUser } from '
|
|
1
|
+
import { AuthSession, PaywallUser } from '@monetize.software/sdk';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
* на любой userChange — bootstrap, /me refresh, после-checkout watcher).
|
|
3
|
+
* Состояние «кто такой текущий пользователь» с точки зрения хоста.
|
|
5
4
|
*
|
|
6
|
-
*
|
|
7
|
-
* (
|
|
5
|
+
* Discriminated union намеренно совмещает три источника: готовность инстанса
|
|
6
|
+
* PaywallUI (Provider mount), наличие session у managed-auth и `getCachedUser()`
|
|
7
|
+
* от bootstrap'а. Это убирает у хоста нужду различать «пейвол ещё грузится»
|
|
8
|
+
* vs «никого нет» вручную — типы сужают каждый случай.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
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
|
|
15
|
-
* if (
|
|
16
|
-
*
|
|
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
|
-
* Реализация
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
|
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;
|
|
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"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use client";"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const
|
|
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
|