@monetize.software/sdk 3.0.0-alpha.0
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/LICENSE +21 -0
- package/README.md +106 -0
- package/dist/chunks/PaywallUI-BHp9afFC.js +2209 -0
- package/dist/chunks/PaywallUI-BHp9afFC.js.map +1 -0
- package/dist/chunks/PaywallUI-Dr-6q-HL.js +26 -0
- package/dist/chunks/PaywallUI-Dr-6q-HL.js.map +1 -0
- package/dist/core.cjs +2 -0
- package/dist/core.cjs.map +1 -0
- package/dist/core.d.ts +1229 -0
- package/dist/core.js +1856 -0
- package/dist/core.js.map +1 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +1695 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/ui.cjs +2 -0
- package/dist/ui.cjs.map +1 -0
- package/dist/ui.d.ts +1615 -0
- package/dist/ui.js +6 -0
- package/dist/ui.js.map +1 -0
- package/package.json +96 -0
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,1229 @@
|
|
|
1
|
+
declare type Acquiring = 'stripe' | 'paddle' | 'chargebee' | 'overpay' | 'freemius';
|
|
2
|
+
|
|
3
|
+
export declare class ApiClient {
|
|
4
|
+
private opts;
|
|
5
|
+
constructor(opts: ApiClientOptions);
|
|
6
|
+
request<T>(path: string, init?: RequestInit): Promise<T>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export declare interface ApiClientOptions {
|
|
10
|
+
apiOrigin: string;
|
|
11
|
+
paywallId: string;
|
|
12
|
+
getAuthToken?: () => string | null | Promise<string | null>;
|
|
13
|
+
capabilities?: string[];
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export declare interface ApiGatewayCallParams {
|
|
18
|
+
/** UUID api-провайдера из платформы (`paywall_internal_api_providers.id`). */
|
|
19
|
+
providerId: string;
|
|
20
|
+
/** Путь после провайдера: `v1/chat/completions`, `messages`, и т.д.
|
|
21
|
+
* Конкатенируется через `/`. Пустая строка/undefined = root провайдера. */
|
|
22
|
+
path?: string;
|
|
23
|
+
method?: 'GET' | 'POST';
|
|
24
|
+
/** JSON-сериализуемый объект → application/json. FormData → multipart с
|
|
25
|
+
* авто-boundary. ReadableStream/Blob/string — пробрасываются как есть.
|
|
26
|
+
* Если undefined и method='POST' — отправляется пустое тело. */
|
|
27
|
+
body?: unknown;
|
|
28
|
+
/** Дополнительные хедеры. Перетирают наши, кроме Authorization (его всегда
|
|
29
|
+
* ставим из auth) и X-Paywall-Id. */
|
|
30
|
+
headers?: Record<string, string>;
|
|
31
|
+
signal?: AbortSignal;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export declare class ApiGatewayClient {
|
|
35
|
+
readonly paywallId: string;
|
|
36
|
+
readonly apiOrigin: string;
|
|
37
|
+
private auth;
|
|
38
|
+
private userId;
|
|
39
|
+
private capabilities;
|
|
40
|
+
private customFetch;
|
|
41
|
+
private onChargeSuccess;
|
|
42
|
+
private onQuotaExceeded;
|
|
43
|
+
constructor(opts: ApiGatewayClientOptions);
|
|
44
|
+
call(params: ApiGatewayCallParams): Promise<Response>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export declare interface ApiGatewayClientOptions {
|
|
48
|
+
paywallId: string;
|
|
49
|
+
apiOrigin?: string;
|
|
50
|
+
/** AuthClient — Bearer добавляется автоматически. На 401 от gateway клиент
|
|
51
|
+
* не делает refresh: AuthClient уже сделал lazy-refresh в getAccessToken. */
|
|
52
|
+
auth?: AuthClient;
|
|
53
|
+
/** Headless-сценарий или legacy-флоу: явный userId вместо Bearer.
|
|
54
|
+
* Передаётся как `X-User-ID`. Если задан и `auth` — Bearer выигрывает. */
|
|
55
|
+
userId?: string;
|
|
56
|
+
capabilities?: string[];
|
|
57
|
+
fetch?: typeof fetch;
|
|
58
|
+
/** Хук для оптимистичного декремента балансов в BillingClient.
|
|
59
|
+
* ApiGatewayClient его дёргает на 200 (success), передавая queryType из
|
|
60
|
+
* ответа (если бэк его прислал в `X-Query-Type`) или undefined.
|
|
61
|
+
* Парсить body для извлечения queryType ApiGatewayClient НЕ умеет — это
|
|
62
|
+
* было бы лишним чтением body, а главное — body может быть стримом. */
|
|
63
|
+
onChargeSuccess?: (queryType: string | undefined) => void;
|
|
64
|
+
/** Хук для рефетча балансов после 402. BillingClient ходит к /balances
|
|
65
|
+
* и обновляет state, чтобы UI показал актуальный счётчик. */
|
|
66
|
+
onQuotaExceeded?: (err: QuotaExceededError) => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export declare type AuthChangeListener = (session: AuthSession | null) => void;
|
|
70
|
+
|
|
71
|
+
export declare class AuthClient {
|
|
72
|
+
readonly paywallId: string;
|
|
73
|
+
readonly apiOrigin: string;
|
|
74
|
+
private storage;
|
|
75
|
+
private api;
|
|
76
|
+
private openPopup;
|
|
77
|
+
private session;
|
|
78
|
+
private hydrated;
|
|
79
|
+
private inflightRefresh;
|
|
80
|
+
/** Дедупликация параллельных signInAnonymously: два click'а на «Войти как
|
|
81
|
+
* гость» должны попасть в одного юзера, не плодить двух (двойная капча +
|
|
82
|
+
* второй /signup создал бы вторую запись с потерянным trial-балансом). */
|
|
83
|
+
private inflightAnonSignin;
|
|
84
|
+
private listeners;
|
|
85
|
+
private storageUnwatch;
|
|
86
|
+
private destroyed;
|
|
87
|
+
/** Pending OAuth flows: state → {verifier, userMeta, startedAt}. Между
|
|
88
|
+
* startOAuthFlow и completeOAuthFlow. GC'атся через OAUTH_FLOW_TTL_MS. */
|
|
89
|
+
private oauthFlows;
|
|
90
|
+
constructor(opts: AuthClientOptions);
|
|
91
|
+
/**
|
|
92
|
+
* Подписывается на изменения session-ключа в storage из других контекстов:
|
|
93
|
+
* - Chrome Extension: `chrome.storage.onChanged` шарится popup ↔ background ↔
|
|
94
|
+
* options ↔ content script. Логин в одном контексте → остальные сразу
|
|
95
|
+
* эмитят onAuthChange и в getAccessToken отдают свежий Bearer.
|
|
96
|
+
* - Web: `window.storage` event фаерится в ДРУГИХ вкладках того же origin'а
|
|
97
|
+
* (своя вкладка свой setItem не получает — петель нет).
|
|
98
|
+
*
|
|
99
|
+
* Loop-guard: сравниваем content по полям session перед applySession, чтобы
|
|
100
|
+
* не фрить лишних onAuthChange при идентичной перезаписи. Вызовы из других
|
|
101
|
+
* контекстов с тем же содержимым (пересохранение) — no-op.
|
|
102
|
+
*/
|
|
103
|
+
private startStorageWatch;
|
|
104
|
+
private applyExternalSession;
|
|
105
|
+
/**
|
|
106
|
+
* Promise гидратации session из storage. До его resolve getCachedSession()
|
|
107
|
+
* может ещё вернуть null. getAccessToken/refresh/signOut/sign* awaitят его
|
|
108
|
+
* сами, наружу выставляем для UI'я, чтобы он мог дождаться initial state
|
|
109
|
+
* прежде чем рисовать «logged-out» вспышку.
|
|
110
|
+
*/
|
|
111
|
+
ready(): Promise<void>;
|
|
112
|
+
/** Sync snapshot без сетевых запросов. null = разлогинен или ещё не гидрировались. */
|
|
113
|
+
getCachedSession(): AuthSession | null;
|
|
114
|
+
getCachedUser(): AuthUser | null;
|
|
115
|
+
/**
|
|
116
|
+
* access_token для Authorization-хедера. Если до expiry < REFRESH_LEEWAY_MS,
|
|
117
|
+
* делает lazy refresh. null = разлогинен или refresh упал на 401 (refresh
|
|
118
|
+
* token revoked) — вызывающему стоит редиректить на логин.
|
|
119
|
+
*
|
|
120
|
+
* Сетевые/5xx ошибки refresh бросаются — текущий access ещё валиден,
|
|
121
|
+
* вызывающий может попробовать запрос с ним; следующий getAccessToken
|
|
122
|
+
* попробует refresh снова.
|
|
123
|
+
*/
|
|
124
|
+
getAccessToken(): Promise<string | null>;
|
|
125
|
+
signInWithEmail(input: {
|
|
126
|
+
email: string;
|
|
127
|
+
password: string;
|
|
128
|
+
userMeta?: Record<string, string>;
|
|
129
|
+
/** Idempotency-key (UUID) — повторный submit при двойном клике вернёт
|
|
130
|
+
* тот же результат вместо второго запроса в GoTrue. Без передачи
|
|
131
|
+
* inflight-дедупликации нет; SDK не дедуплицирует auth по умолчанию,
|
|
132
|
+
* потому что email/password можно поменять между кликами. */
|
|
133
|
+
idempotencyKey?: string;
|
|
134
|
+
}): Promise<AuthSession>;
|
|
135
|
+
/**
|
|
136
|
+
* Signup. Если в Supabase включён email confirm — сервер возвращает
|
|
137
|
+
* `{status: 'confirmation_required', user}` и НЕ выдаёт токены. В этом
|
|
138
|
+
* случае setSession не зовётся, юзер должен пройти OTP/magic-link
|
|
139
|
+
* (отдельная фича следующего PR).
|
|
140
|
+
*/
|
|
141
|
+
signUp(input: {
|
|
142
|
+
email: string;
|
|
143
|
+
password: string;
|
|
144
|
+
userMeta?: Record<string, string>;
|
|
145
|
+
/** Idempotency-key (UUID). Защита от двойного клика на «Sign Up» —
|
|
146
|
+
* без неё бэк может создать trial-balances и отправить confirmation-email
|
|
147
|
+
* дважды. */
|
|
148
|
+
idempotencyKey?: string;
|
|
149
|
+
}): Promise<SignUpResult>;
|
|
150
|
+
/**
|
|
151
|
+
* Повторная отправка confirmation-email после signUp с включённым
|
|
152
|
+
* email-confirm. Использует GoTrue `/resend` type='signup'. Бэк всегда
|
|
153
|
+
* отдаёт ok (anti-enumeration), кроме 429 при rate-limit (~1 раз/мин на
|
|
154
|
+
* email на стороне Supabase). Host обрабатывает 429 показом «подождите
|
|
155
|
+
* минуту»; остальное — как success.
|
|
156
|
+
*/
|
|
157
|
+
resendConfirmation(input: {
|
|
158
|
+
email: string;
|
|
159
|
+
/** Защита от двойного клика. */
|
|
160
|
+
idempotencyKey?: string;
|
|
161
|
+
}): Promise<void>;
|
|
162
|
+
/**
|
|
163
|
+
* Email-OTP / signin без password. Шлёт 6-значный код юзеру на email.
|
|
164
|
+
* Anti-enumeration: бэк всегда отдаёт ok, поэтому метод не различает
|
|
165
|
+
* «email не существует» и «отправлено» — следующий шаг (verifyOtp) сам
|
|
166
|
+
* упадёт invalid_otp если юзера нет. Под капотом GoTrue с create_user=true,
|
|
167
|
+
* так что новые юзеры через OTP логинятся за один шаг (отправка → ввод
|
|
168
|
+
* кода → session).
|
|
169
|
+
*/
|
|
170
|
+
sendOtp(input: {
|
|
171
|
+
email: string;
|
|
172
|
+
createUser?: boolean;
|
|
173
|
+
userMeta?: Record<string, unknown>;
|
|
174
|
+
}): Promise<void>;
|
|
175
|
+
/**
|
|
176
|
+
* Верификация OTP. type='email' (signin/signup-by-otp) — после успеха
|
|
177
|
+
* setSession и onAuthChange. type='recovery' — после /requestPasswordReset:
|
|
178
|
+
* выдаётся короткоживущий access_token для последующего updatePassword.
|
|
179
|
+
* Мы храним recovery-session так же, как обычную: SDK не различает «можно
|
|
180
|
+
* залогиниться» vs «можно сменить пароль» — это одна и та же session.
|
|
181
|
+
*/
|
|
182
|
+
verifyOtp(input: {
|
|
183
|
+
email: string;
|
|
184
|
+
token: string;
|
|
185
|
+
type?: OtpVerifyType;
|
|
186
|
+
userMeta?: Record<string, string>;
|
|
187
|
+
}): Promise<AuthSession>;
|
|
188
|
+
/**
|
|
189
|
+
* Запрос recovery email. Бэк всегда ok, чтобы не палить enumeration.
|
|
190
|
+
* Юзер вводит код из письма в SDK-ui → verifyOtp({type:'recovery'}) →
|
|
191
|
+
* получает session → updatePassword.
|
|
192
|
+
*/
|
|
193
|
+
requestPasswordReset(input: {
|
|
194
|
+
email: string;
|
|
195
|
+
}): Promise<void>;
|
|
196
|
+
/**
|
|
197
|
+
* Меняет пароль текущей session. Работает после verifyOtp({type:'recovery'})
|
|
198
|
+
* (recovery-session) и после обычного логина — оба случая дают валидный
|
|
199
|
+
* access_token. Если session нет — бросаем PaywallError('not_authenticated')
|
|
200
|
+
* до сетевого запроса, чтобы UI не дёргал бэк впустую.
|
|
201
|
+
*/
|
|
202
|
+
updatePassword(input: {
|
|
203
|
+
password: string;
|
|
204
|
+
}): Promise<void>;
|
|
205
|
+
/**
|
|
206
|
+
* Анонимный signin (Supabase user без email). Лестница попыток:
|
|
207
|
+
*
|
|
208
|
+
* 1. Если уже залогинены анонимно (session.user.is_anonymous === true) —
|
|
209
|
+
* no-op, возвращаем текущую session. Идемпотентно для UI'я, который
|
|
210
|
+
* может звать signInAnonymously() в render-loop'е, не отслеживая state.
|
|
211
|
+
*
|
|
212
|
+
* 2. Resume через сохранённый anon refresh_token (`STORAGE_KEYS.anonRefreshToken`).
|
|
213
|
+
* Если токен есть — пробуем `/auth/refresh` им. Success → setSession,
|
|
214
|
+
* возвращаем юзера ТОГО ЖЕ id что был при предыдущем anon signin'е
|
|
215
|
+
* (обещание из user-фидбека: «если разлогинился из анонимного —
|
|
216
|
+
* логинить в этот же акк»).
|
|
217
|
+
*
|
|
218
|
+
* 3. Иначе → POST /auth/anonymous/signin → setSession + сохраняем
|
|
219
|
+
* refresh_token в anonRefreshToken.
|
|
220
|
+
*
|
|
221
|
+
* `captchaToken` сейчас не требуется — captcha protection в Supabase
|
|
222
|
+
* отключена, защита от per-IP abuse держится на rate-limit'е Supabase'а
|
|
223
|
+
* (30/час per real-IP, см. IP forwarding setup в supabaseAuthRest.ts) +
|
|
224
|
+
* CF Bot Fight Mode на edge. Поле оставлено optional для forward-compat:
|
|
225
|
+
* когда сервер начнёт возвращать challenge_required в риск-сценариях,
|
|
226
|
+
* SDK сможет передать proof-of-something обратно без breaking change.
|
|
227
|
+
*
|
|
228
|
+
* `forceCaptcha: true` пропускает шаги 1-2 и сразу делает /signin (создаёт
|
|
229
|
+
* нового anon-юзера). Используется в switch-account flow. Имя поля исторически
|
|
230
|
+
* остаётся `forceCaptcha`, хотя капчи там больше нет — менять имя ломает
|
|
231
|
+
* host-сигнатуру; смысл «принудительно новая anon-сессия» сохранён.
|
|
232
|
+
*
|
|
233
|
+
* Параллельные вызовы дедуплицируются через `inflightAnonSignin` — два
|
|
234
|
+
* click'а на «Войти как гость» не создадут двух anon-юзеров (два /signup =
|
|
235
|
+
* два user_id, второй trial-баланс улетает в нирвану).
|
|
236
|
+
*/
|
|
237
|
+
signInAnonymously(input?: {
|
|
238
|
+
captchaToken?: string;
|
|
239
|
+
userMeta?: Record<string, string>;
|
|
240
|
+
forceCaptcha?: boolean;
|
|
241
|
+
}): Promise<AuthSession>;
|
|
242
|
+
/**
|
|
243
|
+
* Внутренний resume — пробует /auth/refresh с сохранённым anon refresh_token.
|
|
244
|
+
* Возвращает session при успехе, null если токена нет или он отозван (401).
|
|
245
|
+
* Сетевые ошибки бросает наружу — caller сам решает, ретраить или просить
|
|
246
|
+
* пользователя пройти капчу.
|
|
247
|
+
*/
|
|
248
|
+
private resumeAnonymous;
|
|
249
|
+
/**
|
|
250
|
+
* Анон → email/password upgrade. Сохраняет тот же auth.user.id, балансы
|
|
251
|
+
* и trial-quotas остаются. Поведение зависит от Supabase email-confirm
|
|
252
|
+
* настройки проекта:
|
|
253
|
+
*
|
|
254
|
+
* - Confirmation OFF → backend сразу обновляет email + password в auth.users.
|
|
255
|
+
* Возвращаем `kind: 'updated'`, локально патчим session.user.email +
|
|
256
|
+
* is_anonymous=false (текущий access_token остаётся валидным, перевыдавать
|
|
257
|
+
* не нужно — GoTrue не вращает токены на updateUser).
|
|
258
|
+
*
|
|
259
|
+
* - Confirmation ON → backend отдаёт `confirmation_required`. Текущая
|
|
260
|
+
* session ОСТАЁТСЯ анонимной до клика юзером по confirmation-ссылке.
|
|
261
|
+
* Password применяется сразу (можно дальше логиниться по нему даже до
|
|
262
|
+
* confirm'а). После клика — следующий /auth/refresh подтянет обновлённый
|
|
263
|
+
* is_anonymous=false из JWT (refresh не возвращает user, так что
|
|
264
|
+
* UI может явно подёргать `auth.refresh()` через минуту-другую, либо
|
|
265
|
+
* дождаться lazy-refresh при истечении access).
|
|
266
|
+
*
|
|
267
|
+
* Без активной session бросает `not_authenticated`. Дедупликации нет —
|
|
268
|
+
* двойной submit формы UI должен предотвратить idempotencyKey'ом.
|
|
269
|
+
*/
|
|
270
|
+
upgradeAnonymousToEmail(input: {
|
|
271
|
+
email: string;
|
|
272
|
+
password: string;
|
|
273
|
+
userMeta?: Record<string, string>;
|
|
274
|
+
/** Idempotency-key для защиты от двойного клика. GoTrue PUT /user не
|
|
275
|
+
* идемпотентен сам по себе — повторный submit при двойном клике может
|
|
276
|
+
* вызвать race с email-confirmation (две confirmation-ссылки на тот же
|
|
277
|
+
* адрес). UI должен передать UUID. */
|
|
278
|
+
idempotencyKey?: string;
|
|
279
|
+
}): Promise<UpgradeAnonymousResult>;
|
|
280
|
+
/**
|
|
281
|
+
* OAuth signin через popup с PKCE. Жизненный цикл:
|
|
282
|
+
* 1. Генерим verifier+challenge+state локально (verifier не уходит на бэк
|
|
283
|
+
* до /exchange — это защита от перехвата code'а).
|
|
284
|
+
* 2. POST /oauth/init с challenge → бэк отдаёт authorize_url.
|
|
285
|
+
* 3. Открываем popup, ждём postMessage с типом 'pw-oauth' и нашим state.
|
|
286
|
+
* 4. POST /oauth/exchange с {auth_code, code_verifier} → session.
|
|
287
|
+
*
|
|
288
|
+
* Таймаут — 5 минут от открытия popup'а. Если юзер закрыл popup до конца
|
|
289
|
+
* флоу (window.closed → true) — бросаем PaywallError('oauth_cancelled').
|
|
290
|
+
* Параллельные вызовы НЕ дедупятся — каждый открывает свой popup; вызывать
|
|
291
|
+
* параллельно не имеет смысла, но защищаться от этого код не должен.
|
|
292
|
+
*
|
|
293
|
+
* onPopupOpened вызывается сразу после успешного window.open (до ожидания
|
|
294
|
+
* code'а). UI использует это, чтобы сбросить loading-state кнопки: дальше
|
|
295
|
+
* ответственность за флоу у popup'а, основная страница не должна висеть.
|
|
296
|
+
* Если popup'ом не вернулся code (юзер закрыл вкладку, closed-detection
|
|
297
|
+
* не сработал из-за COOP-severance) — promise дойдёт до oauth_timeout
|
|
298
|
+
* через 5 минут, но кнопка к этому моменту уже свободна.
|
|
299
|
+
*/
|
|
300
|
+
signInWithOAuth(input: {
|
|
301
|
+
provider: OAuthProvider;
|
|
302
|
+
scopes?: string;
|
|
303
|
+
userMeta?: Record<string, string>;
|
|
304
|
+
onPopupOpened?: () => void;
|
|
305
|
+
}): Promise<AuthSession>;
|
|
306
|
+
/**
|
|
307
|
+
* Шаг 1 OAuth split-API: инициирует flow на бэке, генерит PKCE verifier
|
|
308
|
+
* + state, сохраняет их у себя, возвращает `{authorize_url, state}` для
|
|
309
|
+
* открытия popup'а. Верификатор НЕ выходит наружу — его держит AuthClient
|
|
310
|
+
* до `completeOAuthFlow`.
|
|
311
|
+
*
|
|
312
|
+
* Используется в offscreen-архитектуре (@monetize/sdk-extension): start
|
|
313
|
+
* вызывается через RPC из content-script'а, content открывает popup
|
|
314
|
+
* нативно (gesture preserved), затем зовёт completeOAuthFlow с code'ом.
|
|
315
|
+
* AuthClient (в offscreen'е) делает /exchange с сохранённым verifier'ом.
|
|
316
|
+
*
|
|
317
|
+
* Pending flows GC'атся через 10мин — больше чем юзеру нужно прокликать
|
|
318
|
+
* Google. Без cleanup'а Map бы рос на каждый закрытый popup.
|
|
319
|
+
*/
|
|
320
|
+
startOAuthFlow(input: {
|
|
321
|
+
provider: OAuthProvider;
|
|
322
|
+
scopes?: string;
|
|
323
|
+
userMeta?: Record<string, string>;
|
|
324
|
+
}): Promise<{
|
|
325
|
+
authorize_url: string;
|
|
326
|
+
state: string;
|
|
327
|
+
}>;
|
|
328
|
+
/**
|
|
329
|
+
* Шаг 2 OAuth split-API: обменивает code (полученный из popup) на session,
|
|
330
|
+
* используя verifier, сохранённый при startOAuthFlow. После успеха — set
|
|
331
|
+
* session и эмит onAuthChange.
|
|
332
|
+
*
|
|
333
|
+
* Если flow не найден (state не из startOAuthFlow или GC'нулся за TTL'ом) —
|
|
334
|
+
* бросает `oauth_invalid_state`. Caller должен начать заново через
|
|
335
|
+
* startOAuthFlow.
|
|
336
|
+
*/
|
|
337
|
+
completeOAuthFlow(input: {
|
|
338
|
+
state: string;
|
|
339
|
+
code: string;
|
|
340
|
+
}): Promise<AuthSession>;
|
|
341
|
+
private gcOAuthFlows;
|
|
342
|
+
/**
|
|
343
|
+
* Refresh access/refresh пары через текущий refresh_token. Дедуплицирует
|
|
344
|
+
* параллельные вызовы (один in-flight promise на весь клиент).
|
|
345
|
+
*
|
|
346
|
+
* - 401 → refresh_token отозван/невалиден → чистим session, эмитим logout.
|
|
347
|
+
* - Сеть/5xx → пробрасываем ошибку, session оставляем — юзер не должен
|
|
348
|
+
* разлогиниваться из-за временной сетевой проблемы.
|
|
349
|
+
* - Нет session → возвращаем null без сетевого запроса.
|
|
350
|
+
*/
|
|
351
|
+
refresh(): Promise<AuthSession | null>;
|
|
352
|
+
/**
|
|
353
|
+
* Глобальный logout — инвалидирует ВСЕ refresh-токены юзера на всех
|
|
354
|
+
* устройствах/контекстах через GoTrue `/logout?scope=global`. Используется
|
|
355
|
+
* для compromise-account флоу («подозрительная активность, разлогинить
|
|
356
|
+
* везде»).
|
|
357
|
+
*
|
|
358
|
+
* Local-side: чистим текущую session, остальные контексты (другие вкладки
|
|
359
|
+
* / extension popup и background) подхватят logout через storage-watch
|
|
360
|
+
* автоматически. Active access-токены в других контекстах останутся валидны
|
|
361
|
+
* до их естественного истечения (1 час max), но refresh уже не сработает —
|
|
362
|
+
* после первого `getAccessToken()` каждый контекст разлогинится сам.
|
|
363
|
+
*
|
|
364
|
+
* Безопасность: бэк не принимает целевой user_id — резолвит юзера из
|
|
365
|
+
* Bearer, нельзя разлогинить чужой аккаунт.
|
|
366
|
+
*/
|
|
367
|
+
revokeAllSessions(): Promise<void>;
|
|
368
|
+
/**
|
|
369
|
+
* Signout: чистит локальную session СРАЗУ (UX — мгновенный logout без
|
|
370
|
+
* ожидания сети), потом best-effort POST /auth/signout с текущим access.
|
|
371
|
+
* Ошибка сети/5xx тут уже не критична — на бэке токен и так истечёт.
|
|
372
|
+
*
|
|
373
|
+
* Anon-aware: по умолчанию anonRefreshToken сохраняется. Это позволяет
|
|
374
|
+
* после signOut() позвать signInAnonymously() и попасть в ТОТ ЖЕ
|
|
375
|
+
* анон-аккаунт без капчи (см. resumeAnonymous). Поведение предсказуемое
|
|
376
|
+
* для UX'а «гость → залогинился → разлогинился → снова гость с теми же
|
|
377
|
+
* балансами».
|
|
378
|
+
*
|
|
379
|
+
* `forgetAnonymous: true` — полное забытие, вместе с anonRefreshToken.
|
|
380
|
+
* Нужно для сценариев типа «свич аккаунта на устройстве» или жалоб на
|
|
381
|
+
* приватность («очисти все мои следы»).
|
|
382
|
+
*/
|
|
383
|
+
signOut(opts?: {
|
|
384
|
+
forgetAnonymous?: boolean;
|
|
385
|
+
}): Promise<void>;
|
|
386
|
+
/**
|
|
387
|
+
* Подписка на изменения session: signin/signup/refresh/signOut/expired-401.
|
|
388
|
+
* Колбек вызывается с текущим snapshot через microtask (если session есть)
|
|
389
|
+
* + на каждое реальное изменение. Возвращает unsubscribe.
|
|
390
|
+
*/
|
|
391
|
+
onAuthChange(cb: AuthChangeListener): () => void;
|
|
392
|
+
private isFresh;
|
|
393
|
+
private toSession;
|
|
394
|
+
private setSession;
|
|
395
|
+
private emit;
|
|
396
|
+
private storageKey;
|
|
397
|
+
private hydrate;
|
|
398
|
+
private rehydrateFromStorage;
|
|
399
|
+
/**
|
|
400
|
+
* Освобождает ресурсы AuthClient'а: отписывает storage-watch, чистит
|
|
401
|
+
* listener'ы, выставляет destroyed-флаг. После destroy все async-операции
|
|
402
|
+
* (inflight refresh, OAuth popup, applyExternalSession) early-return'ят
|
|
403
|
+
* через `isDestroyed()` guard'ы — никаких write-back'ов в storage,
|
|
404
|
+
* никаких эмитов на пустые listener'ы.
|
|
405
|
+
*
|
|
406
|
+
* destroy() идемпотентен: повторный вызов — no-op.
|
|
407
|
+
*/
|
|
408
|
+
destroy(): void;
|
|
409
|
+
/** Sync-проверка: был ли вызван destroy(). Полезно для UI / тестов. */
|
|
410
|
+
isDestroyed(): boolean;
|
|
411
|
+
private persist;
|
|
412
|
+
private readAnonRefreshToken;
|
|
413
|
+
private writeAnonRefreshToken;
|
|
414
|
+
private clearAnonRefreshToken;
|
|
415
|
+
/**
|
|
416
|
+
* Читает stable visitor_id из storage если он там уже есть. НЕ генерит:
|
|
417
|
+
* AuthClient может быть инстанцирован раньше BillingClient, а синтетический
|
|
418
|
+
* visitor_id без касания пейвола не имеет смысла (нет гостевых покупок,
|
|
419
|
+
* которые надо бы линковать). undefined → бэк сам пропустит ветку
|
|
420
|
+
* "merge guest purchases".
|
|
421
|
+
*/
|
|
422
|
+
private readVisitorId;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export declare interface AuthClientOptions {
|
|
426
|
+
paywallId: string;
|
|
427
|
+
apiOrigin?: string;
|
|
428
|
+
storage?: StorageAdapter;
|
|
429
|
+
fetch?: typeof fetch;
|
|
430
|
+
openPopup?: (url: string, name: string) => Window | null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export declare interface AuthSession {
|
|
434
|
+
access_token: string;
|
|
435
|
+
refresh_token: string;
|
|
436
|
+
/** Absolute timestamp в ms (Date.now() сравнимо). null/0 не пишем. */
|
|
437
|
+
expires_at: number;
|
|
438
|
+
user: AuthUser;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
export declare interface AuthUser {
|
|
442
|
+
id: string;
|
|
443
|
+
/** null для анонимного юзера (signInAnonymously). Для всех остальных flow — заполнен. */
|
|
444
|
+
email: string | null;
|
|
445
|
+
country?: string | null;
|
|
446
|
+
/** true — Supabase anonymous user. UI использует, чтобы решать «sign in» vs
|
|
447
|
+
* «signed in as ...», и чтобы при OAuth-апгрейде звать linkIdentity вместо
|
|
448
|
+
* signInWithOAuth (зеркалит легаси StartAuthPage.tsx). */
|
|
449
|
+
is_anonymous?: boolean;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Балансы AI-провайдеров пейвола: один элемент на `query_type` из
|
|
453
|
+
* `paywall_settings.tokenization_queries`. count = доступно вызовов. */
|
|
454
|
+
export declare interface Balance {
|
|
455
|
+
type: string;
|
|
456
|
+
count: number;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
declare type BalancesListener = (balances: Balance[]) => void;
|
|
460
|
+
|
|
461
|
+
export declare class BillingClient {
|
|
462
|
+
readonly paywallId: string;
|
|
463
|
+
readonly apiOrigin: string;
|
|
464
|
+
readonly capabilities: string[] | undefined;
|
|
465
|
+
/** AuthClient, если был передан в options. Иначе undefined. */
|
|
466
|
+
readonly auth: AuthClient | undefined;
|
|
467
|
+
private api;
|
|
468
|
+
private storage;
|
|
469
|
+
private identity;
|
|
470
|
+
private apiKey;
|
|
471
|
+
private fetchImpl;
|
|
472
|
+
private cachedBootstrap;
|
|
473
|
+
private cachedBootstrapAt;
|
|
474
|
+
private inflightBootstrap;
|
|
475
|
+
private bootstrapListeners;
|
|
476
|
+
private bootstrapStorageUnwatch;
|
|
477
|
+
private authUnsubscribe;
|
|
478
|
+
private cachedUser;
|
|
479
|
+
private cachedUserAt;
|
|
480
|
+
private inflightUser;
|
|
481
|
+
private userListeners;
|
|
482
|
+
private visitorIdPromise;
|
|
483
|
+
private visitorId;
|
|
484
|
+
private inflightCheckouts;
|
|
485
|
+
private cachedBalances;
|
|
486
|
+
private cachedBalancesAt;
|
|
487
|
+
private balancesStorageUnwatch;
|
|
488
|
+
private inflightBalances;
|
|
489
|
+
private balanceListeners;
|
|
490
|
+
constructor(opts: BillingClientOptions);
|
|
491
|
+
/**
|
|
492
|
+
* Stable visitor_id (UUID v4). Первый вызов awaitит первичный резолв из
|
|
493
|
+
* storage; последующие — мгновенно из in-memory кеша. Используется
|
|
494
|
+
* EventTracker'ом для атрибуции аналитики.
|
|
495
|
+
*/
|
|
496
|
+
getVisitorId(): Promise<string>;
|
|
497
|
+
/** Sync-доступ к visitor_id. null если ещё не зарезолвили (первые ms жизни). */
|
|
498
|
+
getCachedVisitorId(): string | null;
|
|
499
|
+
setIdentity(identity: Identity | undefined): void;
|
|
500
|
+
/**
|
|
501
|
+
* Отписаться от auth-event'ов и сбросить listener'ы. Вызывать когда
|
|
502
|
+
* BillingClient больше не нужен (тесты, hot-reload, переинициализация).
|
|
503
|
+
* Без destroy() listener на AuthClient переживёт BillingClient и будет
|
|
504
|
+
* дёргать setIdentity на освобождённом инстансе. Слушатели user/balance
|
|
505
|
+
* чистятся, чтобы упавший host (например, размонтированный React-tree)
|
|
506
|
+
* не держал замыкания на эти колбеки.
|
|
507
|
+
*/
|
|
508
|
+
destroy(): void;
|
|
509
|
+
getIdentity(): Identity | undefined;
|
|
510
|
+
getStorage(): StorageAdapter;
|
|
511
|
+
bootstrap(forceOrOpts?: boolean | {
|
|
512
|
+
force?: boolean;
|
|
513
|
+
signal?: AbortSignal;
|
|
514
|
+
}): Promise<PaywallBootstrap>;
|
|
515
|
+
/**
|
|
516
|
+
* Подписка на изменения bootstrap'а: applyBootstrap (сетевой revalidate,
|
|
517
|
+
* cross-context storage.watch). Срабатывает ТОЛЬКО при реальном изменении
|
|
518
|
+
* `version` (unchanged-ответ от сервера не дёргает listener'ов). Возвращает
|
|
519
|
+
* unsubscribe.
|
|
520
|
+
*/
|
|
521
|
+
onBootstrapChange(cb: (b: PaywallBootstrap) => void): () => void;
|
|
522
|
+
private fetchBootstrap;
|
|
523
|
+
private revalidateBootstrap;
|
|
524
|
+
private applyBootstrap;
|
|
525
|
+
private hydrateBootstrapFromStorage;
|
|
526
|
+
private persistBootstrap;
|
|
527
|
+
private subscribeBootstrapStorage;
|
|
528
|
+
/** Возвращает последний загруженный bootstrap без сетевого запроса.
|
|
529
|
+
* null = bootstrap ещё не загружали. Удобно для post-checkout-логики
|
|
530
|
+
* (PaywallUI читает success_redirect_url, не делая второго round-trip'а). */
|
|
531
|
+
getCachedBootstrap(): PaywallBootstrap | null;
|
|
532
|
+
/**
|
|
533
|
+
* Шорткат поверх `bootstrap()`: ждёт загрузку структуры пейвола и возвращает
|
|
534
|
+
* цены. Полезно когда host рисует цены вне модалки (карточки на лендинге,
|
|
535
|
+
* "Pricing" page и т.п.) и не хочет руками распаковывать bootstrap.
|
|
536
|
+
*
|
|
537
|
+
* Locale-оверрайды (`label`/`description` под `navigator.language`) уже
|
|
538
|
+
* применены — массив готов к рендеру. Кэш/TTL/stale-while-revalidate — те
|
|
539
|
+
* же, что у `bootstrap()`: повторный вызов не штурмует сервер.
|
|
540
|
+
*/
|
|
541
|
+
getPrices(opts?: {
|
|
542
|
+
force?: boolean;
|
|
543
|
+
signal?: AbortSignal;
|
|
544
|
+
}): Promise<PaywallPrice[]>;
|
|
545
|
+
/** Sync-снимок цен из последнего bootstrap'а. null = ещё не загружали. */
|
|
546
|
+
getCachedPrices(): PaywallPrice[] | null;
|
|
547
|
+
/**
|
|
548
|
+
* Снимок того, какой язык SDK сейчас считает «языком юзера». Полезно для
|
|
549
|
+
* синхронизации i18n хоста с тем, что фактически показывает пейвол — чтобы
|
|
550
|
+
* окружающий UI не противоречил модалке (например, host рисует кнопку
|
|
551
|
+
* "Subscribe" на английском, а пейвол показывает «Подписаться» на русском).
|
|
552
|
+
*
|
|
553
|
+
* Возвращает структуру, а не один тэг, чтобы интегратор мог:
|
|
554
|
+
* - быстро взять `tag` для своих переводов;
|
|
555
|
+
* - отличить «пейвол реально на этом языке» (`applied !== null`) от
|
|
556
|
+
* «SDK угадал, но локали для этого языка нет — рендерится база»;
|
|
557
|
+
* - решить, чему доверять при противоречии browserLanguage vs countryLanguage
|
|
558
|
+
* (тур, expat, VPN — у каждого свой ответ).
|
|
559
|
+
*
|
|
560
|
+
* Sync-вызов: данные уже в bootstrap'е, отдельных запросов не делает.
|
|
561
|
+
* Если `bootstrap()` ещё не вызывался — `applied` и `countryLanguage`
|
|
562
|
+
* будут `null`, но `browserLanguage` и `tag` всё равно отдадутся, если
|
|
563
|
+
* есть `navigator.language`.
|
|
564
|
+
*/
|
|
565
|
+
getUserLanguage(): UserLanguageInfo;
|
|
566
|
+
/**
|
|
567
|
+
* Получить актуальное состояние подписки/покупок.
|
|
568
|
+
*
|
|
569
|
+
* - In-memory cache TTL 5с — naïve setInterval(1000) не нагружает сервер.
|
|
570
|
+
* - In-flight dedupe — параллельные вызовы получают один promise.
|
|
571
|
+
* - `force: true` обходит кеш (для post-checkout проверки).
|
|
572
|
+
* - Без identity возвращает empty-state (сервер тоже так делает).
|
|
573
|
+
*/
|
|
574
|
+
getUser({ force, signal }?: {
|
|
575
|
+
force?: boolean;
|
|
576
|
+
signal?: AbortSignal;
|
|
577
|
+
}): Promise<PaywallUser>;
|
|
578
|
+
/**
|
|
579
|
+
* Подписка на изменения user-state. Колбек вызывается:
|
|
580
|
+
* - сразу с last-known user (если есть в кеше) — по умолчанию через
|
|
581
|
+
* microtask, опционально SYNC (см. опции);
|
|
582
|
+
* - на каждое реальное изменение (getUser/bootstrap принёс другой shape).
|
|
583
|
+
*
|
|
584
|
+
* `opts.immediate`:
|
|
585
|
+
* - `'microtask'` (default) — initial snapshot отдаётся в queueMicrotask,
|
|
586
|
+
* чтобы host успел доресетнуть state в том же тике. Безопасный выбор
|
|
587
|
+
* для большинства интеграций.
|
|
588
|
+
* - `'sync'` — initial snapshot отдаётся прямо в текущем frame'е, до
|
|
589
|
+
* возврата из onUserChange. Удобно для React/Vue useEffect-cleanup'а
|
|
590
|
+
* (избегаем лишнего ре-рендера) и SSR (мгновенная синхронизация).
|
|
591
|
+
* - `'none'` — не отдавать initial snapshot, только реальные изменения.
|
|
592
|
+
*
|
|
593
|
+
* Возвращает функцию отписки.
|
|
594
|
+
*/
|
|
595
|
+
onUserChange(cb: UserListener, opts?: {
|
|
596
|
+
immediate?: 'microtask' | 'sync' | 'none';
|
|
597
|
+
}): () => void;
|
|
598
|
+
/** Текущий cached user без сетевого запроса. null = ещё не загружали. */
|
|
599
|
+
getCachedUser(): PaywallUser | null;
|
|
600
|
+
private applyUser;
|
|
601
|
+
private storageKey;
|
|
602
|
+
private hydrateUserFromStorage;
|
|
603
|
+
private persistUser;
|
|
604
|
+
/**
|
|
605
|
+
* Балансы AI-провайдеров (`paywall_balances` × `tokenization_queries`).
|
|
606
|
+
*
|
|
607
|
+
* - In-memory cache TTL 5с — параллельные UI-renders не дёргают сеть;
|
|
608
|
+
* - In-flight dedupe — параллельные `getBalances` получают один promise;
|
|
609
|
+
* - `force: true` обходит кеш (типичный кейс — после QuotaExceededError);
|
|
610
|
+
* - Без auth (Bearer не выдан) возвращает пустой массив без сетевого
|
|
611
|
+
* запроса: бэк всё равно ответит 401, нет смысла тратить round-trip.
|
|
612
|
+
*
|
|
613
|
+
* Если у пейвола `tokenization=false` — бэк отдаёт `[]`, как для гостя.
|
|
614
|
+
* SDK не различает «нет квоты» и «нет квот вообще» — caller сам решает
|
|
615
|
+
* по `currentBalance` в QuotaExceededError или `balances.length`.
|
|
616
|
+
*/
|
|
617
|
+
getBalances({ force, signal }?: {
|
|
618
|
+
force?: boolean;
|
|
619
|
+
signal?: AbortSignal;
|
|
620
|
+
}): Promise<Balance[]>;
|
|
621
|
+
private fetchBalances;
|
|
622
|
+
/** Sync snapshot. null = ещё не загружали (или explicit clear на re-login). */
|
|
623
|
+
getCachedBalances(): Balance[] | null;
|
|
624
|
+
/**
|
|
625
|
+
* Подписка на изменения балансов: getBalances/decrementBalanceLocal/setIdentity.
|
|
626
|
+
* `opts.immediate` работает так же, как в `onUserChange`: 'microtask'
|
|
627
|
+
* (default), 'sync' (для React/Vue useEffect), 'none' (только изменения).
|
|
628
|
+
* Возвращает unsubscribe.
|
|
629
|
+
*/
|
|
630
|
+
onBalanceChange(cb: BalancesListener, opts?: {
|
|
631
|
+
immediate?: 'microtask' | 'sync' | 'none';
|
|
632
|
+
}): () => void;
|
|
633
|
+
/**
|
|
634
|
+
* Оптимистично уменьшает count для `queryType` на 1 и нотифицирует
|
|
635
|
+
* listener'ов. Используется ApiGatewayClient'ом сразу после успешного
|
|
636
|
+
* gateway-вызова (бэк уже снял кредит, см. `chargeApiQueries`).
|
|
637
|
+
*
|
|
638
|
+
* Если queryType отсутствует в кеше или count<=0 — no-op (не уходим в
|
|
639
|
+
* отрицательные значения, бэк всё равно правильный source-of-truth).
|
|
640
|
+
* Если кеша нет вовсе — тоже no-op: явный getBalances({force:true}) на
|
|
641
|
+
* следующем рендере подтянет актуальный shape.
|
|
642
|
+
*
|
|
643
|
+
* queryType может быть undefined (gateway не прислал X-Query-Type) —
|
|
644
|
+
* в этом случае декремент не делаем, но просим refreshBalances() для
|
|
645
|
+
* выравнивания.
|
|
646
|
+
*/
|
|
647
|
+
decrementBalanceLocal(queryType: string | undefined): void;
|
|
648
|
+
/** Принудительный re-fetch — типичный вызов после QuotaExceededError, чтобы
|
|
649
|
+
* UI получил актуальный balance=0 и нарисовал upgrade-prompt. */
|
|
650
|
+
refreshBalances(): Promise<Balance[]>;
|
|
651
|
+
/**
|
|
652
|
+
* Фабрика ApiGatewayClient'а с подключённым к этому billing'у balance-стейтом:
|
|
653
|
+
* - Bearer/identity берутся из текущего auth/identity;
|
|
654
|
+
* - на success декрементим cachedBalances оптимистично;
|
|
655
|
+
* - на 402 (QuotaExceededError) триггерим refreshBalances() для актуального snapshot'а.
|
|
656
|
+
*
|
|
657
|
+
* Если переопределить опции через `overrides` — принимаются как есть, но
|
|
658
|
+
* `onChargeSuccess`/`onQuotaExceeded` всё равно вызываются (composable, host
|
|
659
|
+
* может добавить свой колбек поверх).
|
|
660
|
+
*/
|
|
661
|
+
createApiGatewayClient(overrides?: Partial<Omit<ApiGatewayClientOptions, 'paywallId' | 'auth' | 'userId'>>): ApiGatewayClient;
|
|
662
|
+
private applyBalances;
|
|
663
|
+
private balancesStorageKey;
|
|
664
|
+
private hydrateBalancesFromStorage;
|
|
665
|
+
private persistBalances;
|
|
666
|
+
private subscribeBalancesStorage;
|
|
667
|
+
createCheckout(params: {
|
|
668
|
+
priceId: string;
|
|
669
|
+
successUrl?: string;
|
|
670
|
+
errorUrl?: string;
|
|
671
|
+
shopUrl?: string;
|
|
672
|
+
trialDays?: number;
|
|
673
|
+
/**
|
|
674
|
+
* Stage 1 защиты от дубликатов покупок. Идемпотентный ключ запроса
|
|
675
|
+
* (UUID). Повторный вызов с тем же ключом вернёт тот же checkout-URL
|
|
676
|
+
* без второго обращения к платёжному провайдеру. Если не передан —
|
|
677
|
+
* SDK генерит UUID v4 сам и дедуплицирует параллельные клики по
|
|
678
|
+
* `auto:${priceId}`.
|
|
679
|
+
*/
|
|
680
|
+
idempotencyKey?: string;
|
|
681
|
+
/** Renewal/upgrade flow — игнорирует у бэка проверку has_active_subscription.
|
|
682
|
+
* По умолчанию /start-checkout возвращает 409 если у юзера уже есть
|
|
683
|
+
* active subscription (защита от случайных двойных оплат). С
|
|
684
|
+
* `ignoreActivePurchase: true` бэк создаёт новый checkout, прежняя
|
|
685
|
+
* подписка отменится после успешной оплаты. Передавать только когда
|
|
686
|
+
* юзер явно выбрал "Renew/Upgrade" в host-UI. */
|
|
687
|
+
ignoreActivePurchase?: boolean;
|
|
688
|
+
/** Отмена inflight-запроса. Параллельные вызовы дедуплицируются по
|
|
689
|
+
* `inflightKey`, поэтому signal отменяет ВСЕ ожидающие на этот ключ —
|
|
690
|
+
* это OK для типичного UX (юзер закрыл модалку — все checkout'ы отменены). */
|
|
691
|
+
signal?: AbortSignal;
|
|
692
|
+
}): Promise<CheckoutResult>;
|
|
693
|
+
/**
|
|
694
|
+
* URL Stripe/Paddle/Chargebee customer portal — место, где залогиненный
|
|
695
|
+
* юзер может управлять подпиской (отменить, обновить карту, скачать
|
|
696
|
+
* инвойсы). Опен-флоу управляется host'ом:
|
|
697
|
+
*
|
|
698
|
+
* ```ts
|
|
699
|
+
* const { url } = await billing.getCustomerPortalUrl();
|
|
700
|
+
* window.open(url, '_blank');
|
|
701
|
+
* ```
|
|
702
|
+
*
|
|
703
|
+
* Auth: Bearer (через AuthClient) или server-side `apiKey`. Без auth и
|
|
704
|
+
* без apiKey бросает PaywallError('identity_required'). 403 от бэка
|
|
705
|
+
* (нет активной подписки / acquiring не поддерживает portal) пробрасывается
|
|
706
|
+
* как PaywallError('forbidden') с `status: 403` — host рендерит "no
|
|
707
|
+
* subscription to manage".
|
|
708
|
+
*/
|
|
709
|
+
getCustomerPortalUrl(opts?: {
|
|
710
|
+
signal?: AbortSignal;
|
|
711
|
+
}): Promise<{
|
|
712
|
+
url: string;
|
|
713
|
+
}>;
|
|
714
|
+
/**
|
|
715
|
+
* Список покупок юзера с rich-полями (цена, валюта, interval, discount,
|
|
716
|
+
* cancel-метаданные). Подходит для customer-portal UI: cards с кнопками
|
|
717
|
+
* Cancel/Renew/Manage. Менее cache-friendly чем `getUser` — ходит в
|
|
718
|
+
* `/api/v1/paywall/[id]/user` без unstable_cache, потому что list для UI
|
|
719
|
+
* должен быть свежим после cancel-а.
|
|
720
|
+
*
|
|
721
|
+
* Auth: Bearer обязателен (через AuthClient). Без Bearer — 401 от бэка,
|
|
722
|
+
* пробрасываем как PaywallError('http_401'). Гость → пустой список.
|
|
723
|
+
*/
|
|
724
|
+
listPurchases(opts?: {
|
|
725
|
+
signal?: AbortSignal;
|
|
726
|
+
}): Promise<PaywallPurchaseDetailed[]>;
|
|
727
|
+
/**
|
|
728
|
+
* Отменить подписку. Бэк проверит что subscription принадлежит auth-юзеру
|
|
729
|
+
* и сделает cancel у acquiring'а (Stripe/Paddle/Chargebee). По умолчанию
|
|
730
|
+
* cancel в конце текущего периода — юзер сохраняет access до renewal date'ы.
|
|
731
|
+
*
|
|
732
|
+
* `reason` обязательна (валидация на бэке). Удобно собрать через select
|
|
733
|
+
* причин в host-UI, как в legacy customer portal'е.
|
|
734
|
+
*
|
|
735
|
+
* Auth: Bearer обязателен.
|
|
736
|
+
*/
|
|
737
|
+
cancelSubscription(params: {
|
|
738
|
+
subscriptionId: string;
|
|
739
|
+
reason: string;
|
|
740
|
+
signal?: AbortSignal;
|
|
741
|
+
}): Promise<{
|
|
742
|
+
subscription: {
|
|
743
|
+
status: string | null;
|
|
744
|
+
canceled_at: string | null;
|
|
745
|
+
cancel_at: string | null;
|
|
746
|
+
cancel_at_period_end: boolean | null;
|
|
747
|
+
};
|
|
748
|
+
}>;
|
|
749
|
+
/**
|
|
750
|
+
* Создаёт саппорт-тикет. Если есть `files` — multipart/form-data, иначе JSON.
|
|
751
|
+
* Email берётся (1) из явного поля payload.email; (2) из identity если оно есть.
|
|
752
|
+
* Если ни того, ни другого нет — бэк отвергнет тикет (`email_required`).
|
|
753
|
+
*
|
|
754
|
+
* Bearer-токен (если AuthClient подключён) добавляется автоматически — бэк
|
|
755
|
+
* перевешивает customer_email на email из сессии (защита от подделки).
|
|
756
|
+
*/
|
|
757
|
+
createSupportTicket(payload: {
|
|
758
|
+
subject: string;
|
|
759
|
+
content: string;
|
|
760
|
+
email?: string;
|
|
761
|
+
files?: File[];
|
|
762
|
+
}): Promise<{
|
|
763
|
+
ticket: {
|
|
764
|
+
id: number;
|
|
765
|
+
status: string;
|
|
766
|
+
};
|
|
767
|
+
}>;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export declare interface BillingClientOptions {
|
|
771
|
+
paywallId: string;
|
|
772
|
+
apiOrigin?: string;
|
|
773
|
+
identity?: Identity;
|
|
774
|
+
storage?: StorageAdapter;
|
|
775
|
+
capabilities?: string[];
|
|
776
|
+
fetch?: typeof fetch;
|
|
777
|
+
/**
|
|
778
|
+
* Server SDK API key. Используется для `/start-checkout` в headless/hybrid-сценариях,
|
|
779
|
+
* где вызов идёт из trusted-окружения (backend клиента). В client-native пути
|
|
780
|
+
* ключ НЕ передавать — приватный токен утечёт в браузер.
|
|
781
|
+
*/
|
|
782
|
+
apiKey?: string;
|
|
783
|
+
/**
|
|
784
|
+
* AuthClient для подключения Bearer-авторизации и автосинка identity. Если
|
|
785
|
+
* передан — все запросы получают `Authorization: Bearer <access_token>`,
|
|
786
|
+
* а identity пересчитывается из auth.user на каждом login/logout/refresh
|
|
787
|
+
* (перетирает явно заданный `opts.identity` после первого auth-event'а).
|
|
788
|
+
*
|
|
789
|
+
* Без auth BillingClient работает как раньше: identity приходит снаружи
|
|
790
|
+
* через `setIdentity`, Bearer не отправляется.
|
|
791
|
+
*/
|
|
792
|
+
auth?: AuthClient;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
export declare interface CheckoutResult {
|
|
796
|
+
url: string;
|
|
797
|
+
sessionId?: string;
|
|
798
|
+
/** Платёжный процессор, к которому ушёл checkout. Полезно для аналитики
|
|
799
|
+
* конверсии по эквайрингам (host может ветвить UX по acquiring). */
|
|
800
|
+
acquiring?: Acquiring;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export declare function createStorage(override?: StorageAdapter): StorageAdapter;
|
|
804
|
+
|
|
805
|
+
export declare function ensureVisitorId(storage: StorageAdapter): Promise<string>;
|
|
806
|
+
|
|
807
|
+
export declare class EventTracker {
|
|
808
|
+
private opts;
|
|
809
|
+
private buffer;
|
|
810
|
+
private flushTimer;
|
|
811
|
+
private destroyed;
|
|
812
|
+
private unloadHandler;
|
|
813
|
+
private visibilityHandler;
|
|
814
|
+
constructor(opts: EventTrackerOptions);
|
|
815
|
+
private isEnabled;
|
|
816
|
+
track(type: string, props?: Record<string, unknown>): void;
|
|
817
|
+
private scheduleFlush;
|
|
818
|
+
flush(): Promise<void>;
|
|
819
|
+
/**
|
|
820
|
+
* Отправка через navigator.sendBeacon — для unload/pagehide. Гарантированно
|
|
821
|
+
* долетает (POST с keepalive тоже почти, но beacon сделан именно под это).
|
|
822
|
+
* Headers ставить нельзя (спецификация), поэтому SDK metadata едет в body
|
|
823
|
+
* как fallback-поля, которые сервер читает в дополнение к headers.
|
|
824
|
+
*/
|
|
825
|
+
flushBeacon(): void;
|
|
826
|
+
private buildHeaders;
|
|
827
|
+
private attachUnloadHandlers;
|
|
828
|
+
private detachUnloadHandlers;
|
|
829
|
+
destroy(): void;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export declare interface EventTrackerOptions {
|
|
833
|
+
endpoint: string;
|
|
834
|
+
paywallId: string;
|
|
835
|
+
capabilities?: string[];
|
|
836
|
+
getVisitorId: () => Promise<string>;
|
|
837
|
+
getCachedVisitorId?: () => string | null;
|
|
838
|
+
getUserId?: () => string | null | undefined;
|
|
839
|
+
enabled?: boolean;
|
|
840
|
+
flushIntervalMs?: number;
|
|
841
|
+
maxBufferSize?: number;
|
|
842
|
+
/** Тестовый override fetch'а. */
|
|
843
|
+
fetch?: typeof fetch;
|
|
844
|
+
/** Тестовый override sendBeacon'а — позволяет проверить unload-flow в jsdom. */
|
|
845
|
+
sendBeacon?: (url: string, data: BodyInit) => boolean;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
export declare function generateVisitorId(): string;
|
|
849
|
+
|
|
850
|
+
export declare interface Identity {
|
|
851
|
+
email?: string;
|
|
852
|
+
userId?: string;
|
|
853
|
+
anonymousId?: string;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
export declare interface Layout {
|
|
857
|
+
type: 'modal';
|
|
858
|
+
blocks: LayoutBlock[];
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export declare type LayoutBlock = {
|
|
862
|
+
type: 'heading';
|
|
863
|
+
text: string;
|
|
864
|
+
level?: 1 | 2 | 3;
|
|
865
|
+
} | {
|
|
866
|
+
type: 'text';
|
|
867
|
+
text: string;
|
|
868
|
+
} | {
|
|
869
|
+
type: 'price_grid';
|
|
870
|
+
priceIds?: string[];
|
|
871
|
+
/** Раскладка карточек цен. `vertical` (default) — стек сверху вниз;
|
|
872
|
+
* `horizontal` — ряд side-by-side. v2-аналог `view: 'default' | 'telegram'`. */
|
|
873
|
+
view?: 'vertical' | 'horizontal';
|
|
874
|
+
/** ID цены, которая помечается лейблом «популярный план». v2-аналог
|
|
875
|
+
* пары `price_label_id` + `price_label`. */
|
|
876
|
+
popular_price_id?: string;
|
|
877
|
+
/** Текст лейбла «популярный план». По умолчанию "Most popular".
|
|
878
|
+
* v2-аналог `price_label_text`. Локализация — через bootstrap.locales. */
|
|
879
|
+
popular_label?: string;
|
|
880
|
+
} | {
|
|
881
|
+
type: 'cta_button';
|
|
882
|
+
label: string;
|
|
883
|
+
action: 'checkout' | 'close';
|
|
884
|
+
priceId?: string;
|
|
885
|
+
} | {
|
|
886
|
+
/** Footer-блок под cta_button: залогинен — рисует "Signed in as <email> | Sign out",
|
|
887
|
+
* иначе — кнопку "Restore purchases", которая открывает auth-gate без pendingCheckout
|
|
888
|
+
* (после signIn gate просто схлопывается, юзер видит свой signed-in state). */
|
|
889
|
+
type: 'current_session';
|
|
890
|
+
} | {
|
|
891
|
+
type: 'auth_panel';
|
|
892
|
+
/** OAuth-провайдеры в порядке отображения. Пусто/опущено — только email-форма. */
|
|
893
|
+
providers?: Array<'google' | 'apple' | 'github' | 'facebook'>;
|
|
894
|
+
/** Показывать toggle "Sign up". По умолчанию true. */
|
|
895
|
+
allow_signup?: boolean;
|
|
896
|
+
/** Показывать ссылку "Forgot password?". По умолчанию true. */
|
|
897
|
+
allow_password_reset?: boolean;
|
|
898
|
+
/** Скрывать панель, если юзер уже залогинен. По умолчанию true.
|
|
899
|
+
* false — показываем "Signed in as ... [Sign out]" даже после логина. */
|
|
900
|
+
hide_when_authenticated?: boolean;
|
|
901
|
+
/** Заголовок над формой (h2). Если опущен — заголовок не рендерится. */
|
|
902
|
+
heading?: string;
|
|
903
|
+
} | {
|
|
904
|
+
/** Список фич/преимуществ продукта. v2-аналог `features_list` + `features_view`.
|
|
905
|
+
* До 5 элементов — рендерим как чек-лист с заголовком и описанием. */
|
|
906
|
+
type: 'features_list';
|
|
907
|
+
items: Array<{
|
|
908
|
+
id: string;
|
|
909
|
+
name: string;
|
|
910
|
+
desc?: string;
|
|
911
|
+
}>;
|
|
912
|
+
} | {
|
|
913
|
+
/** Информационный список «что включено в выбранный план» — рендерится
|
|
914
|
+
* под price_grid, без интерактивности. v2-аналог `tokenization` +
|
|
915
|
+
* `tokenization_queries`. Для каждого query показываем count,
|
|
916
|
+
* умноженный на множитель интервала выбранной цены (`week=0.25`,
|
|
917
|
+
* `month=1`, `year=12`) — т.е. count в БД хранится как месячная
|
|
918
|
+
* норма. Заголовок реактивно отражает текущий interval. */
|
|
919
|
+
type: 'tokenization_gate';
|
|
920
|
+
queries: Array<{
|
|
921
|
+
id: string;
|
|
922
|
+
name: string;
|
|
923
|
+
desc: string;
|
|
924
|
+
count: number;
|
|
925
|
+
}>;
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
/** Локализационные оверрайды для одного языка. Накатываются поверх дефолтного
|
|
929
|
+
* layout/prices при матче `navigator.language` ↔ ключа в `bootstrap.locales`.
|
|
930
|
+
* v2-аналог поля `translations` JSON в paywall_settings. */
|
|
931
|
+
export declare interface LocaleOverrides {
|
|
932
|
+
/** Полная замена layout для языка. Если опущен — берётся дефолтный
|
|
933
|
+
* bootstrap.layout. */
|
|
934
|
+
layout?: Layout;
|
|
935
|
+
/** Точечные оверрайды текстовых полей цен. Ключ — price.id, значения
|
|
936
|
+
* накатываются на label/description. */
|
|
937
|
+
prices?: Record<string, {
|
|
938
|
+
label?: string;
|
|
939
|
+
description?: string;
|
|
940
|
+
}>;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export declare type OAuthProvider = 'google' | 'apple' | 'github' | 'facebook';
|
|
944
|
+
|
|
945
|
+
export declare type OtpVerifyType = 'email' | 'recovery' | 'signup' | 'magiclink' | 'invite';
|
|
946
|
+
|
|
947
|
+
export declare interface PaywallBootstrap {
|
|
948
|
+
settings: PaywallSettings;
|
|
949
|
+
prices: PaywallPrice[];
|
|
950
|
+
offers: PaywallOffer[];
|
|
951
|
+
layout?: Layout;
|
|
952
|
+
/** Snapshot user-state на момент bootstrap'а. Без identity (гость) — всё пусто.
|
|
953
|
+
* Дальше обновляется через BillingClient.getUser() / PaywallUI.onUserChange. */
|
|
954
|
+
user?: PaywallUser;
|
|
955
|
+
/** Локализационные оверрайды по BCP-47 кодам (`en`, `en-US`, `ru`, ...).
|
|
956
|
+
* BillingClient.bootstrap() матчит `navigator.language` с fallback на
|
|
957
|
+
* `settings.locale_default` и применяет оверрайды поверх layout/prices. */
|
|
958
|
+
locales?: Record<string, LocaleOverrides>;
|
|
959
|
+
/** Stable content-hash структурной части bootstrap'а (без user). SDK
|
|
960
|
+
* персистит payload в StorageAdapter и шлёт `?if_version=<v>` на
|
|
961
|
+
* ревалидации — бэк отвечает `{unchanged:true, version, user}` без
|
|
962
|
+
* полного payload, если version совпала. Optional для совместимости
|
|
963
|
+
* с старыми бэками. */
|
|
964
|
+
version?: string;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
export declare class PaywallError extends Error {
|
|
968
|
+
readonly code: string;
|
|
969
|
+
readonly status?: number;
|
|
970
|
+
readonly cause?: unknown;
|
|
971
|
+
constructor(code: string, message: string, opts?: {
|
|
972
|
+
status?: number;
|
|
973
|
+
cause?: unknown;
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
export declare interface PaywallOffer {
|
|
978
|
+
id: string;
|
|
979
|
+
discount_percent: number | null;
|
|
980
|
+
expires_at: string | null;
|
|
981
|
+
price_id: string | null;
|
|
982
|
+
label?: string | null;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export declare interface PaywallPrice {
|
|
986
|
+
id: string;
|
|
987
|
+
currency: string;
|
|
988
|
+
amount: number;
|
|
989
|
+
interval: 'month' | 'year' | 'week' | 'day' | 'lifetime' | null;
|
|
990
|
+
interval_count: number | null;
|
|
991
|
+
trial_days: number | null;
|
|
992
|
+
label?: string | null;
|
|
993
|
+
description?: string | null;
|
|
994
|
+
local?: {
|
|
995
|
+
currency: string;
|
|
996
|
+
amount: number;
|
|
997
|
+
} | null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/** Rich-shape от `/api/v1/paywall/[id]/user` для customer-portal UX (cancel,
|
|
1001
|
+
* renew, история платежей). В отличие от `PaywallUserPurchase` (которая
|
|
1002
|
+
* идёт из `/user-state` и имеет минимум для access-gate'а), этот shape
|
|
1003
|
+
* включает цену/валюту/discount — чтобы host мог нарисовать список подписок
|
|
1004
|
+
* как в legacy customer portal'е. */
|
|
1005
|
+
declare interface PaywallPurchaseDetailed {
|
|
1006
|
+
id: string;
|
|
1007
|
+
status: string | null;
|
|
1008
|
+
cancel_at: string | null;
|
|
1009
|
+
cancel_at_period_end: boolean;
|
|
1010
|
+
canceled_at: string | null;
|
|
1011
|
+
created: string;
|
|
1012
|
+
ended_at: string | null;
|
|
1013
|
+
current_period_end: string | null;
|
|
1014
|
+
current_period_start: string | null;
|
|
1015
|
+
/** Цена в minor units (центах). Для legacy совместимости — sometimes из
|
|
1016
|
+
* `paywall_internal_prices.unit_amount * 100`, иногда из local_amount. */
|
|
1017
|
+
unit_amount: number;
|
|
1018
|
+
currency: string;
|
|
1019
|
+
interval: string | null;
|
|
1020
|
+
/** Скидка в процентах от offer (если был применён). undefined — без offer'а. */
|
|
1021
|
+
discount?: number;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
export declare interface PaywallSettings {
|
|
1025
|
+
id: string;
|
|
1026
|
+
name: string;
|
|
1027
|
+
brand_color?: string | null;
|
|
1028
|
+
custom_css?: string | null;
|
|
1029
|
+
locale_default?: string | null;
|
|
1030
|
+
runtime_mode?: 'client' | 'hybrid' | 'server' | 'client-native' | 'hybrid-native';
|
|
1031
|
+
/** true, если эквайринг пейвола в test-mode — SDK рисует TEST MODE бейдж. */
|
|
1032
|
+
is_test_mode?: boolean;
|
|
1033
|
+
/** Auth-flow относительно checkout. `guest` (default) — без auth перед оплатой;
|
|
1034
|
+
* `preauth` — клик по cta_button=checkout сначала открывает AuthPanel-gate,
|
|
1035
|
+
* после signIn auto-resume исходного createCheckout. Поле общее с legacy v2. */
|
|
1036
|
+
checkout_mode?: 'guest' | 'preauth';
|
|
1037
|
+
/** OAuth-провайдеры для preauth-gate в порядке отображения. Бэк сейчас отдаёт
|
|
1038
|
+
* фиксированный список (google + apple); если поле не задано — gate рисует
|
|
1039
|
+
* только email-форму. Не путать с `block.providers` у inline-блока auth_panel. */
|
|
1040
|
+
auth_providers?: Array<'google' | 'apple' | 'github' | 'facebook'>;
|
|
1041
|
+
/** Разрешён ли вход без email — анонимный юзер. `paywall.signInAnonymously()`
|
|
1042
|
+
* падает с code='anonymous_disabled', если флаг = false. Поле дублирует
|
|
1043
|
+
* `paywall_settings.allow_anonymous` из БД, то же что используется в legacy
|
|
1044
|
+
* v2 (PayWallIframeOpener.tsx). Защита от abuse — на стороне сервера (Supabase
|
|
1045
|
+
* rate-limit per real-IP + CF Bot Fight Mode), capтча в SDK не используется. */
|
|
1046
|
+
allow_anonymous?: boolean;
|
|
1047
|
+
/** Можно ли закрыть модалку (крестик, клик по overlay, ESC). По умолчанию true.
|
|
1048
|
+
* false — модалка показывается до успешной покупки или явного host-close().
|
|
1049
|
+
* v2-аналог `allow_close`. */
|
|
1050
|
+
allow_close?: boolean;
|
|
1051
|
+
/** Авто-подгонка размера шрифта heading-блока, чтобы заголовок влезал в 2
|
|
1052
|
+
* строки. v2-аналог `title_auto_fit`. По умолчанию false. */
|
|
1053
|
+
title_auto_fit?: boolean;
|
|
1054
|
+
/** URL, куда редиректить вкладку после успешной покупки (server-confirmed
|
|
1055
|
+
* через UserWatcher). null/undefined — остаёмся на месте, показываем
|
|
1056
|
+
* PurchaseSuccessView. v2-аналог `success_redirect_url`. */
|
|
1057
|
+
success_redirect_url?: string | null;
|
|
1058
|
+
/** URL "Вернуться в магазин" — пробрасывается в createCheckout как `shopUrl`
|
|
1059
|
+
* для Stripe/Paddle страницы оплаты. v2-аналог `checkout_shop_url`. */
|
|
1060
|
+
checkout_shop_url?: string | null;
|
|
1061
|
+
/** Имя продукта на странице оплаты Stripe/Paddle (line_item.name). Бэк
|
|
1062
|
+
* использует при создании checkout-сессии. v2-аналог `checkout_product_name`. */
|
|
1063
|
+
checkout_product_name?: string | null;
|
|
1064
|
+
/** Конфиг pre-paywall триала (паывол не показывается, пока триал активен).
|
|
1065
|
+
* null/undefined — триал отключён, `paywall.open()` сразу открывает модалку.
|
|
1066
|
+
* v2-аналог пары `trial` + `trial_payload` в paywall_settings. Не путать с
|
|
1067
|
+
* card-trial (PaywallPrice.trial_days) — это автосписание после оплаты. */
|
|
1068
|
+
trial?: TrialConfig | null;
|
|
1069
|
+
/** Server-computed targeting-gate: матчится ли текущий юзер (страна/девайс)
|
|
1070
|
+
* под настройки таргетинга пейвола, плюс общий on/off-флаг. SDK перед open()
|
|
1071
|
+
* читает `visible`: false → эмитит `visibility_blocked` и не монтирует
|
|
1072
|
+
* модалку. country/tier выдаются всегда — host'ы используют для аналитики.
|
|
1073
|
+
* v2-аналог `visibilityEnabledAndTargetingMatch` + `detectInvisible` в
|
|
1074
|
+
* PaywallClient.tsx + StateService. */
|
|
1075
|
+
visibility?: VisibilityStatus;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export declare interface PaywallUser {
|
|
1079
|
+
/** Главный флаг для большинства интеграций. true, если есть активная подписка
|
|
1080
|
+
* ИЛИ оплаченный lifetime ИЛИ активный trial. */
|
|
1081
|
+
has_active_subscription: boolean;
|
|
1082
|
+
purchases: PaywallUserPurchase[];
|
|
1083
|
+
trial: {
|
|
1084
|
+
started_at: string | null;
|
|
1085
|
+
expires_at: string | null;
|
|
1086
|
+
} | null;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
export declare interface PaywallUserPurchase {
|
|
1090
|
+
id: string;
|
|
1091
|
+
status: string | null;
|
|
1092
|
+
current_period_end: string | null;
|
|
1093
|
+
cancel_at_period_end: boolean | null;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** 402 от api-gateway: квота закончилась. UI ловит и открывает paywall;
|
|
1097
|
+
* headless caller — обрабатывает сам. balances/queryType/currentBalance —
|
|
1098
|
+
* то же, что отдаёт бэк в `details`. */
|
|
1099
|
+
export declare class QuotaExceededError extends PaywallError {
|
|
1100
|
+
readonly balances: Balance[];
|
|
1101
|
+
readonly queryType: string;
|
|
1102
|
+
readonly currentBalance: Balance | null;
|
|
1103
|
+
constructor(input: {
|
|
1104
|
+
balances: Balance[];
|
|
1105
|
+
queryType: string;
|
|
1106
|
+
currentBalance: Balance | null;
|
|
1107
|
+
message?: string;
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
export declare const SDK_VERSION = "3.0.0-alpha.0";
|
|
1112
|
+
|
|
1113
|
+
export declare type SignUpResult = {
|
|
1114
|
+
kind: 'signed_in';
|
|
1115
|
+
session: AuthSession;
|
|
1116
|
+
} | {
|
|
1117
|
+
kind: 'confirmation_required';
|
|
1118
|
+
user: {
|
|
1119
|
+
id: string;
|
|
1120
|
+
email: string;
|
|
1121
|
+
};
|
|
1122
|
+
};
|
|
1123
|
+
|
|
1124
|
+
export declare const STORAGE_KEYS: {
|
|
1125
|
+
visitorId: string;
|
|
1126
|
+
lastLoginMethod: (paywallId: string) => string;
|
|
1127
|
+
lastLoginEmail: (paywallId: string) => string;
|
|
1128
|
+
userState: (paywallId: string, identityKey: string) => string;
|
|
1129
|
+
authSession: (paywallId: string) => string;
|
|
1130
|
+
anonRefreshToken: (paywallId: string) => string;
|
|
1131
|
+
bootstrap: (paywallId: string) => string;
|
|
1132
|
+
balances: (paywallId: string, identityKey: string) => string;
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
export declare interface StorageAdapter {
|
|
1136
|
+
getItem(key: string): Promise<string | null>;
|
|
1137
|
+
setItem(key: string, value: string): Promise<void>;
|
|
1138
|
+
removeItem(key: string): Promise<void>;
|
|
1139
|
+
/**
|
|
1140
|
+
* Опционально: подписка на изменение `key` извне (другая вкладка /
|
|
1141
|
+
* background-контекст extension'а). Возвращает unsubscribe.
|
|
1142
|
+
*
|
|
1143
|
+
* Контракт callback'а:
|
|
1144
|
+
* - `value` — новое значение (string) или `null` (удалили / нет).
|
|
1145
|
+
* - Вызывается ТОЛЬКО для cross-context изменений; собственный setItem
|
|
1146
|
+
* / removeItem callback дёргать НЕ обязан (для chrome.storage.onChanged
|
|
1147
|
+
* он дёрнется и так — потребитель обязан фильтровать сам).
|
|
1148
|
+
*
|
|
1149
|
+
* Адаптеры без поддержки (memory) опускают это поле, потребитель должен
|
|
1150
|
+
* проверять `typeof storage.watch === 'function'`.
|
|
1151
|
+
*/
|
|
1152
|
+
watch?(key: string, cb: (value: string | null) => void): () => void;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export declare interface TrackedEvent {
|
|
1156
|
+
type: string;
|
|
1157
|
+
ts: number;
|
|
1158
|
+
props?: Record<string, unknown>;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
declare interface TrialConfig {
|
|
1162
|
+
/** `time` — паывол скрыт N часов после первого open(); `opens` — N первых
|
|
1163
|
+
* open() закрываются молча, N+1-й уже показывает паывол. */
|
|
1164
|
+
mode: 'time' | 'opens';
|
|
1165
|
+
/** Часы для `time`, количество открытий для `opens`. */
|
|
1166
|
+
payload: number;
|
|
1167
|
+
/** Где живёт состояние триала. `client` — localStorage (default, мгновенно,
|
|
1168
|
+
* юзер может сбросить очисткой storage). `server` — серверный endpoint
|
|
1169
|
+
* (сейчас стаб; включится, когда будет серверный handler). */
|
|
1170
|
+
storage: 'client' | 'server';
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/** Результат `upgradeAnonymousToEmail`. `updated` — confirmation off либо
|
|
1174
|
+
* прошёл; session.user.email уже обновлён, is_anonymous=false. `confirmation_required` —
|
|
1175
|
+
* GoTrue отправил confirmation на новый email; session всё ещё анонимная,
|
|
1176
|
+
* юзер должен кликнуть ссылку (после чего может вызвать `auth.refresh()` —
|
|
1177
|
+
* токены обновятся с email'ом и is_anonymous=false). */
|
|
1178
|
+
declare type UpgradeAnonymousResult = {
|
|
1179
|
+
kind: 'updated';
|
|
1180
|
+
session: AuthSession;
|
|
1181
|
+
} | {
|
|
1182
|
+
kind: 'confirmation_required';
|
|
1183
|
+
email: string;
|
|
1184
|
+
};
|
|
1185
|
+
|
|
1186
|
+
/** Снимок language-resolution для синхронизации i18n host-приложения с тем, что
|
|
1187
|
+
* показывает пейвол. Возвращается из `BillingClient.getUserLanguage()` /
|
|
1188
|
+
* `PaywallUI.getUserLanguage()`. */
|
|
1189
|
+
export declare interface UserLanguageInfo {
|
|
1190
|
+
/** Best-guess BCP-47 тэг для host'а. Приоритет: `applied` → `browserLanguage`
|
|
1191
|
+
* → `countryLanguage`. null — bootstrap ещё не загружен и navigator
|
|
1192
|
+
* недоступен (например, ранний вызов в service worker). */
|
|
1193
|
+
tag: string | null;
|
|
1194
|
+
/** Ключ из `bootstrap.locales`, который SDK фактически применил к
|
|
1195
|
+
* layout/prices. null = match'а не было, рендерится база из layout/prices
|
|
1196
|
+
* без оверрайдов. */
|
|
1197
|
+
applied: string | null;
|
|
1198
|
+
/** `navigator.language` — что репортит браузер. null в окружениях без
|
|
1199
|
+
* navigator (service worker до пропатчивания, Node). */
|
|
1200
|
+
browserLanguage: string | null;
|
|
1201
|
+
/** Server-resolved язык по стране юзера (IP). Берётся из
|
|
1202
|
+
* `bootstrap.settings.locale_default` — AT→de, RU→ru, LV→en, и т.д.
|
|
1203
|
+
* null — bootstrap ещё не загружен или сервер не отдал поле. */
|
|
1204
|
+
countryLanguage: string | null;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
declare type UserListener = (user: PaywallUser) => void;
|
|
1208
|
+
|
|
1209
|
+
declare interface VisibilityStatus {
|
|
1210
|
+
/** true — паывол можно открывать. false — какой-то таргетинг не сошёлся,
|
|
1211
|
+
* смотри `reason`. */
|
|
1212
|
+
visible: boolean;
|
|
1213
|
+
/** Почему `visible=false`. null когда `visible=true`.
|
|
1214
|
+
* - `disabled` — владелец выключил visibility-флаг.
|
|
1215
|
+
* - `country_not_match` — страна юзера не в whitelist (countries_tier +
|
|
1216
|
+
* extra_countries).
|
|
1217
|
+
* - `device_not_match` — extension-канал (device_target=true), юзер не на
|
|
1218
|
+
* macOS. Имеет приоритет над country, потому что в этом канале device —
|
|
1219
|
+
* главное условие.
|
|
1220
|
+
*/
|
|
1221
|
+
reason: 'country_not_match' | 'device_not_match' | 'disabled' | null;
|
|
1222
|
+
/** ISO-код страны юзера (по IP). null — не удалось определить. */
|
|
1223
|
+
country: string | null;
|
|
1224
|
+
/** Тир страны 1/2/3 (см. legacy `new_country_code_to_tier`). null — страна
|
|
1225
|
+
* не определилась. Все unmapped страны → 3. */
|
|
1226
|
+
tier: 1 | 2 | 3 | null;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
export { }
|