@monetize.software/sdk 3.0.0-alpha.2 → 3.0.0-alpha.21
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 +34 -13
- package/dist/chunks/PaywallUI-BJKIG_oT.js +3555 -0
- package/dist/chunks/PaywallUI-BJKIG_oT.js.map +1 -0
- package/dist/chunks/PaywallUI-C2aGYQ7I.js +26 -0
- package/dist/chunks/PaywallUI-C2aGYQ7I.js.map +1 -0
- package/dist/chunks/ar-OnxZkqWR.js +2 -0
- package/dist/chunks/ar-OnxZkqWR.js.map +1 -0
- package/dist/chunks/ar-rSKgwKvp.js +129 -0
- package/dist/chunks/ar-rSKgwKvp.js.map +1 -0
- package/dist/chunks/cs-Cb2KZ70r.js +2 -0
- package/dist/chunks/cs-Cb2KZ70r.js.map +1 -0
- package/dist/chunks/cs-DIWkcge_.js +125 -0
- package/dist/chunks/cs-DIWkcge_.js.map +1 -0
- package/dist/chunks/da-DdMW98j3.js +125 -0
- package/dist/chunks/da-DdMW98j3.js.map +1 -0
- package/dist/chunks/da-DyQC12xy.js +2 -0
- package/dist/chunks/da-DyQC12xy.js.map +1 -0
- package/dist/chunks/de-CWM13wnK.js +2 -0
- package/dist/chunks/de-CWM13wnK.js.map +1 -0
- package/dist/chunks/de-D1bSmD_-.js +144 -0
- package/dist/chunks/de-D1bSmD_-.js.map +1 -0
- package/dist/chunks/el-BtKuORsc.js +2 -0
- package/dist/chunks/el-BtKuORsc.js.map +1 -0
- package/dist/chunks/el-C4LtWpfP.js +129 -0
- package/dist/chunks/el-C4LtWpfP.js.map +1 -0
- package/dist/chunks/es-Bhx7w85J.js +144 -0
- package/dist/chunks/es-Bhx7w85J.js.map +1 -0
- package/dist/chunks/es-qLcKnBft.js +2 -0
- package/dist/chunks/es-qLcKnBft.js.map +1 -0
- package/dist/chunks/fi-C34Oc6rg.js +125 -0
- package/dist/chunks/fi-C34Oc6rg.js.map +1 -0
- package/dist/chunks/fi-kGtbK51C.js +2 -0
- package/dist/chunks/fi-kGtbK51C.js.map +1 -0
- package/dist/chunks/fr-BrWtqej3.js +144 -0
- package/dist/chunks/fr-BrWtqej3.js.map +1 -0
- package/dist/chunks/fr-CScwFVNj.js +2 -0
- package/dist/chunks/fr-CScwFVNj.js.map +1 -0
- package/dist/chunks/he-Byr2r07x.js +129 -0
- package/dist/chunks/he-Byr2r07x.js.map +1 -0
- package/dist/chunks/he-ChFVbP_S.js +2 -0
- package/dist/chunks/he-ChFVbP_S.js.map +1 -0
- package/dist/chunks/hi-BIswPYL2.js +2 -0
- package/dist/chunks/hi-BIswPYL2.js.map +1 -0
- package/dist/chunks/hi-CABVgpKU.js +129 -0
- package/dist/chunks/hi-CABVgpKU.js.map +1 -0
- package/dist/chunks/hu-CSQ9avfJ.js +125 -0
- package/dist/chunks/hu-CSQ9avfJ.js.map +1 -0
- package/dist/chunks/hu-CT_jwL0k.js +2 -0
- package/dist/chunks/hu-CT_jwL0k.js.map +1 -0
- package/dist/chunks/id-2lYf7ogC.js +2 -0
- package/dist/chunks/id-2lYf7ogC.js.map +1 -0
- package/dist/chunks/id-BJ5w6RSU.js +125 -0
- package/dist/chunks/id-BJ5w6RSU.js.map +1 -0
- package/dist/chunks/index-CLB1AgLg.js +2074 -0
- package/dist/chunks/index-CLB1AgLg.js.map +1 -0
- package/dist/chunks/index-CzDSBl4d.js +2 -0
- package/dist/chunks/index-CzDSBl4d.js.map +1 -0
- package/dist/chunks/it-CEMhCvXU.js +2 -0
- package/dist/chunks/it-CEMhCvXU.js.map +1 -0
- package/dist/chunks/it-Df8ChmTK.js +144 -0
- package/dist/chunks/it-Df8ChmTK.js.map +1 -0
- package/dist/chunks/ja-CkpO3n78.js +2 -0
- package/dist/chunks/ja-CkpO3n78.js.map +1 -0
- package/dist/chunks/ja-a53E5b2s.js +148 -0
- package/dist/chunks/ja-a53E5b2s.js.map +1 -0
- package/dist/chunks/ko-AZ8GrmXu.js +148 -0
- package/dist/chunks/ko-AZ8GrmXu.js.map +1 -0
- package/dist/chunks/ko-BKdzk0jX.js +2 -0
- package/dist/chunks/ko-BKdzk0jX.js.map +1 -0
- package/dist/chunks/nl-Bek7IiHL.js +2 -0
- package/dist/chunks/nl-Bek7IiHL.js.map +1 -0
- package/dist/chunks/nl-sF6ms5FU.js +144 -0
- package/dist/chunks/nl-sF6ms5FU.js.map +1 -0
- package/dist/chunks/no-BztcQKh8.js +2 -0
- package/dist/chunks/no-BztcQKh8.js.map +1 -0
- package/dist/chunks/no-DGf5PuW5.js +125 -0
- package/dist/chunks/no-DGf5PuW5.js.map +1 -0
- package/dist/chunks/pl-CMF2KerQ.js +2 -0
- package/dist/chunks/pl-CMF2KerQ.js.map +1 -0
- package/dist/chunks/pl-Dd-Ze6wn.js +125 -0
- package/dist/chunks/pl-Dd-Ze6wn.js.map +1 -0
- package/dist/chunks/pt-BL9X8Du2.js +144 -0
- package/dist/chunks/pt-BL9X8Du2.js.map +1 -0
- package/dist/chunks/pt-DF9cd_iW.js +2 -0
- package/dist/chunks/pt-DF9cd_iW.js.map +1 -0
- package/dist/chunks/ro-CGYmtR8q.js +125 -0
- package/dist/chunks/ro-CGYmtR8q.js.map +1 -0
- package/dist/chunks/ro-DpPc1UhJ.js +2 -0
- package/dist/chunks/ro-DpPc1UhJ.js.map +1 -0
- package/dist/chunks/ru-gt3-clOi.js +2 -0
- package/dist/chunks/ru-gt3-clOi.js.map +1 -0
- package/dist/chunks/ru-oPoQtUxk.js +147 -0
- package/dist/chunks/ru-oPoQtUxk.js.map +1 -0
- package/dist/chunks/sv-Cg7O9Uh3.js +2 -0
- package/dist/chunks/sv-Cg7O9Uh3.js.map +1 -0
- package/dist/chunks/sv-kXHP1Ct3.js +125 -0
- package/dist/chunks/sv-kXHP1Ct3.js.map +1 -0
- package/dist/chunks/th-DMcmb36d.js +129 -0
- package/dist/chunks/th-DMcmb36d.js.map +1 -0
- package/dist/chunks/th-pvtT9u-U.js +2 -0
- package/dist/chunks/th-pvtT9u-U.js.map +1 -0
- package/dist/chunks/tr-gAn3KCul.js +2 -0
- package/dist/chunks/tr-gAn3KCul.js.map +1 -0
- package/dist/chunks/tr-zjLbddlL.js +125 -0
- package/dist/chunks/tr-zjLbddlL.js.map +1 -0
- package/dist/chunks/uk-BYSiM14V.js +147 -0
- package/dist/chunks/uk-BYSiM14V.js.map +1 -0
- package/dist/chunks/uk-HIaOETe4.js +2 -0
- package/dist/chunks/uk-HIaOETe4.js.map +1 -0
- package/dist/chunks/vi-B7DVCjxx.js +2 -0
- package/dist/chunks/vi-B7DVCjxx.js.map +1 -0
- package/dist/chunks/vi-FbVRwy9D.js +125 -0
- package/dist/chunks/vi-FbVRwy9D.js.map +1 -0
- package/dist/chunks/zh-007yK7rl.js +2 -0
- package/dist/chunks/zh-007yK7rl.js.map +1 -0
- package/dist/chunks/zh-Cv0Yw4qR.js +148 -0
- package/dist/chunks/zh-Cv0Yw4qR.js.map +1 -0
- package/dist/core.cjs +1 -1
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.ts +260 -26
- package/dist/core.js +17 -1899
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +411 -45
- package/dist/index.js +17 -13
- package/dist/ui.cjs +1 -1
- package/dist/ui.d.ts +376 -44
- package/dist/ui.js +1 -1
- package/package.json +32 -31
- package/dist/chunks/PaywallUI-CRTEPjJm.js +0 -2229
- package/dist/chunks/PaywallUI-CRTEPjJm.js.map +0 -1
- package/dist/chunks/PaywallUI-CbbcfXXZ.js +0 -26
- package/dist/chunks/PaywallUI-CbbcfXXZ.js.map +0 -1
package/dist/core.js
CHANGED
|
@@ -1,1902 +1,20 @@
|
|
|
1
|
-
|
|
2
|
-
constructor(t, e, s = {}) {
|
|
3
|
-
super(e), this.name = "PaywallError", this.code = t, this.status = s.status, this.cause = s.cause;
|
|
4
|
-
}
|
|
5
|
-
}
|
|
6
|
-
class q extends r {
|
|
7
|
-
constructor(t) {
|
|
8
|
-
super("not_enough_queries", t.message ?? "Not enough queries", {
|
|
9
|
-
status: 402
|
|
10
|
-
}), this.name = "QuotaExceededError", this.balances = t.balances, this.queryType = t.queryType, this.currentBalance = t.currentBalance;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
const m = "3.0.0-alpha.0";
|
|
14
|
-
class O {
|
|
15
|
-
constructor(t) {
|
|
16
|
-
this.opts = t;
|
|
17
|
-
}
|
|
18
|
-
async request(t, e = {}) {
|
|
19
|
-
const s = new URL(t, this.opts.apiOrigin).toString(), i = this.opts.fetch ?? fetch, n = new Headers(e.headers);
|
|
20
|
-
n.set("Accept", "application/json"), n.set("X-SDK-Version", m), n.set("X-Paywall-Id", this.opts.paywallId), this.opts.capabilities?.length && n.set("X-SDK-Capabilities", this.opts.capabilities.join(","));
|
|
21
|
-
const o = await this.opts.getAuthToken?.();
|
|
22
|
-
o && n.set("Authorization", `Bearer ${o}`);
|
|
23
|
-
const u = typeof FormData < "u" && e.body instanceof FormData;
|
|
24
|
-
e.body && !n.has("Content-Type") && !u && n.set("Content-Type", "application/json");
|
|
25
|
-
let l;
|
|
26
|
-
try {
|
|
27
|
-
l = await i(s, {
|
|
28
|
-
...e,
|
|
29
|
-
headers: n,
|
|
30
|
-
credentials: "omit"
|
|
31
|
-
});
|
|
32
|
-
} catch (c) {
|
|
33
|
-
throw (c && typeof c == "object" && "name" in c ? c.name : void 0) === "AbortError" ? new r("aborted", "Request aborted", { cause: c }) : new r("network_error", "Network request failed", { cause: c });
|
|
34
|
-
}
|
|
35
|
-
const f = (l.headers.get("content-type") ?? "").includes("application/json") ? await l.json().catch(() => null) : null;
|
|
36
|
-
if (!l.ok) {
|
|
37
|
-
const c = f && typeof f == "object" && "code" in f && String(f.code) || `http_${l.status}`, w = f && typeof f == "object" && "message" in f && String(f.message) || l.statusText || "Request failed";
|
|
38
|
-
throw new r(c, w, { status: l.status, cause: f });
|
|
39
|
-
}
|
|
40
|
-
return f;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
const $ = "https://appbox.space";
|
|
44
|
-
class K {
|
|
45
|
-
constructor(t) {
|
|
46
|
-
if (!t.paywallId)
|
|
47
|
-
throw new r("invalid_config", "paywallId is required");
|
|
48
|
-
this.paywallId = t.paywallId, this.apiOrigin = t.apiOrigin ?? $, this.auth = t.auth, this.userId = t.userId, this.capabilities = t.capabilities, this.customFetch = t.fetch, this.onChargeSuccess = t.onChargeSuccess, this.onQuotaExceeded = t.onQuotaExceeded, t.userId && !t.auth && typeof window < "u" && typeof window.document < "u" && console.warn(
|
|
49
|
-
"[paywall] WARNING: ApiGatewayClient.userId set without auth in browser. Client can spoof userId. Use AuthClient + Bearer for trusted user.id."
|
|
50
|
-
);
|
|
51
|
-
}
|
|
52
|
-
async call(t) {
|
|
53
|
-
const e = t.path ? t.path.replace(/^\/+/, "") : "", s = new URL(
|
|
54
|
-
`/api/v1/api-gateway/${encodeURIComponent(t.providerId)}${e ? `/${e}` : ""}`,
|
|
55
|
-
this.apiOrigin
|
|
56
|
-
);
|
|
57
|
-
s.searchParams.set("paywall_id", this.paywallId);
|
|
58
|
-
const i = new Headers(t.headers);
|
|
59
|
-
i.set("X-SDK-Version", m), i.set("X-Paywall-Id", this.paywallId), this.capabilities?.length && i.set("X-SDK-Capabilities", this.capabilities.join(","));
|
|
60
|
-
const n = await this.auth?.getAccessToken();
|
|
61
|
-
n ? i.set("Authorization", `Bearer ${n}`) : this.userId && i.set("X-User-ID", this.userId);
|
|
62
|
-
const o = typeof FormData < "u" && t.body instanceof FormData, u = typeof Blob < "u" && t.body instanceof Blob, l = typeof ReadableStream < "u" && t.body instanceof ReadableStream, d = typeof t.body == "string";
|
|
63
|
-
let h;
|
|
64
|
-
t.body === void 0 || t.body === null ? h = void 0 : o || u || l || d ? h = t.body : (h = JSON.stringify(t.body), i.has("Content-Type") || i.set("Content-Type", "application/json"));
|
|
65
|
-
const f = this.customFetch ?? fetch;
|
|
66
|
-
let c;
|
|
67
|
-
try {
|
|
68
|
-
c = await f(s.toString(), {
|
|
69
|
-
method: t.method ?? "POST",
|
|
70
|
-
headers: i,
|
|
71
|
-
body: h,
|
|
72
|
-
signal: t.signal,
|
|
73
|
-
credentials: "omit"
|
|
74
|
-
});
|
|
75
|
-
} catch (p) {
|
|
76
|
-
const R = p instanceof Error ? p.message : String(p);
|
|
77
|
-
throw new r("network_error", `Network request failed: ${R}`, { cause: p });
|
|
78
|
-
}
|
|
79
|
-
if (c.status === 402) {
|
|
80
|
-
const p = await F(c);
|
|
81
|
-
throw this.onQuotaExceeded?.(p), p;
|
|
82
|
-
}
|
|
83
|
-
if (!c.ok) {
|
|
84
|
-
const p = await N(c.clone());
|
|
85
|
-
throw new r(
|
|
86
|
-
p ?? `http_${c.status}`,
|
|
87
|
-
c.statusText || "Gateway request failed",
|
|
88
|
-
{ status: c.status }
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
const w = c.headers.get("X-Query-Type") ?? void 0;
|
|
92
|
-
return this.onChargeSuccess?.(w), c;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
async function F(a) {
|
|
96
|
-
let t = {};
|
|
97
|
-
try {
|
|
98
|
-
t = await a.json();
|
|
99
|
-
} catch {
|
|
100
|
-
}
|
|
101
|
-
const e = t.details?.balances;
|
|
102
|
-
let s = [];
|
|
103
|
-
if (Array.isArray(e)) {
|
|
104
|
-
const i = e[0];
|
|
105
|
-
Array.isArray(i) ? s = i : i && Array.isArray(i.balances) && (s = i.balances);
|
|
106
|
-
}
|
|
107
|
-
return new q({
|
|
108
|
-
balances: s,
|
|
109
|
-
queryType: t.details?.queryType ?? "",
|
|
110
|
-
currentBalance: t.details?.currentBalance ?? null
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
async function N(a) {
|
|
114
|
-
if (!(a.headers.get("content-type") ?? "").includes("application/json")) return null;
|
|
115
|
-
try {
|
|
116
|
-
const e = await a.json();
|
|
117
|
-
return e.code || e.error || null;
|
|
118
|
-
} catch {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
function D() {
|
|
123
|
-
return typeof chrome < "u" && !!chrome?.storage?.local && !!chrome?.runtime?.id;
|
|
124
|
-
}
|
|
125
|
-
const M = {
|
|
126
|
-
getItem(a) {
|
|
127
|
-
return new Promise((t) => {
|
|
128
|
-
chrome.storage.local.get([a], (e) => {
|
|
129
|
-
const s = e[a];
|
|
130
|
-
t(typeof s == "string" ? s : null);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
},
|
|
134
|
-
setItem(a, t) {
|
|
135
|
-
return new Promise((e) => {
|
|
136
|
-
chrome.storage.local.set({ [a]: t }, () => e());
|
|
137
|
-
});
|
|
138
|
-
},
|
|
139
|
-
removeItem(a) {
|
|
140
|
-
return new Promise((t) => {
|
|
141
|
-
chrome.storage.local.remove([a], () => t());
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
watch(a, t) {
|
|
145
|
-
const e = chrome?.storage?.onChanged;
|
|
146
|
-
if (!e) return () => {
|
|
147
|
-
};
|
|
148
|
-
const s = (i, n) => {
|
|
149
|
-
if (n !== "local") return;
|
|
150
|
-
const o = i[a];
|
|
151
|
-
o && t(typeof o.newValue == "string" ? o.newValue : null);
|
|
152
|
-
};
|
|
153
|
-
return e.addListener(s), () => e.removeListener(s);
|
|
154
|
-
}
|
|
155
|
-
}, x = {
|
|
156
|
-
async getItem(a) {
|
|
157
|
-
try {
|
|
158
|
-
return window.localStorage.getItem(a);
|
|
159
|
-
} catch {
|
|
160
|
-
return null;
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
async setItem(a, t) {
|
|
164
|
-
try {
|
|
165
|
-
window.localStorage.setItem(a, t);
|
|
166
|
-
} catch {
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
async removeItem(a) {
|
|
170
|
-
try {
|
|
171
|
-
window.localStorage.removeItem(a);
|
|
172
|
-
} catch {
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
watch(a, t) {
|
|
176
|
-
if (typeof window > "u") return () => {
|
|
177
|
-
};
|
|
178
|
-
const e = (s) => {
|
|
179
|
-
s.storageArea === window.localStorage && s.key === a && t(s.newValue);
|
|
180
|
-
};
|
|
181
|
-
return window.addEventListener("storage", e), () => window.removeEventListener("storage", e);
|
|
182
|
-
}
|
|
183
|
-
}, b = /* @__PURE__ */ new Map(), J = {
|
|
184
|
-
async getItem(a) {
|
|
185
|
-
return b.get(a) ?? null;
|
|
186
|
-
},
|
|
187
|
-
async setItem(a, t) {
|
|
188
|
-
b.set(a, t);
|
|
189
|
-
},
|
|
190
|
-
async removeItem(a) {
|
|
191
|
-
b.delete(a);
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
function C(a) {
|
|
195
|
-
return a || (D() ? M : typeof window < "u" && "localStorage" in window ? x : J);
|
|
196
|
-
}
|
|
197
|
-
const y = {
|
|
198
|
-
visitorId: "pw-visitor-id",
|
|
199
|
-
lastLoginMethod: (a) => `pw-${a}-last-login-method`,
|
|
200
|
-
lastLoginEmail: (a) => `pw-${a}-last-login-email`,
|
|
201
|
-
// last-known PaywallUser. Используется как offline-fallback на старте, пока
|
|
202
|
-
// первый getUser() не вернётся. Ключ зависит от paywallId+identity hash —
|
|
203
|
-
// переключение identity не должно отдавать чужой user.
|
|
204
|
-
userState: (a, t) => `pw-${a}-${t}-user-v1`,
|
|
205
|
-
// Persisted auth bundle (access_token, refresh_token, expires_at, user) для
|
|
206
|
-
// одного пейвола. Ключ привязан к paywallId — мульти-пейвольное приложение
|
|
207
|
-
// не пересекает сессии. Bump '-v1' на breaking shape change.
|
|
208
|
-
authSession: (a) => `pw-${a}-auth-v1`,
|
|
209
|
-
// Refresh-token последнего анонимного юзера. Хранится отдельно от authSession,
|
|
210
|
-
// потому что должен пережить signOut: после signOut() юзер может опять
|
|
211
|
-
// зайти как тот же аноним — без капчи, через этот токен. signIn другим
|
|
212
|
-
// методом (email/oauth) тоже его не трогает. Чистится только явным
|
|
213
|
-
// signOut({forgetAnonymous: true}) или 401 от refresh-эндпоинта (значит
|
|
214
|
-
// токен отозван, дальше держать бессмысленно).
|
|
215
|
-
anonRefreshToken: (a) => `pw-${a}-anon-rt-v1`,
|
|
216
|
-
// Persisted bootstrap (settings/prices/offers/layout/locales/version) для
|
|
217
|
-
// stale-while-revalidate. Не зависит от identity — layout одинаков для всех
|
|
218
|
-
// юзеров одного пейвола; user-state живёт отдельно под `userState(...)`.
|
|
219
|
-
// Bump '-v1' на breaking shape change.
|
|
220
|
-
bootstrap: (a) => `pw-${a}-bootstrap-v1`,
|
|
221
|
-
// Persisted balances (AI-провайдеры × tokenization_queries). Identity-bound,
|
|
222
|
-
// т.к. balance считается per-Bearer-юзеру; при re-login ключ меняется и
|
|
223
|
-
// чужие balances не видны. Меняются после оплаты (бэк) и API-вызовов
|
|
224
|
-
// (оптимистично через `decrementBalanceLocal`).
|
|
225
|
-
balances: (a, t) => `pw-${a}-${t}-balances-v1`
|
|
226
|
-
};
|
|
227
|
-
function E() {
|
|
228
|
-
const a = typeof globalThis < "u" ? globalThis.crypto : void 0;
|
|
229
|
-
if (a && typeof a.randomUUID == "function") return a.randomUUID();
|
|
230
|
-
const t = new Uint8Array(16);
|
|
231
|
-
if (a && typeof a.getRandomValues == "function")
|
|
232
|
-
a.getRandomValues(t);
|
|
233
|
-
else
|
|
234
|
-
for (let s = 0; s < 16; s++) t[s] = Math.floor(Math.random() * 256);
|
|
235
|
-
t[6] = t[6] & 15 | 64, t[8] = t[8] & 63 | 128;
|
|
236
|
-
const e = Array.from(t, (s) => s.toString(16).padStart(2, "0")).join("");
|
|
237
|
-
return `${e.slice(0, 8)}-${e.slice(8, 12)}-${e.slice(12, 16)}-${e.slice(16, 20)}-${e.slice(20)}`;
|
|
238
|
-
}
|
|
239
|
-
async function S(a) {
|
|
240
|
-
try {
|
|
241
|
-
const e = await a.getItem(y.visitorId);
|
|
242
|
-
if (e && typeof e == "string" && e.length >= 16) return e;
|
|
243
|
-
} catch {
|
|
244
|
-
}
|
|
245
|
-
const t = E();
|
|
246
|
-
try {
|
|
247
|
-
await a.setItem(y.visitorId, t);
|
|
248
|
-
} catch {
|
|
249
|
-
}
|
|
250
|
-
return t;
|
|
251
|
-
}
|
|
252
|
-
const H = 5e3, V = 30 * 6e4, I = 60 * 6e4, j = 5 * 6e4, B = {
|
|
253
|
-
has_active_subscription: !1,
|
|
254
|
-
purchases: [],
|
|
255
|
-
trial: null
|
|
256
|
-
};
|
|
257
|
-
function _(a) {
|
|
258
|
-
return a && (a.email || a.userId || a.anonymousId) || "guest";
|
|
259
|
-
}
|
|
260
|
-
function X(a, t) {
|
|
261
|
-
return a === t ? !0 : !a || !t ? !1 : JSON.stringify(a) === JSON.stringify(t);
|
|
262
|
-
}
|
|
263
|
-
const z = 5e3, A = 5 * 6e4, G = 3e4;
|
|
264
|
-
function Q(a, t) {
|
|
265
|
-
if (a === t) return !0;
|
|
266
|
-
if (!a || !t || a.length !== t.length) return !1;
|
|
267
|
-
for (let e = 0; e < a.length; e++)
|
|
268
|
-
if (a[e].type !== t[e].type || a[e].count !== t[e].count) return !1;
|
|
269
|
-
return !0;
|
|
270
|
-
}
|
|
271
|
-
const W = "https://appbox.space";
|
|
272
|
-
class ut {
|
|
273
|
-
constructor(t) {
|
|
274
|
-
if (this.cachedBootstrap = null, this.cachedBootstrapAt = 0, this.inflightBootstrap = null, this.bootstrapListeners = /* @__PURE__ */ new Set(), this.bootstrapStorageUnwatch = null, this.authUnsubscribe = null, this.cachedUser = null, this.cachedUserAt = 0, this.inflightUser = null, this.userListeners = /* @__PURE__ */ new Set(), this.visitorIdPromise = null, this.visitorId = null, this.inflightCheckouts = /* @__PURE__ */ new Map(), this.cachedBalances = null, this.cachedBalancesAt = 0, this.balancesStorageUnwatch = null, this.inflightBalances = null, this.balanceListeners = /* @__PURE__ */ new Set(), this.previewVersionCounter = 0, !t.paywallId)
|
|
275
|
-
throw new r("invalid_config", "paywallId is required");
|
|
276
|
-
this.paywallId = t.paywallId, this.apiOrigin = t.apiOrigin ?? W, this.capabilities = t.capabilities, this.auth = t.auth, this.previewMode = t.preview === !0;
|
|
277
|
-
const e = t.auth?.getCachedUser();
|
|
278
|
-
this.identity = t.identity ?? (e ? T(e) : void 0), this.apiKey = t.apiKey, this.fetchImpl = t.fetch, t.apiKey && typeof window < "u" && typeof window.document < "u" && console.error(
|
|
279
|
-
"[paywall] SECURITY: BillingClient.apiKey detected in browser context. This is a server-SDK key and exposes your account. Remove apiKey or move BillingClient to a trusted backend."
|
|
280
|
-
), this.storage = C(t.storage), this.api = new O({
|
|
281
|
-
apiOrigin: this.apiOrigin,
|
|
282
|
-
paywallId: t.paywallId,
|
|
283
|
-
capabilities: t.capabilities,
|
|
284
|
-
fetch: t.fetch,
|
|
285
|
-
// Bearer прокидывается каждый запрос. AuthClient.getAccessToken
|
|
286
|
-
// делает lazy refresh, дедупит, на 401 возвращает null — тогда
|
|
287
|
-
// Authorization-хедер просто не выставится.
|
|
288
|
-
getAuthToken: t.auth ? () => t.auth.getAccessToken() : void 0
|
|
289
|
-
}), t.auth && (this.authUnsubscribe = t.auth.onAuthChange((s) => {
|
|
290
|
-
const i = s ? T(s.user) : void 0;
|
|
291
|
-
Y(this.identity, i) || this.setIdentity(i);
|
|
292
|
-
})), this.hydrateUserFromStorage(), this.hydrateBootstrapFromStorage(), this.subscribeBootstrapStorage(), this.hydrateBalancesFromStorage(), this.subscribeBalancesStorage(), this.visitorIdPromise = S(this.storage).then((s) => (this.visitorId = s, s));
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Stable visitor_id (UUID v4). Первый вызов awaitит первичный резолв из
|
|
296
|
-
* storage; последующие — мгновенно из in-memory кеша. Используется
|
|
297
|
-
* EventTracker'ом для атрибуции аналитики.
|
|
298
|
-
*/
|
|
299
|
-
async getVisitorId() {
|
|
300
|
-
return this.visitorId ? this.visitorId : (this.visitorIdPromise || (this.visitorIdPromise = S(this.storage).then((t) => (this.visitorId = t, t))), this.visitorIdPromise);
|
|
301
|
-
}
|
|
302
|
-
/** Sync-доступ к visitor_id. null если ещё не зарезолвили (первые ms жизни). */
|
|
303
|
-
getCachedVisitorId() {
|
|
304
|
-
return this.visitorId;
|
|
305
|
-
}
|
|
306
|
-
setIdentity(t) {
|
|
307
|
-
this.identity = t, this.cachedUser = null, this.cachedUserAt = 0, this.inflightUser = null, this.cachedBalances = null, this.cachedBalancesAt = 0, this.inflightBalances = null, this.balancesStorageUnwatch && (this.balancesStorageUnwatch(), this.balancesStorageUnwatch = null), this.hydrateBalancesFromStorage(), this.subscribeBalancesStorage(), this.hydrateUserFromStorage(), t && this.getUser({ force: !0 }).catch(() => {
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
/**
|
|
311
|
-
* Отписаться от auth-event'ов и сбросить listener'ы. Вызывать когда
|
|
312
|
-
* BillingClient больше не нужен (тесты, hot-reload, переинициализация).
|
|
313
|
-
* Без destroy() listener на AuthClient переживёт BillingClient и будет
|
|
314
|
-
* дёргать setIdentity на освобождённом инстансе. Слушатели user/balance
|
|
315
|
-
* чистятся, чтобы упавший host (например, размонтированный React-tree)
|
|
316
|
-
* не держал замыкания на эти колбеки.
|
|
317
|
-
*/
|
|
318
|
-
destroy() {
|
|
319
|
-
this.authUnsubscribe && (this.authUnsubscribe(), this.authUnsubscribe = null), this.bootstrapStorageUnwatch && (this.bootstrapStorageUnwatch(), this.bootstrapStorageUnwatch = null), this.balancesStorageUnwatch && (this.balancesStorageUnwatch(), this.balancesStorageUnwatch = null), this.userListeners.clear(), this.balanceListeners.clear(), this.bootstrapListeners.clear();
|
|
320
|
-
}
|
|
321
|
-
getIdentity() {
|
|
322
|
-
return this.identity;
|
|
323
|
-
}
|
|
324
|
-
getStorage() {
|
|
325
|
-
return this.storage;
|
|
326
|
-
}
|
|
327
|
-
async bootstrap(t = !1) {
|
|
328
|
-
const e = typeof t == "boolean" ? { force: t } : t;
|
|
329
|
-
if (this.previewMode) {
|
|
330
|
-
if (this.cachedBootstrap) return this.cachedBootstrap;
|
|
331
|
-
throw new r(
|
|
332
|
-
"invalid_config",
|
|
333
|
-
"BillingClient in preview mode but cachedBootstrap is not seeded. Call setBootstrap(bootstrap) before open()."
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
const s = Date.now(), i = this.cachedBootstrap && this.cachedBootstrapAt > 0 && s - this.cachedBootstrapAt < I;
|
|
337
|
-
return !e.force && i ? (s - this.cachedBootstrapAt > j && this.revalidateBootstrap(e.signal).catch(() => {
|
|
338
|
-
}), this.cachedBootstrap) : this.inflightBootstrap ? this.inflightBootstrap : (this.inflightBootstrap = this.fetchBootstrap({
|
|
339
|
-
ifVersion: e.force ? void 0 : this.cachedBootstrap?.version,
|
|
340
|
-
signal: e.signal
|
|
341
|
-
}).finally(() => {
|
|
342
|
-
this.inflightBootstrap = null;
|
|
343
|
-
}), this.inflightBootstrap);
|
|
344
|
-
}
|
|
345
|
-
/**
|
|
346
|
-
* Подписка на изменения bootstrap'а: applyBootstrap (сетевой revalidate,
|
|
347
|
-
* cross-context storage.watch). Срабатывает ТОЛЬКО при реальном изменении
|
|
348
|
-
* `version` (unchanged-ответ от сервера не дёргает listener'ов). Возвращает
|
|
349
|
-
* unsubscribe.
|
|
350
|
-
*/
|
|
351
|
-
onBootstrapChange(t) {
|
|
352
|
-
return this.bootstrapListeners.add(t), () => {
|
|
353
|
-
this.bootstrapListeners.delete(t);
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
/**
|
|
357
|
-
* Заменить cachedBootstrap частичными или полными данными и эмитнуть всем
|
|
358
|
-
* подписчикам. Используется host'ом в preview-mode (редактор админки) для
|
|
359
|
-
* live-обновления открытой модалки без сетевого revalidate'а.
|
|
360
|
-
*
|
|
361
|
-
* Поведение:
|
|
362
|
-
* - Без `cachedBootstrap` ожидаются как минимум `settings` + `prices` —
|
|
363
|
-
* иначе PaywallRoot не сможет отрендерить тарифы и упадёт.
|
|
364
|
-
* - С существующим кешем партиал мёрджится поверх: `settings` глубокий мёрдж
|
|
365
|
-
* на 1 уровень (поля настроек), массивы `prices`/`offers` перезаписываются.
|
|
366
|
-
* - Каждый вызов бампит `version` ("preview:<n>"), чтобы applyBootstrap'овая
|
|
367
|
-
* проверка `versionChanged` всегда срабатывала и listener'ы дёргались.
|
|
368
|
-
* - Persist в storage НЕ делаем — preview не должен утекать в другие вкладки.
|
|
369
|
-
*
|
|
370
|
-
* В non-preview режиме метод доступен, но это редкий путь (например, для
|
|
371
|
-
* тестов host'а) — production-код должен полагаться на bootstrap() + revalidate.
|
|
372
|
-
*/
|
|
373
|
-
setBootstrap(t) {
|
|
374
|
-
const e = this.cachedBootstrap ?? {
|
|
375
|
-
settings: { id: this.paywallId, name: "" },
|
|
376
|
-
prices: [],
|
|
377
|
-
offers: []
|
|
378
|
-
}, s = {
|
|
379
|
-
...e,
|
|
380
|
-
...t,
|
|
381
|
-
settings: t.settings !== void 0 ? { ...e.settings, ...t.settings } : e.settings,
|
|
382
|
-
prices: t.prices !== void 0 ? t.prices : e.prices,
|
|
383
|
-
offers: t.offers !== void 0 ? t.offers : e.offers,
|
|
384
|
-
version: `preview:${++this.previewVersionCounter}`
|
|
385
|
-
};
|
|
386
|
-
s.layout || (s.layout = U(s.settings, s.prices)), g(s), this.cachedBootstrap = s, this.cachedBootstrapAt = Date.now();
|
|
387
|
-
for (const i of this.bootstrapListeners)
|
|
388
|
-
try {
|
|
389
|
-
i(s);
|
|
390
|
-
} catch (n) {
|
|
391
|
-
console.warn("[paywall] onBootstrapChange listener threw", n);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// Network primitive — единая точка для force-запроса, revalidate'а и
|
|
395
|
-
// первого холодного bootstrap'а. `ifVersion` шлёт server-side short-circuit:
|
|
396
|
-
// если совпала — бэк отвечает `{unchanged: true, version, user}` и мы лишь
|
|
397
|
-
// обновляем cached user, structure (layout/prices/offers/locales) не трогаем.
|
|
398
|
-
async fetchBootstrap(t) {
|
|
399
|
-
const e = {};
|
|
400
|
-
this.identity?.email && (e["X-User-Email"] = this.identity.email);
|
|
401
|
-
const s = t.ifVersion ? `/api/v1/paywall/${this.paywallId}/bootstrap?if_version=${encodeURIComponent(t.ifVersion)}` : `/api/v1/paywall/${this.paywallId}/bootstrap`, i = await this.api.request(s, {
|
|
402
|
-
...Object.keys(e).length ? { headers: e } : {},
|
|
403
|
-
signal: t.signal
|
|
404
|
-
});
|
|
405
|
-
if ("unchanged" in i && i.unchanged)
|
|
406
|
-
return this.cachedBootstrap ? (this.cachedBootstrapAt = Date.now(), i.user && this.applyUser(i.user), this.cachedBootstrap) : this.fetchBootstrap({ signal: t.signal });
|
|
407
|
-
const n = i;
|
|
408
|
-
return n.layout || (n.layout = U(n.settings, n.prices)), g(n), this.applyBootstrap(n, { persist: !0 }), n.user && this.applyUser(n.user), n;
|
|
409
|
-
}
|
|
410
|
-
// Фоновый revalidate из stale-while-revalidate ветки. Дедуплицируется через
|
|
411
|
-
// `inflightBootstrap`, чтобы параллельные revalidate'ы не пересекались.
|
|
412
|
-
revalidateBootstrap(t) {
|
|
413
|
-
return this.inflightBootstrap ? this.inflightBootstrap : (this.inflightBootstrap = this.fetchBootstrap({
|
|
414
|
-
ifVersion: this.cachedBootstrap?.version,
|
|
415
|
-
signal: t
|
|
416
|
-
}).finally(() => {
|
|
417
|
-
this.inflightBootstrap = null;
|
|
418
|
-
}), this.inflightBootstrap);
|
|
419
|
-
}
|
|
420
|
-
// Применяет fresh bootstrap к state: emit listeners ТОЛЬКО при изменении
|
|
421
|
-
// version (т.е. structure реально другая). Это нужно, чтобы повторный
|
|
422
|
-
// applyBootstrap из storage.watch не перерисовал UI зря, если другая
|
|
423
|
-
// вкладка нашла тот же version. persist=false для пути «получили из
|
|
424
|
-
// storage» — там кто-то другой уже записал.
|
|
425
|
-
applyBootstrap(t, { persist: e }) {
|
|
426
|
-
const s = !this.cachedBootstrap || this.cachedBootstrap.version !== t.version;
|
|
427
|
-
if (this.cachedBootstrap = t, this.cachedBootstrapAt = Date.now(), e && this.persistBootstrap(t), s)
|
|
428
|
-
for (const i of this.bootstrapListeners)
|
|
429
|
-
try {
|
|
430
|
-
i(t);
|
|
431
|
-
} catch (n) {
|
|
432
|
-
console.warn("[paywall] onBootstrapChange listener threw", n);
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
async hydrateBootstrapFromStorage() {
|
|
436
|
-
if (!this.cachedBootstrap)
|
|
437
|
-
try {
|
|
438
|
-
const t = await this.storage.getItem(y.bootstrap(this.paywallId));
|
|
439
|
-
if (!t) return;
|
|
440
|
-
const e = JSON.parse(t);
|
|
441
|
-
if (!e?.bootstrap || Date.now() - e.at > I || this.cachedBootstrap) return;
|
|
442
|
-
g(e.bootstrap), this.cachedBootstrap = e.bootstrap, this.cachedBootstrapAt = e.at;
|
|
443
|
-
for (const s of this.bootstrapListeners)
|
|
444
|
-
try {
|
|
445
|
-
s(e.bootstrap);
|
|
446
|
-
} catch (i) {
|
|
447
|
-
console.warn("[paywall] onBootstrapChange listener threw", i);
|
|
448
|
-
}
|
|
449
|
-
} catch {
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
async persistBootstrap(t) {
|
|
453
|
-
if (t.version)
|
|
454
|
-
try {
|
|
455
|
-
const { user: e, ...s } = t;
|
|
456
|
-
await this.storage.setItem(
|
|
457
|
-
y.bootstrap(this.paywallId),
|
|
458
|
-
JSON.stringify({ at: Date.now(), bootstrap: s })
|
|
459
|
-
);
|
|
460
|
-
} catch {
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
// Cross-context sync: другая вкладка / popup / sw записали свежий bootstrap
|
|
464
|
-
// → мы подхватываем без сетевого запроса. Адаптеры без watch (memory) —
|
|
465
|
-
// no-op, всё работает как раньше через сеть.
|
|
466
|
-
subscribeBootstrapStorage() {
|
|
467
|
-
typeof this.storage.watch == "function" && (this.bootstrapStorageUnwatch = this.storage.watch(
|
|
468
|
-
y.bootstrap(this.paywallId),
|
|
469
|
-
(t) => {
|
|
470
|
-
if (t)
|
|
471
|
-
try {
|
|
472
|
-
const e = JSON.parse(t);
|
|
473
|
-
if (!e?.bootstrap) return;
|
|
474
|
-
if (this.cachedBootstrap?.version && this.cachedBootstrap.version === e.bootstrap.version) {
|
|
475
|
-
this.cachedBootstrapAt = e.at;
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
g(e.bootstrap), this.applyBootstrap(e.bootstrap, { persist: !1 });
|
|
479
|
-
} catch {
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
));
|
|
483
|
-
}
|
|
484
|
-
/** Возвращает последний загруженный bootstrap без сетевого запроса.
|
|
485
|
-
* null = bootstrap ещё не загружали. Удобно для post-checkout-логики
|
|
486
|
-
* (PaywallUI читает success_redirect_url, не делая второго round-trip'а). */
|
|
487
|
-
getCachedBootstrap() {
|
|
488
|
-
return this.cachedBootstrap;
|
|
489
|
-
}
|
|
490
|
-
/**
|
|
491
|
-
* Шорткат поверх `bootstrap()`: ждёт загрузку структуры пейвола и возвращает
|
|
492
|
-
* цены. Полезно когда host рисует цены вне модалки (карточки на лендинге,
|
|
493
|
-
* "Pricing" page и т.п.) и не хочет руками распаковывать bootstrap.
|
|
494
|
-
*
|
|
495
|
-
* Locale-оверрайды (`label`/`description` под `navigator.language`) уже
|
|
496
|
-
* применены — массив готов к рендеру. Кэш/TTL/stale-while-revalidate — те
|
|
497
|
-
* же, что у `bootstrap()`: повторный вызов не штурмует сервер.
|
|
498
|
-
*/
|
|
499
|
-
async getPrices(t = {}) {
|
|
500
|
-
return (await this.bootstrap(t)).prices;
|
|
501
|
-
}
|
|
502
|
-
/** Sync-снимок цен из последнего bootstrap'а. null = ещё не загружали. */
|
|
503
|
-
getCachedPrices() {
|
|
504
|
-
return this.cachedBootstrap?.prices ?? null;
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
* Снимок того, какой язык SDK сейчас считает «языком юзера». Полезно для
|
|
508
|
-
* синхронизации i18n хоста с тем, что фактически показывает пейвол — чтобы
|
|
509
|
-
* окружающий UI не противоречил модалке (например, host рисует кнопку
|
|
510
|
-
* "Subscribe" на английском, а пейвол показывает «Подписаться» на русском).
|
|
511
|
-
*
|
|
512
|
-
* Возвращает структуру, а не один тэг, чтобы интегратор мог:
|
|
513
|
-
* - быстро взять `tag` для своих переводов;
|
|
514
|
-
* - отличить «пейвол реально на этом языке» (`applied !== null`) от
|
|
515
|
-
* «SDK угадал, но локали для этого языка нет — рендерится база»;
|
|
516
|
-
* - решить, чему доверять при противоречии browserLanguage vs countryLanguage
|
|
517
|
-
* (тур, expat, VPN — у каждого свой ответ).
|
|
518
|
-
*
|
|
519
|
-
* Sync-вызов: данные уже в bootstrap'е, отдельных запросов не делает.
|
|
520
|
-
* Если `bootstrap()` ещё не вызывался — `applied` и `countryLanguage`
|
|
521
|
-
* будут `null`, но `browserLanguage` и `tag` всё равно отдадутся, если
|
|
522
|
-
* есть `navigator.language`.
|
|
523
|
-
*/
|
|
524
|
-
getUserLanguage() {
|
|
525
|
-
const t = typeof navigator < "u" && navigator.language ? navigator.language : null, e = this.cachedBootstrap?.settings.locale_default ?? null, s = this.cachedBootstrap ? L(this.cachedBootstrap) : null;
|
|
526
|
-
return { tag: s ?? t ?? e, applied: s, browserLanguage: t, countryLanguage: e };
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Получить актуальное состояние подписки/покупок.
|
|
530
|
-
*
|
|
531
|
-
* - In-memory cache TTL 5с — naïve setInterval(1000) не нагружает сервер.
|
|
532
|
-
* - In-flight dedupe — параллельные вызовы получают один promise.
|
|
533
|
-
* - `force: true` обходит кеш (для post-checkout проверки).
|
|
534
|
-
* - Без identity возвращает empty-state (сервер тоже так делает).
|
|
535
|
-
*/
|
|
536
|
-
async getUser({ force: t = !1, signal: e } = {}) {
|
|
537
|
-
return !t && this.cachedUser && Date.now() - this.cachedUserAt < H ? this.cachedUser : this.inflightUser ? this.inflightUser : (this.inflightUser = (async () => {
|
|
538
|
-
try {
|
|
539
|
-
if (!this.identity?.email)
|
|
540
|
-
return this.applyUser(B), B;
|
|
541
|
-
const s = await this.api.request(
|
|
542
|
-
`/api/v1/paywall/${this.paywallId}/user-state`,
|
|
543
|
-
{ headers: { "X-User-Email": this.identity.email }, signal: e }
|
|
544
|
-
);
|
|
545
|
-
return this.applyUser(s), s;
|
|
546
|
-
} finally {
|
|
547
|
-
this.inflightUser = null;
|
|
548
|
-
}
|
|
549
|
-
})(), this.inflightUser);
|
|
550
|
-
}
|
|
551
|
-
/**
|
|
552
|
-
* Подписка на изменения user-state. Колбек вызывается:
|
|
553
|
-
* - сразу с last-known user (если есть в кеше) — по умолчанию через
|
|
554
|
-
* microtask, опционально SYNC (см. опции);
|
|
555
|
-
* - на каждое реальное изменение (getUser/bootstrap принёс другой shape).
|
|
556
|
-
*
|
|
557
|
-
* `opts.immediate`:
|
|
558
|
-
* - `'microtask'` (default) — initial snapshot отдаётся в queueMicrotask,
|
|
559
|
-
* чтобы host успел доресетнуть state в том же тике. Безопасный выбор
|
|
560
|
-
* для большинства интеграций.
|
|
561
|
-
* - `'sync'` — initial snapshot отдаётся прямо в текущем frame'е, до
|
|
562
|
-
* возврата из onUserChange. Удобно для React/Vue useEffect-cleanup'а
|
|
563
|
-
* (избегаем лишнего ре-рендера) и SSR (мгновенная синхронизация).
|
|
564
|
-
* - `'none'` — не отдавать initial snapshot, только реальные изменения.
|
|
565
|
-
*
|
|
566
|
-
* Возвращает функцию отписки.
|
|
567
|
-
*/
|
|
568
|
-
onUserChange(t, e = {}) {
|
|
569
|
-
this.userListeners.add(t);
|
|
570
|
-
const s = e.immediate ?? "microtask";
|
|
571
|
-
if (this.cachedUser && s !== "none") {
|
|
572
|
-
const i = this.cachedUser;
|
|
573
|
-
if (s === "sync")
|
|
574
|
-
try {
|
|
575
|
-
t(i);
|
|
576
|
-
} catch (n) {
|
|
577
|
-
console.warn("[paywall] onUserChange initial sync threw", n);
|
|
578
|
-
}
|
|
579
|
-
else
|
|
580
|
-
queueMicrotask(() => {
|
|
581
|
-
this.userListeners.has(t) && t(i);
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
return () => {
|
|
585
|
-
this.userListeners.delete(t);
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
/** Текущий cached user без сетевого запроса. null = ещё не загружали. */
|
|
589
|
-
getCachedUser() {
|
|
590
|
-
return this.cachedUser;
|
|
591
|
-
}
|
|
592
|
-
applyUser(t) {
|
|
593
|
-
const e = !X(this.cachedUser, t);
|
|
594
|
-
if (this.cachedUser = t, this.cachedUserAt = Date.now(), e) {
|
|
595
|
-
this.persistUser(t);
|
|
596
|
-
for (const s of this.userListeners)
|
|
597
|
-
try {
|
|
598
|
-
s(t);
|
|
599
|
-
} catch (i) {
|
|
600
|
-
console.warn("[paywall] onUserChange listener threw", i);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
storageKey() {
|
|
605
|
-
return y.userState(this.paywallId, _(this.identity));
|
|
606
|
-
}
|
|
607
|
-
async hydrateUserFromStorage() {
|
|
608
|
-
if (!this.cachedUser)
|
|
609
|
-
try {
|
|
610
|
-
const t = await this.storage.getItem(this.storageKey());
|
|
611
|
-
if (!t) return;
|
|
612
|
-
const e = JSON.parse(t);
|
|
613
|
-
if (!e?.user || Date.now() - e.at > V || this.cachedUser) return;
|
|
614
|
-
this.applyUser(e.user);
|
|
615
|
-
} catch {
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
async persistUser(t) {
|
|
619
|
-
try {
|
|
620
|
-
await this.storage.setItem(
|
|
621
|
-
this.storageKey(),
|
|
622
|
-
JSON.stringify({ at: Date.now(), user: t })
|
|
623
|
-
);
|
|
624
|
-
} catch {
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
/**
|
|
628
|
-
* Балансы AI-провайдеров (`paywall_balances` × `tokenization_queries`).
|
|
629
|
-
*
|
|
630
|
-
* - In-memory cache TTL 5с — параллельные UI-renders не дёргают сеть;
|
|
631
|
-
* - In-flight dedupe — параллельные `getBalances` получают один promise;
|
|
632
|
-
* - `force: true` обходит кеш (типичный кейс — после QuotaExceededError);
|
|
633
|
-
* - Без auth (Bearer не выдан) возвращает пустой массив без сетевого
|
|
634
|
-
* запроса: бэк всё равно ответит 401, нет смысла тратить round-trip.
|
|
635
|
-
*
|
|
636
|
-
* Если у пейвола `tokenization=false` — бэк отдаёт `[]`, как для гостя.
|
|
637
|
-
* SDK не различает «нет квоты» и «нет квот вообще» — caller сам решает
|
|
638
|
-
* по `currentBalance` в QuotaExceededError или `balances.length`.
|
|
639
|
-
*/
|
|
640
|
-
async getBalances({ force: t = !1, signal: e } = {}) {
|
|
641
|
-
const s = Date.now(), i = this.cachedBalances ? s - this.cachedBalancesAt : 1 / 0;
|
|
642
|
-
return !t && this.cachedBalances && (i < z || i < G) ? this.cachedBalances : !t && this.cachedBalances && i < A ? (this.fetchBalances({ signal: e }).catch(() => {
|
|
643
|
-
}), this.cachedBalances) : this.inflightBalances ? this.inflightBalances : this.fetchBalances({ signal: e });
|
|
644
|
-
}
|
|
645
|
-
// Network primitive — единая точка для force/stale-revalidate/cold-start.
|
|
646
|
-
// Дедуплицируется через `inflightBalances`.
|
|
647
|
-
fetchBalances({ signal: t } = {}) {
|
|
648
|
-
return this.inflightBalances ? this.inflightBalances : (this.inflightBalances = (async () => {
|
|
649
|
-
try {
|
|
650
|
-
if (!this.auth)
|
|
651
|
-
return this.applyBalances([]), [];
|
|
652
|
-
const e = await this.api.request(`/api/v1/paywall/${this.paywallId}/balances`, { signal: t }), s = Array.isArray(e.balances) ? e.balances : [];
|
|
653
|
-
return this.applyBalances(s), s;
|
|
654
|
-
} finally {
|
|
655
|
-
this.inflightBalances = null;
|
|
656
|
-
}
|
|
657
|
-
})(), this.inflightBalances);
|
|
658
|
-
}
|
|
659
|
-
/** Sync snapshot. null = ещё не загружали (или explicit clear на re-login). */
|
|
660
|
-
getCachedBalances() {
|
|
661
|
-
return this.cachedBalances;
|
|
662
|
-
}
|
|
663
|
-
/**
|
|
664
|
-
* Подписка на изменения балансов: getBalances/decrementBalanceLocal/setIdentity.
|
|
665
|
-
* `opts.immediate` работает так же, как в `onUserChange`: 'microtask'
|
|
666
|
-
* (default), 'sync' (для React/Vue useEffect), 'none' (только изменения).
|
|
667
|
-
* Возвращает unsubscribe.
|
|
668
|
-
*/
|
|
669
|
-
onBalanceChange(t, e = {}) {
|
|
670
|
-
this.balanceListeners.add(t);
|
|
671
|
-
const s = e.immediate ?? "microtask";
|
|
672
|
-
if (this.cachedBalances && s !== "none") {
|
|
673
|
-
const i = this.cachedBalances;
|
|
674
|
-
if (s === "sync")
|
|
675
|
-
try {
|
|
676
|
-
t(i);
|
|
677
|
-
} catch (n) {
|
|
678
|
-
console.warn("[paywall] onBalanceChange initial sync threw", n);
|
|
679
|
-
}
|
|
680
|
-
else
|
|
681
|
-
queueMicrotask(() => {
|
|
682
|
-
this.balanceListeners.has(t) && t(i);
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
return () => {
|
|
686
|
-
this.balanceListeners.delete(t);
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
/**
|
|
690
|
-
* Оптимистично уменьшает count для `queryType` на 1 и нотифицирует
|
|
691
|
-
* listener'ов. Используется ApiGatewayClient'ом сразу после успешного
|
|
692
|
-
* gateway-вызова (бэк уже снял кредит, см. `chargeApiQueries`).
|
|
693
|
-
*
|
|
694
|
-
* Если queryType отсутствует в кеше или count<=0 — no-op (не уходим в
|
|
695
|
-
* отрицательные значения, бэк всё равно правильный source-of-truth).
|
|
696
|
-
* Если кеша нет вовсе — тоже no-op: явный getBalances({force:true}) на
|
|
697
|
-
* следующем рендере подтянет актуальный shape.
|
|
698
|
-
*
|
|
699
|
-
* queryType может быть undefined (gateway не прислал X-Query-Type) —
|
|
700
|
-
* в этом случае декремент не делаем, но просим refreshBalances() для
|
|
701
|
-
* выравнивания.
|
|
702
|
-
*/
|
|
703
|
-
decrementBalanceLocal(t) {
|
|
704
|
-
if (!t) {
|
|
705
|
-
this.getBalances({ force: !0 });
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (!this.cachedBalances) return;
|
|
709
|
-
const e = this.cachedBalances.findIndex((n) => n.type === t);
|
|
710
|
-
if (e < 0 || this.cachedBalances[e].count <= 0) return;
|
|
711
|
-
const i = this.cachedBalances.map(
|
|
712
|
-
(n, o) => o === e ? { ...n, count: n.count - 1 } : n
|
|
713
|
-
);
|
|
714
|
-
this.applyBalances(i);
|
|
715
|
-
}
|
|
716
|
-
/** Принудительный re-fetch — типичный вызов после QuotaExceededError, чтобы
|
|
717
|
-
* UI получил актуальный balance=0 и нарисовал upgrade-prompt. */
|
|
718
|
-
refreshBalances() {
|
|
719
|
-
return this.getBalances({ force: !0 });
|
|
720
|
-
}
|
|
721
|
-
/**
|
|
722
|
-
* Фабрика ApiGatewayClient'а с подключённым к этому billing'у balance-стейтом:
|
|
723
|
-
* - Bearer/identity берутся из текущего auth/identity;
|
|
724
|
-
* - на success декрементим cachedBalances оптимистично;
|
|
725
|
-
* - на 402 (QuotaExceededError) триггерим refreshBalances() для актуального snapshot'а.
|
|
726
|
-
*
|
|
727
|
-
* Если переопределить опции через `overrides` — принимаются как есть, но
|
|
728
|
-
* `onChargeSuccess`/`onQuotaExceeded` всё равно вызываются (composable, host
|
|
729
|
-
* может добавить свой колбек поверх).
|
|
730
|
-
*/
|
|
731
|
-
createApiGatewayClient(t = {}) {
|
|
732
|
-
const e = t.onChargeSuccess, s = t.onQuotaExceeded;
|
|
733
|
-
return new K({
|
|
734
|
-
paywallId: this.paywallId,
|
|
735
|
-
apiOrigin: this.apiOrigin,
|
|
736
|
-
auth: this.auth,
|
|
737
|
-
userId: this.auth ? void 0 : this.identity?.userId,
|
|
738
|
-
capabilities: this.capabilities,
|
|
739
|
-
fetch: this.fetchImpl,
|
|
740
|
-
...t,
|
|
741
|
-
onChargeSuccess: (i) => {
|
|
742
|
-
this.decrementBalanceLocal(i), e?.(i);
|
|
743
|
-
},
|
|
744
|
-
onQuotaExceeded: (i) => {
|
|
745
|
-
this.refreshBalances(), s?.(i);
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
}
|
|
749
|
-
applyBalances(t, { persist: e = !0 } = {}) {
|
|
750
|
-
const s = !Q(this.cachedBalances, t);
|
|
751
|
-
if (this.cachedBalances = t, this.cachedBalancesAt = Date.now(), e && this.persistBalances(t), s)
|
|
752
|
-
for (const i of this.balanceListeners)
|
|
753
|
-
try {
|
|
754
|
-
i(t);
|
|
755
|
-
} catch (n) {
|
|
756
|
-
console.warn("[paywall] onBalanceChange listener threw", n);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
balancesStorageKey() {
|
|
760
|
-
return y.balances(this.paywallId, _(this.identity));
|
|
761
|
-
}
|
|
762
|
-
async hydrateBalancesFromStorage() {
|
|
763
|
-
if (!this.cachedBalances)
|
|
764
|
-
try {
|
|
765
|
-
const t = await this.storage.getItem(this.balancesStorageKey());
|
|
766
|
-
if (!t) return;
|
|
767
|
-
const e = JSON.parse(t);
|
|
768
|
-
if (!e?.balances || !Array.isArray(e.balances) || Date.now() - e.at > A || this.cachedBalances) return;
|
|
769
|
-
this.cachedBalances = e.balances, this.cachedBalancesAt = e.at;
|
|
770
|
-
for (const s of this.balanceListeners)
|
|
771
|
-
try {
|
|
772
|
-
s(e.balances);
|
|
773
|
-
} catch (i) {
|
|
774
|
-
console.warn("[paywall] onBalanceChange listener threw", i);
|
|
775
|
-
}
|
|
776
|
-
} catch {
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
async persistBalances(t) {
|
|
780
|
-
try {
|
|
781
|
-
await this.storage.setItem(
|
|
782
|
-
this.balancesStorageKey(),
|
|
783
|
-
JSON.stringify({ at: Date.now(), balances: t })
|
|
784
|
-
);
|
|
785
|
-
} catch {
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
// Cross-context sync: другая вкладка / popup / SW обновили balances
|
|
789
|
-
// (свежий getBalances или оптимистичный decrement) → подхватываем без
|
|
790
|
-
// сетевого запроса.
|
|
791
|
-
subscribeBalancesStorage() {
|
|
792
|
-
typeof this.storage.watch == "function" && (this.balancesStorageUnwatch = this.storage.watch(
|
|
793
|
-
this.balancesStorageKey(),
|
|
794
|
-
(t) => {
|
|
795
|
-
if (t)
|
|
796
|
-
try {
|
|
797
|
-
const e = JSON.parse(t);
|
|
798
|
-
if (!e?.balances || !Array.isArray(e.balances) || e.at <= this.cachedBalancesAt) return;
|
|
799
|
-
this.applyBalances(e.balances, { persist: !1 });
|
|
800
|
-
} catch {
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
));
|
|
804
|
-
}
|
|
805
|
-
async createCheckout(t) {
|
|
806
|
-
if (!this.identity?.email)
|
|
807
|
-
throw new r(
|
|
808
|
-
"identity_required",
|
|
809
|
-
"createCheckout requires identity with email"
|
|
810
|
-
);
|
|
811
|
-
const e = t.idempotencyKey ?? `auto:${t.priceId}`, s = this.inflightCheckouts.get(e);
|
|
812
|
-
if (s) return s;
|
|
813
|
-
const n = {
|
|
814
|
-
"Idempotency-Key": t.idempotencyKey ?? E()
|
|
815
|
-
};
|
|
816
|
-
this.apiKey && (n["X-Api-Key"] = this.apiKey);
|
|
817
|
-
const o = this.cachedBootstrap?.settings, u = t.successUrl ?? o?.success_redirect_url ?? void 0, l = t.shopUrl ?? o?.checkout_shop_url ?? void 0, d = this.api.request(`/api/v1/paywall/${this.paywallId}/start-checkout`, {
|
|
818
|
-
method: "POST",
|
|
819
|
-
headers: n,
|
|
820
|
-
signal: t.signal,
|
|
821
|
-
body: JSON.stringify({
|
|
822
|
-
email: this.identity.email,
|
|
823
|
-
priceId: Number(t.priceId),
|
|
824
|
-
successUrl: u,
|
|
825
|
-
errorUrl: t.errorUrl,
|
|
826
|
-
shopUrl: l,
|
|
827
|
-
productName: o?.checkout_product_name ?? void 0,
|
|
828
|
-
trial_days: t.trialDays,
|
|
829
|
-
ignoreActivePurchase: t.ignoreActivePurchase ? !0 : void 0,
|
|
830
|
-
userMeta: this.identity.userId ? { userId: this.identity.userId } : void 0
|
|
831
|
-
})
|
|
832
|
-
}).then((h) => ({ url: h.checkoutUrl, acquiring: h.acquiring })).catch((h) => {
|
|
833
|
-
throw h instanceof r && h.status === 409 && h.cause && typeof h.cause == "object" && h.cause.hasActivePurchase === !0 ? new r(
|
|
834
|
-
"already_purchased",
|
|
835
|
-
"You already have an active subscription",
|
|
836
|
-
{ status: 409, cause: h.cause }
|
|
837
|
-
) : h;
|
|
838
|
-
});
|
|
839
|
-
return this.inflightCheckouts.set(e, d), d.finally(() => {
|
|
840
|
-
this.inflightCheckouts.get(e) === d && this.inflightCheckouts.delete(e);
|
|
841
|
-
}).catch(() => {
|
|
842
|
-
}), d;
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* URL Stripe/Paddle/Chargebee customer portal — место, где залогиненный
|
|
846
|
-
* юзер может управлять подпиской (отменить, обновить карту, скачать
|
|
847
|
-
* инвойсы). Опен-флоу управляется host'ом:
|
|
848
|
-
*
|
|
849
|
-
* ```ts
|
|
850
|
-
* const { url } = await billing.getCustomerPortalUrl();
|
|
851
|
-
* window.open(url, '_blank');
|
|
852
|
-
* ```
|
|
853
|
-
*
|
|
854
|
-
* Auth: Bearer (через AuthClient) или server-side `apiKey`. Без auth и
|
|
855
|
-
* без apiKey бросает PaywallError('identity_required'). 403 от бэка
|
|
856
|
-
* (нет активной подписки / acquiring не поддерживает portal) пробрасывается
|
|
857
|
-
* как PaywallError('forbidden') с `status: 403` — host рендерит "no
|
|
858
|
-
* subscription to manage".
|
|
859
|
-
*/
|
|
860
|
-
async getCustomerPortalUrl(t = {}) {
|
|
861
|
-
if (!this.auth && !this.apiKey && !this.identity?.email)
|
|
862
|
-
throw new r(
|
|
863
|
-
"identity_required",
|
|
864
|
-
"getCustomerPortalUrl requires auth, apiKey, or identity.email"
|
|
865
|
-
);
|
|
866
|
-
const e = {};
|
|
867
|
-
this.apiKey && (e["X-Api-Key"] = this.apiKey);
|
|
868
|
-
const s = this.auth && this.auth.getCachedSession() ? {} : {
|
|
869
|
-
email: this.identity?.email,
|
|
870
|
-
userMeta: this.identity?.userId ? { userId: this.identity.userId } : void 0
|
|
871
|
-
};
|
|
872
|
-
return { url: (await this.api.request(
|
|
873
|
-
`/api/v1/paywall/${this.paywallId}/get-customer-portal`,
|
|
874
|
-
{
|
|
875
|
-
method: "POST",
|
|
876
|
-
headers: Object.keys(e).length ? e : void 0,
|
|
877
|
-
body: JSON.stringify(s),
|
|
878
|
-
signal: t.signal
|
|
879
|
-
}
|
|
880
|
-
)).url };
|
|
881
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Список покупок юзера с rich-полями (цена, валюта, interval, discount,
|
|
884
|
-
* cancel-метаданные). Подходит для customer-portal UI: cards с кнопками
|
|
885
|
-
* Cancel/Renew/Manage. Менее cache-friendly чем `getUser` — ходит в
|
|
886
|
-
* `/api/v1/paywall/[id]/user` без unstable_cache, потому что list для UI
|
|
887
|
-
* должен быть свежим после cancel-а.
|
|
888
|
-
*
|
|
889
|
-
* Auth: Bearer обязателен (через AuthClient). Без Bearer — 401 от бэка,
|
|
890
|
-
* пробрасываем как PaywallError('http_401'). Гость → пустой список.
|
|
891
|
-
*/
|
|
892
|
-
async listPurchases(t = {}) {
|
|
893
|
-
if (!this.auth)
|
|
894
|
-
throw new r(
|
|
895
|
-
"auth_required",
|
|
896
|
-
"listPurchases requires AuthClient (Bearer auth)"
|
|
897
|
-
);
|
|
898
|
-
return (await this.api.request(`/api/v1/paywall/${this.paywallId}/user`, {
|
|
899
|
-
method: "GET",
|
|
900
|
-
signal: t.signal
|
|
901
|
-
})).purchases ?? [];
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
|
-
* Отменить подписку. Бэк проверит что subscription принадлежит auth-юзеру
|
|
905
|
-
* и сделает cancel у acquiring'а (Stripe/Paddle/Chargebee). По умолчанию
|
|
906
|
-
* cancel в конце текущего периода — юзер сохраняет access до renewal date'ы.
|
|
907
|
-
*
|
|
908
|
-
* `reason` обязательна (валидация на бэке). Удобно собрать через select
|
|
909
|
-
* причин в host-UI, как в legacy customer portal'е.
|
|
910
|
-
*
|
|
911
|
-
* Auth: Bearer обязателен.
|
|
912
|
-
*/
|
|
913
|
-
async cancelSubscription(t) {
|
|
914
|
-
if (!this.auth)
|
|
915
|
-
throw new r(
|
|
916
|
-
"auth_required",
|
|
917
|
-
"cancelSubscription requires AuthClient (Bearer auth)"
|
|
918
|
-
);
|
|
919
|
-
return this.api.request("/api/paywall/cancel-subscription", {
|
|
920
|
-
method: "POST",
|
|
921
|
-
body: JSON.stringify({
|
|
922
|
-
subscriptionId: t.subscriptionId,
|
|
923
|
-
paywallId: this.paywallId,
|
|
924
|
-
cancellationReason: t.reason
|
|
925
|
-
}),
|
|
926
|
-
signal: t.signal
|
|
927
|
-
});
|
|
928
|
-
}
|
|
929
|
-
/**
|
|
930
|
-
* Создаёт саппорт-тикет. Если есть `files` — multipart/form-data, иначе JSON.
|
|
931
|
-
* Email берётся (1) из явного поля payload.email; (2) из identity если оно есть.
|
|
932
|
-
* Если ни того, ни другого нет — бэк отвергнет тикет (`email_required`).
|
|
933
|
-
*
|
|
934
|
-
* Bearer-токен (если AuthClient подключён) добавляется автоматически — бэк
|
|
935
|
-
* перевешивает customer_email на email из сессии (защита от подделки).
|
|
936
|
-
*/
|
|
937
|
-
async createSupportTicket(t) {
|
|
938
|
-
const e = t.email ?? this.identity?.email ?? null, s = `/api/v1/paywall/${this.paywallId}/support/ticket`;
|
|
939
|
-
if (!!t.files && t.files.length > 0) {
|
|
940
|
-
const n = new FormData();
|
|
941
|
-
n.set("subject", t.subject), n.set("content", t.content), e && n.set("customer_email", e);
|
|
942
|
-
for (const o of t.files) n.append("files", o);
|
|
943
|
-
return this.api.request(s, {
|
|
944
|
-
method: "POST",
|
|
945
|
-
body: n
|
|
946
|
-
});
|
|
947
|
-
}
|
|
948
|
-
return this.api.request(s, {
|
|
949
|
-
method: "POST",
|
|
950
|
-
body: JSON.stringify({
|
|
951
|
-
subject: t.subject,
|
|
952
|
-
content: t.content,
|
|
953
|
-
customer_email: e
|
|
954
|
-
})
|
|
955
|
-
});
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
function T(a) {
|
|
959
|
-
return { email: a.email, userId: a.id };
|
|
960
|
-
}
|
|
961
|
-
function Y(a, t) {
|
|
962
|
-
return a === t ? !0 : !a || !t ? !1 : a.email === t.email && a.userId === t.userId && a.anonymousId === t.anonymousId;
|
|
963
|
-
}
|
|
964
|
-
function U(a, t) {
|
|
965
|
-
return {
|
|
966
|
-
type: "modal",
|
|
967
|
-
blocks: [
|
|
968
|
-
{ type: "heading", text: a.name || "Upgrade", level: 1 },
|
|
969
|
-
{ type: "price_grid", priceIds: t.map((e) => e.id) },
|
|
970
|
-
{ type: "cta_button", label: "Continue", action: "checkout" }
|
|
971
|
-
]
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
function L(a) {
|
|
975
|
-
const t = a.locales;
|
|
976
|
-
if (!t) return null;
|
|
977
|
-
const e = [];
|
|
978
|
-
if (typeof navigator < "u") {
|
|
979
|
-
navigator.language && e.push(navigator.language);
|
|
980
|
-
const i = navigator.language?.split("-")[0];
|
|
981
|
-
i && i !== navigator.language && e.push(i);
|
|
982
|
-
}
|
|
983
|
-
const s = a.settings.locale_default;
|
|
984
|
-
s && e.push(s);
|
|
985
|
-
for (const i of e)
|
|
986
|
-
if (i && Object.prototype.hasOwnProperty.call(t, i)) return i;
|
|
987
|
-
return null;
|
|
988
|
-
}
|
|
989
|
-
function g(a) {
|
|
990
|
-
const t = L(a);
|
|
991
|
-
if (!t) return;
|
|
992
|
-
const e = a.locales?.[t];
|
|
993
|
-
e && (e.layout && (a.layout = e.layout), e.prices && (a.prices = a.prices.map((s) => {
|
|
994
|
-
const i = e.prices?.[s.id];
|
|
995
|
-
if (!i) return s;
|
|
996
|
-
const n = { ...s };
|
|
997
|
-
return "label" in i && (n.label = i.label ?? null), "description" in i && (n.description = i.description ?? null), n;
|
|
998
|
-
})));
|
|
999
|
-
}
|
|
1000
|
-
function P(a) {
|
|
1001
|
-
const t = new Uint8Array(a), e = typeof globalThis < "u" ? globalThis.crypto : void 0;
|
|
1002
|
-
if (e && typeof e.getRandomValues == "function")
|
|
1003
|
-
e.getRandomValues(t);
|
|
1004
|
-
else
|
|
1005
|
-
for (let s = 0; s < a; s++) t[s] = Math.floor(Math.random() * 256);
|
|
1006
|
-
return t;
|
|
1007
|
-
}
|
|
1008
|
-
function v(a) {
|
|
1009
|
-
let t = "";
|
|
1010
|
-
for (let e = 0; e < a.length; e++) t += String.fromCharCode(a[e]);
|
|
1011
|
-
return btoa(t).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1012
|
-
}
|
|
1013
|
-
function Z() {
|
|
1014
|
-
return v(P(64));
|
|
1015
|
-
}
|
|
1016
|
-
async function tt(a) {
|
|
1017
|
-
const t = new TextEncoder().encode(a), e = globalThis.crypto;
|
|
1018
|
-
if (!e?.subtle?.digest)
|
|
1019
|
-
throw new Error("crypto.subtle is required for PKCE");
|
|
1020
|
-
const s = await e.subtle.digest("SHA-256", t);
|
|
1021
|
-
return v(new Uint8Array(s));
|
|
1022
|
-
}
|
|
1023
|
-
function et() {
|
|
1024
|
-
return v(P(16));
|
|
1025
|
-
}
|
|
1026
|
-
const st = "https://appbox.space", it = 6e4, at = 600 * 1e3;
|
|
1027
|
-
class dt {
|
|
1028
|
-
constructor(t) {
|
|
1029
|
-
if (this.session = null, this.inflightRefresh = null, this.inflightAnonSignin = null, this.listeners = /* @__PURE__ */ new Set(), this.storageUnwatch = null, this.destroyed = !1, this.oauthFlows = /* @__PURE__ */ new Map(), !t.paywallId)
|
|
1030
|
-
throw new r("invalid_config", "paywallId is required");
|
|
1031
|
-
this.paywallId = t.paywallId, this.apiOrigin = t.apiOrigin ?? st, this.storage = C(t.storage), this.api = new O({
|
|
1032
|
-
apiOrigin: this.apiOrigin,
|
|
1033
|
-
paywallId: t.paywallId,
|
|
1034
|
-
fetch: t.fetch
|
|
1035
|
-
}), this.openPopup = t.openPopup ?? ((e, s) => typeof window > "u" ? null : window.open(e, s, "width=480,height=640,popup=yes")), this.hydrated = this.hydrate(), this.startStorageWatch();
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Подписывается на изменения session-ключа в storage из других контекстов:
|
|
1039
|
-
* - Chrome Extension: `chrome.storage.onChanged` шарится popup ↔ background ↔
|
|
1040
|
-
* options ↔ content script. Логин в одном контексте → остальные сразу
|
|
1041
|
-
* эмитят onAuthChange и в getAccessToken отдают свежий Bearer.
|
|
1042
|
-
* - Web: `window.storage` event фаерится в ДРУГИХ вкладках того же origin'а
|
|
1043
|
-
* (своя вкладка свой setItem не получает — петель нет).
|
|
1044
|
-
*
|
|
1045
|
-
* Loop-guard: сравниваем content по полям session перед applySession, чтобы
|
|
1046
|
-
* не фрить лишних onAuthChange при идентичной перезаписи. Вызовы из других
|
|
1047
|
-
* контекстов с тем же содержимым (пересохранение) — no-op.
|
|
1048
|
-
*/
|
|
1049
|
-
startStorageWatch() {
|
|
1050
|
-
typeof this.storage.watch == "function" && (this.storageUnwatch = this.storage.watch(this.storageKey(), (t) => {
|
|
1051
|
-
this.applyExternalSession(t);
|
|
1052
|
-
}));
|
|
1053
|
-
}
|
|
1054
|
-
async applyExternalSession(t) {
|
|
1055
|
-
if (!this.destroyed && (await this.hydrated, !this.destroyed)) {
|
|
1056
|
-
if (t == null) {
|
|
1057
|
-
this.session && this.setSession(null, { skipPersist: !0 });
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
try {
|
|
1061
|
-
const e = JSON.parse(t);
|
|
1062
|
-
if (!e || typeof e.access_token != "string" || typeof e.refresh_token != "string" || typeof e.expires_at != "number" || !e.user)
|
|
1063
|
-
return;
|
|
1064
|
-
this.setSession(e, { skipPersist: !0 });
|
|
1065
|
-
} catch {
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* Promise гидратации session из storage. До его resolve getCachedSession()
|
|
1071
|
-
* может ещё вернуть null. getAccessToken/refresh/signOut/sign* awaitят его
|
|
1072
|
-
* сами, наружу выставляем для UI'я, чтобы он мог дождаться initial state
|
|
1073
|
-
* прежде чем рисовать «logged-out» вспышку.
|
|
1074
|
-
*/
|
|
1075
|
-
ready() {
|
|
1076
|
-
return this.hydrated;
|
|
1077
|
-
}
|
|
1078
|
-
/** Sync snapshot без сетевых запросов. null = разлогинен или ещё не гидрировались. */
|
|
1079
|
-
getCachedSession() {
|
|
1080
|
-
return this.session;
|
|
1081
|
-
}
|
|
1082
|
-
getCachedUser() {
|
|
1083
|
-
return this.session?.user ?? null;
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* access_token для Authorization-хедера. Если до expiry < REFRESH_LEEWAY_MS,
|
|
1087
|
-
* делает lazy refresh. null = разлогинен или refresh упал на 401 (refresh
|
|
1088
|
-
* token revoked) — вызывающему стоит редиректить на логин.
|
|
1089
|
-
*
|
|
1090
|
-
* Сетевые/5xx ошибки refresh бросаются — текущий access ещё валиден,
|
|
1091
|
-
* вызывающий может попробовать запрос с ним; следующий getAccessToken
|
|
1092
|
-
* попробует refresh снова.
|
|
1093
|
-
*/
|
|
1094
|
-
async getAccessToken() {
|
|
1095
|
-
if (await this.hydrated, !this.session && (await this.rehydrateFromStorage(), !this.session))
|
|
1096
|
-
return null;
|
|
1097
|
-
if (this.isFresh(this.session)) return this.session.access_token;
|
|
1098
|
-
try {
|
|
1099
|
-
return (await this.refresh())?.access_token ?? null;
|
|
1100
|
-
} catch {
|
|
1101
|
-
return this.session?.access_token ?? null;
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
async signInWithEmail(t) {
|
|
1105
|
-
await this.hydrated;
|
|
1106
|
-
const e = await this.readVisitorId(), s = {};
|
|
1107
|
-
t.idempotencyKey && (s["Idempotency-Key"] = t.idempotencyKey);
|
|
1108
|
-
const i = await this.api.request(
|
|
1109
|
-
`/api/v1/paywall/${this.paywallId}/auth/email/signin`,
|
|
1110
|
-
{
|
|
1111
|
-
method: "POST",
|
|
1112
|
-
headers: Object.keys(s).length ? s : void 0,
|
|
1113
|
-
body: JSON.stringify({
|
|
1114
|
-
email: t.email,
|
|
1115
|
-
password: t.password,
|
|
1116
|
-
visitor_id: e,
|
|
1117
|
-
user_meta: t.userMeta
|
|
1118
|
-
})
|
|
1119
|
-
}
|
|
1120
|
-
), n = this.toSession(i, i.user);
|
|
1121
|
-
return this.setSession(n), n;
|
|
1122
|
-
}
|
|
1123
|
-
/**
|
|
1124
|
-
* Signup. Если в Supabase включён email confirm — сервер возвращает
|
|
1125
|
-
* `{status: 'confirmation_required', user}` и НЕ выдаёт токены. В этом
|
|
1126
|
-
* случае setSession не зовётся, юзер должен пройти OTP/magic-link
|
|
1127
|
-
* (отдельная фича следующего PR).
|
|
1128
|
-
*/
|
|
1129
|
-
async signUp(t) {
|
|
1130
|
-
await this.hydrated;
|
|
1131
|
-
const e = await this.readVisitorId(), s = {};
|
|
1132
|
-
t.idempotencyKey && (s["Idempotency-Key"] = t.idempotencyKey);
|
|
1133
|
-
const i = await this.api.request(
|
|
1134
|
-
`/api/v1/paywall/${this.paywallId}/auth/email/signup`,
|
|
1135
|
-
{
|
|
1136
|
-
method: "POST",
|
|
1137
|
-
headers: Object.keys(s).length ? s : void 0,
|
|
1138
|
-
body: JSON.stringify({
|
|
1139
|
-
email: t.email,
|
|
1140
|
-
password: t.password,
|
|
1141
|
-
visitor_id: e,
|
|
1142
|
-
user_meta: t.userMeta
|
|
1143
|
-
})
|
|
1144
|
-
}
|
|
1145
|
-
);
|
|
1146
|
-
if (i.status === "confirmation_required")
|
|
1147
|
-
return { kind: "confirmation_required", user: i.user };
|
|
1148
|
-
const n = this.toSession(i, i.user);
|
|
1149
|
-
return this.setSession(n), { kind: "signed_in", session: n };
|
|
1150
|
-
}
|
|
1151
|
-
/**
|
|
1152
|
-
* Повторная отправка confirmation-email после signUp с включённым
|
|
1153
|
-
* email-confirm. Использует GoTrue `/resend` type='signup'. Бэк всегда
|
|
1154
|
-
* отдаёт ok (anti-enumeration), кроме 429 при rate-limit (~1 раз/мин на
|
|
1155
|
-
* email на стороне Supabase). Host обрабатывает 429 показом «подождите
|
|
1156
|
-
* минуту»; остальное — как success.
|
|
1157
|
-
*/
|
|
1158
|
-
async resendConfirmation(t) {
|
|
1159
|
-
await this.hydrated;
|
|
1160
|
-
const e = {};
|
|
1161
|
-
t.idempotencyKey && (e["Idempotency-Key"] = t.idempotencyKey), await this.api.request(
|
|
1162
|
-
`/api/v1/paywall/${this.paywallId}/auth/email/resend`,
|
|
1163
|
-
{
|
|
1164
|
-
method: "POST",
|
|
1165
|
-
headers: Object.keys(e).length ? e : void 0,
|
|
1166
|
-
body: JSON.stringify({ email: t.email })
|
|
1167
|
-
}
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
/**
|
|
1171
|
-
* Email-OTP / signin без password. Шлёт 6-значный код юзеру на email.
|
|
1172
|
-
* Anti-enumeration: бэк всегда отдаёт ok, поэтому метод не различает
|
|
1173
|
-
* «email не существует» и «отправлено» — следующий шаг (verifyOtp) сам
|
|
1174
|
-
* упадёт invalid_otp если юзера нет. Под капотом GoTrue с create_user=true,
|
|
1175
|
-
* так что новые юзеры через OTP логинятся за один шаг (отправка → ввод
|
|
1176
|
-
* кода → session).
|
|
1177
|
-
*/
|
|
1178
|
-
async sendOtp(t) {
|
|
1179
|
-
await this.hydrated, await this.api.request(
|
|
1180
|
-
`/api/v1/paywall/${this.paywallId}/auth/otp/send`,
|
|
1181
|
-
{
|
|
1182
|
-
method: "POST",
|
|
1183
|
-
body: JSON.stringify({
|
|
1184
|
-
email: t.email,
|
|
1185
|
-
create_user: t.createUser ?? !0,
|
|
1186
|
-
user_meta: t.userMeta
|
|
1187
|
-
})
|
|
1188
|
-
}
|
|
1189
|
-
);
|
|
1190
|
-
}
|
|
1191
|
-
/**
|
|
1192
|
-
* Верификация OTP. type='email' (signin/signup-by-otp) — после успеха
|
|
1193
|
-
* setSession и onAuthChange. type='recovery' — после /requestPasswordReset:
|
|
1194
|
-
* выдаётся короткоживущий access_token для последующего updatePassword.
|
|
1195
|
-
* Мы храним recovery-session так же, как обычную: SDK не различает «можно
|
|
1196
|
-
* залогиниться» vs «можно сменить пароль» — это одна и та же session.
|
|
1197
|
-
*/
|
|
1198
|
-
async verifyOtp(t) {
|
|
1199
|
-
await this.hydrated;
|
|
1200
|
-
const e = await this.readVisitorId(), s = await this.api.request(
|
|
1201
|
-
`/api/v1/paywall/${this.paywallId}/auth/otp/verify`,
|
|
1202
|
-
{
|
|
1203
|
-
method: "POST",
|
|
1204
|
-
body: JSON.stringify({
|
|
1205
|
-
email: t.email,
|
|
1206
|
-
token: t.token,
|
|
1207
|
-
type: t.type ?? "email",
|
|
1208
|
-
visitor_id: e,
|
|
1209
|
-
user_meta: t.userMeta
|
|
1210
|
-
})
|
|
1211
|
-
}
|
|
1212
|
-
), i = this.toSession(s, s.user);
|
|
1213
|
-
return this.setSession(i), i;
|
|
1214
|
-
}
|
|
1215
|
-
/**
|
|
1216
|
-
* Запрос recovery email. Бэк всегда ok, чтобы не палить enumeration.
|
|
1217
|
-
* Юзер вводит код из письма в SDK-ui → verifyOtp({type:'recovery'}) →
|
|
1218
|
-
* получает session → updatePassword.
|
|
1219
|
-
*/
|
|
1220
|
-
async requestPasswordReset(t) {
|
|
1221
|
-
await this.hydrated, await this.api.request(
|
|
1222
|
-
`/api/v1/paywall/${this.paywallId}/auth/password/request-reset`,
|
|
1223
|
-
{
|
|
1224
|
-
method: "POST",
|
|
1225
|
-
body: JSON.stringify({ email: t.email })
|
|
1226
|
-
}
|
|
1227
|
-
);
|
|
1228
|
-
}
|
|
1229
|
-
/**
|
|
1230
|
-
* Меняет пароль текущей session. Работает после verifyOtp({type:'recovery'})
|
|
1231
|
-
* (recovery-session) и после обычного логина — оба случая дают валидный
|
|
1232
|
-
* access_token. Если session нет — бросаем PaywallError('not_authenticated')
|
|
1233
|
-
* до сетевого запроса, чтобы UI не дёргал бэк впустую.
|
|
1234
|
-
*/
|
|
1235
|
-
async updatePassword(t) {
|
|
1236
|
-
await this.hydrated;
|
|
1237
|
-
const e = await this.getAccessToken();
|
|
1238
|
-
if (!e)
|
|
1239
|
-
throw new r("not_authenticated", "no active session");
|
|
1240
|
-
await this.api.request(
|
|
1241
|
-
`/api/v1/paywall/${this.paywallId}/auth/password/update`,
|
|
1242
|
-
{
|
|
1243
|
-
method: "POST",
|
|
1244
|
-
headers: { Authorization: `Bearer ${e}` },
|
|
1245
|
-
body: JSON.stringify({ password: t.password })
|
|
1246
|
-
}
|
|
1247
|
-
);
|
|
1248
|
-
}
|
|
1249
|
-
/**
|
|
1250
|
-
* Анонимный signin (Supabase user без email). Лестница попыток:
|
|
1251
|
-
*
|
|
1252
|
-
* 1. Если уже залогинены анонимно (session.user.is_anonymous === true) —
|
|
1253
|
-
* no-op, возвращаем текущую session. Идемпотентно для UI'я, который
|
|
1254
|
-
* может звать signInAnonymously() в render-loop'е, не отслеживая state.
|
|
1255
|
-
*
|
|
1256
|
-
* 2. Resume через сохранённый anon refresh_token (`STORAGE_KEYS.anonRefreshToken`).
|
|
1257
|
-
* Если токен есть — пробуем `/auth/refresh` им. Success → setSession,
|
|
1258
|
-
* возвращаем юзера ТОГО ЖЕ id что был при предыдущем anon signin'е
|
|
1259
|
-
* (обещание из user-фидбека: «если разлогинился из анонимного —
|
|
1260
|
-
* логинить в этот же акк»).
|
|
1261
|
-
*
|
|
1262
|
-
* 3. Иначе → POST /auth/anonymous/signin → setSession + сохраняем
|
|
1263
|
-
* refresh_token в anonRefreshToken.
|
|
1264
|
-
*
|
|
1265
|
-
* `captchaToken` сейчас не требуется — captcha protection в Supabase
|
|
1266
|
-
* отключена, защита от per-IP abuse держится на rate-limit'е Supabase'а
|
|
1267
|
-
* (30/час per real-IP, см. IP forwarding setup в supabaseAuthRest.ts) +
|
|
1268
|
-
* CF Bot Fight Mode на edge. Поле оставлено optional для forward-compat:
|
|
1269
|
-
* когда сервер начнёт возвращать challenge_required в риск-сценариях,
|
|
1270
|
-
* SDK сможет передать proof-of-something обратно без breaking change.
|
|
1271
|
-
*
|
|
1272
|
-
* `forceCaptcha: true` пропускает шаги 1-2 и сразу делает /signin (создаёт
|
|
1273
|
-
* нового anon-юзера). Используется в switch-account flow. Имя поля исторически
|
|
1274
|
-
* остаётся `forceCaptcha`, хотя капчи там больше нет — менять имя ломает
|
|
1275
|
-
* host-сигнатуру; смысл «принудительно новая anon-сессия» сохранён.
|
|
1276
|
-
*
|
|
1277
|
-
* Параллельные вызовы дедуплицируются через `inflightAnonSignin` — два
|
|
1278
|
-
* click'а на «Войти как гость» не создадут двух anon-юзеров (два /signup =
|
|
1279
|
-
* два user_id, второй trial-баланс улетает в нирвану).
|
|
1280
|
-
*/
|
|
1281
|
-
async signInAnonymously(t = {}) {
|
|
1282
|
-
if (this.inflightAnonSignin) return this.inflightAnonSignin;
|
|
1283
|
-
this.inflightAnonSignin = (async () => {
|
|
1284
|
-
if (await this.hydrated, !t.forceCaptcha && this.session?.user.is_anonymous === !0)
|
|
1285
|
-
return this.session;
|
|
1286
|
-
if (!t.forceCaptcha) {
|
|
1287
|
-
const o = await this.resumeAnonymous();
|
|
1288
|
-
if (o) return o;
|
|
1289
|
-
}
|
|
1290
|
-
const e = await this.readVisitorId(), s = await this.api.request(
|
|
1291
|
-
`/api/v1/paywall/${this.paywallId}/auth/anonymous/signin`,
|
|
1292
|
-
{
|
|
1293
|
-
method: "POST",
|
|
1294
|
-
body: JSON.stringify({
|
|
1295
|
-
...t.captchaToken ? { captcha_token: t.captchaToken } : {},
|
|
1296
|
-
visitor_id: e,
|
|
1297
|
-
user_meta: t.userMeta
|
|
1298
|
-
})
|
|
1299
|
-
}
|
|
1300
|
-
), i = {
|
|
1301
|
-
...s.user,
|
|
1302
|
-
email: s.user.email ?? null,
|
|
1303
|
-
is_anonymous: !0
|
|
1304
|
-
}, n = this.toSession(s, i);
|
|
1305
|
-
return this.setSession(n), await this.writeAnonRefreshToken(n.refresh_token), n;
|
|
1306
|
-
})();
|
|
1307
|
-
try {
|
|
1308
|
-
return await this.inflightAnonSignin;
|
|
1309
|
-
} finally {
|
|
1310
|
-
this.inflightAnonSignin = null;
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
/**
|
|
1314
|
-
* Внутренний resume — пробует /auth/refresh с сохранённым anon refresh_token.
|
|
1315
|
-
* Возвращает session при успехе, null если токена нет или он отозван (401).
|
|
1316
|
-
* Сетевые ошибки бросает наружу — caller сам решает, ретраить или просить
|
|
1317
|
-
* пользователя пройти капчу.
|
|
1318
|
-
*/
|
|
1319
|
-
async resumeAnonymous() {
|
|
1320
|
-
const t = await this.readAnonRefreshToken();
|
|
1321
|
-
if (!t) return null;
|
|
1322
|
-
try {
|
|
1323
|
-
const e = await this.api.request(
|
|
1324
|
-
`/api/v1/paywall/${this.paywallId}/auth/refresh`,
|
|
1325
|
-
{ method: "POST", body: JSON.stringify({ refresh_token: t }) }
|
|
1326
|
-
), s = this.session?.user.is_anonymous === !0 ? this.session.user : { id: "", email: null, is_anonymous: !0 }, i = this.toSession(e, s);
|
|
1327
|
-
return this.setSession(i), await this.writeAnonRefreshToken(i.refresh_token), i;
|
|
1328
|
-
} catch (e) {
|
|
1329
|
-
if (e instanceof r && e.status === 401)
|
|
1330
|
-
return await this.clearAnonRefreshToken(), null;
|
|
1331
|
-
throw e;
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* Анон → email/password upgrade. Сохраняет тот же auth.user.id, балансы
|
|
1336
|
-
* и trial-quotas остаются. Поведение зависит от Supabase email-confirm
|
|
1337
|
-
* настройки проекта:
|
|
1338
|
-
*
|
|
1339
|
-
* - Confirmation OFF → backend сразу обновляет email + password в auth.users.
|
|
1340
|
-
* Возвращаем `kind: 'updated'`, локально патчим session.user.email +
|
|
1341
|
-
* is_anonymous=false (текущий access_token остаётся валидным, перевыдавать
|
|
1342
|
-
* не нужно — GoTrue не вращает токены на updateUser).
|
|
1343
|
-
*
|
|
1344
|
-
* - Confirmation ON → backend отдаёт `confirmation_required`. Текущая
|
|
1345
|
-
* session ОСТАЁТСЯ анонимной до клика юзером по confirmation-ссылке.
|
|
1346
|
-
* Password применяется сразу (можно дальше логиниться по нему даже до
|
|
1347
|
-
* confirm'а). После клика — следующий /auth/refresh подтянет обновлённый
|
|
1348
|
-
* is_anonymous=false из JWT (refresh не возвращает user, так что
|
|
1349
|
-
* UI может явно подёргать `auth.refresh()` через минуту-другую, либо
|
|
1350
|
-
* дождаться lazy-refresh при истечении access).
|
|
1351
|
-
*
|
|
1352
|
-
* Без активной session бросает `not_authenticated`. Дедупликации нет —
|
|
1353
|
-
* двойной submit формы UI должен предотвратить idempotencyKey'ом.
|
|
1354
|
-
*/
|
|
1355
|
-
async upgradeAnonymousToEmail(t) {
|
|
1356
|
-
await this.hydrated;
|
|
1357
|
-
const e = await this.getAccessToken();
|
|
1358
|
-
if (!e)
|
|
1359
|
-
throw new r("not_authenticated", "no active session");
|
|
1360
|
-
const s = {
|
|
1361
|
-
Authorization: `Bearer ${e}`
|
|
1362
|
-
};
|
|
1363
|
-
t.idempotencyKey && (s["Idempotency-Key"] = t.idempotencyKey);
|
|
1364
|
-
const i = await this.api.request(
|
|
1365
|
-
`/api/v1/paywall/${this.paywallId}/auth/anonymous/upgrade`,
|
|
1366
|
-
{
|
|
1367
|
-
method: "POST",
|
|
1368
|
-
headers: s,
|
|
1369
|
-
body: JSON.stringify({
|
|
1370
|
-
email: t.email,
|
|
1371
|
-
password: t.password,
|
|
1372
|
-
user_meta: t.userMeta
|
|
1373
|
-
})
|
|
1374
|
-
}
|
|
1375
|
-
);
|
|
1376
|
-
if (i.status === "confirmation_required")
|
|
1377
|
-
return { kind: "confirmation_required", email: i.email };
|
|
1378
|
-
const n = this.session;
|
|
1379
|
-
if (!n)
|
|
1380
|
-
throw new r(
|
|
1381
|
-
"not_authenticated",
|
|
1382
|
-
"session disappeared during upgrade"
|
|
1383
|
-
);
|
|
1384
|
-
const o = {
|
|
1385
|
-
...n.user,
|
|
1386
|
-
id: i.user.id,
|
|
1387
|
-
email: i.user.email,
|
|
1388
|
-
is_anonymous: i.user.is_anonymous ?? !1
|
|
1389
|
-
}, u = { ...n, user: o };
|
|
1390
|
-
return this.setSession(u), await this.clearAnonRefreshToken(), { kind: "updated", session: u };
|
|
1391
|
-
}
|
|
1392
|
-
/**
|
|
1393
|
-
* OAuth signin через popup с PKCE. Жизненный цикл:
|
|
1394
|
-
* 1. Генерим verifier+challenge+state локально (verifier не уходит на бэк
|
|
1395
|
-
* до /exchange — это защита от перехвата code'а).
|
|
1396
|
-
* 2. POST /oauth/init с challenge → бэк отдаёт authorize_url.
|
|
1397
|
-
* 3. Открываем popup, ждём postMessage с типом 'pw-oauth' и нашим state.
|
|
1398
|
-
* 4. POST /oauth/exchange с {auth_code, code_verifier} → session.
|
|
1399
|
-
*
|
|
1400
|
-
* Таймаут — 5 минут от открытия popup'а. Если юзер закрыл popup до конца
|
|
1401
|
-
* флоу (window.closed → true) — бросаем PaywallError('oauth_cancelled').
|
|
1402
|
-
* Параллельные вызовы НЕ дедупятся — каждый открывает свой popup; вызывать
|
|
1403
|
-
* параллельно не имеет смысла, но защищаться от этого код не должен.
|
|
1404
|
-
*
|
|
1405
|
-
* onPopupOpened вызывается сразу после успешного window.open (до ожидания
|
|
1406
|
-
* code'а). UI использует это, чтобы сбросить loading-state кнопки: дальше
|
|
1407
|
-
* ответственность за флоу у popup'а, основная страница не должна висеть.
|
|
1408
|
-
* Если popup'ом не вернулся code (юзер закрыл вкладку, closed-detection
|
|
1409
|
-
* не сработал из-за COOP-severance) — promise дойдёт до oauth_timeout
|
|
1410
|
-
* через 5 минут, но кнопка к этому моменту уже свободна.
|
|
1411
|
-
*/
|
|
1412
|
-
async signInWithOAuth(t) {
|
|
1413
|
-
if (typeof window > "u")
|
|
1414
|
-
throw new r("oauth_unavailable", "window is required for OAuth");
|
|
1415
|
-
const { authorize_url: e, state: s } = await this.startOAuthFlow({
|
|
1416
|
-
provider: t.provider,
|
|
1417
|
-
scopes: t.scopes,
|
|
1418
|
-
userMeta: t.userMeta
|
|
1419
|
-
}), i = this.openPopup(e, `pw-oauth-${s}`);
|
|
1420
|
-
if (!i)
|
|
1421
|
-
throw this.oauthFlows.delete(s), new r(
|
|
1422
|
-
"popup_blocked",
|
|
1423
|
-
"browser blocked auth popup — call from a user gesture"
|
|
1424
|
-
);
|
|
1425
|
-
t.onPopupOpened?.();
|
|
1426
|
-
const n = await ot(i, s);
|
|
1427
|
-
if (this.destroyed)
|
|
1428
|
-
throw this.oauthFlows.delete(s), new r("aborted", "AuthClient destroyed mid-flow");
|
|
1429
|
-
return this.completeOAuthFlow({ state: s, code: n });
|
|
1430
|
-
}
|
|
1431
|
-
/**
|
|
1432
|
-
* Шаг 1 OAuth split-API: инициирует flow на бэке, генерит PKCE verifier
|
|
1433
|
-
* + state, сохраняет их у себя, возвращает `{authorize_url, state}` для
|
|
1434
|
-
* открытия popup'а. Верификатор НЕ выходит наружу — его держит AuthClient
|
|
1435
|
-
* до `completeOAuthFlow`.
|
|
1436
|
-
*
|
|
1437
|
-
* Используется в offscreen-архитектуре (@monetize/sdk-extension): start
|
|
1438
|
-
* вызывается через RPC из content-script'а, content открывает popup
|
|
1439
|
-
* нативно (gesture preserved), затем зовёт completeOAuthFlow с code'ом.
|
|
1440
|
-
* AuthClient (в offscreen'е) делает /exchange с сохранённым verifier'ом.
|
|
1441
|
-
*
|
|
1442
|
-
* Pending flows GC'атся через 10мин — больше чем юзеру нужно прокликать
|
|
1443
|
-
* Google. Без cleanup'а Map бы рос на каждый закрытый popup.
|
|
1444
|
-
*/
|
|
1445
|
-
async startOAuthFlow(t) {
|
|
1446
|
-
await this.hydrated, this.gcOAuthFlows();
|
|
1447
|
-
const e = Z(), s = await tt(e), i = et(), n = {}, o = await this.getAccessToken().catch(() => null);
|
|
1448
|
-
o && (n.Authorization = `Bearer ${o}`);
|
|
1449
|
-
const { authorize_url: u } = await this.api.request(
|
|
1450
|
-
`/api/v1/paywall/${this.paywallId}/auth/oauth/init`,
|
|
1451
|
-
{
|
|
1452
|
-
method: "POST",
|
|
1453
|
-
headers: Object.keys(n).length ? n : void 0,
|
|
1454
|
-
body: JSON.stringify({
|
|
1455
|
-
provider: t.provider,
|
|
1456
|
-
code_challenge: s,
|
|
1457
|
-
code_challenge_method: "s256",
|
|
1458
|
-
scopes: t.scopes
|
|
1459
|
-
})
|
|
1460
|
-
}
|
|
1461
|
-
);
|
|
1462
|
-
return this.oauthFlows.set(i, {
|
|
1463
|
-
verifier: e,
|
|
1464
|
-
userMeta: t.userMeta,
|
|
1465
|
-
startedAt: Date.now()
|
|
1466
|
-
}), { authorize_url: u, state: i };
|
|
1467
|
-
}
|
|
1468
|
-
/**
|
|
1469
|
-
* Шаг 2 OAuth split-API: обменивает code (полученный из popup) на session,
|
|
1470
|
-
* используя verifier, сохранённый при startOAuthFlow. После успеха — set
|
|
1471
|
-
* session и эмит onAuthChange.
|
|
1472
|
-
*
|
|
1473
|
-
* Если flow не найден (state не из startOAuthFlow или GC'нулся за TTL'ом) —
|
|
1474
|
-
* бросает `oauth_invalid_state`. Caller должен начать заново через
|
|
1475
|
-
* startOAuthFlow.
|
|
1476
|
-
*/
|
|
1477
|
-
async completeOAuthFlow(t) {
|
|
1478
|
-
await this.hydrated;
|
|
1479
|
-
const e = this.oauthFlows.get(t.state);
|
|
1480
|
-
if (!e)
|
|
1481
|
-
throw new r(
|
|
1482
|
-
"oauth_invalid_state",
|
|
1483
|
-
"OAuth flow not found — start with startOAuthFlow first or check TTL"
|
|
1484
|
-
);
|
|
1485
|
-
this.oauthFlows.delete(t.state);
|
|
1486
|
-
const s = await this.readVisitorId(), i = await this.api.request(
|
|
1487
|
-
`/api/v1/paywall/${this.paywallId}/auth/oauth/exchange`,
|
|
1488
|
-
{
|
|
1489
|
-
method: "POST",
|
|
1490
|
-
body: JSON.stringify({
|
|
1491
|
-
auth_code: t.code,
|
|
1492
|
-
code_verifier: e.verifier,
|
|
1493
|
-
visitor_id: s,
|
|
1494
|
-
user_meta: e.userMeta
|
|
1495
|
-
})
|
|
1496
|
-
}
|
|
1497
|
-
);
|
|
1498
|
-
if (this.destroyed)
|
|
1499
|
-
throw new r("aborted", "AuthClient destroyed mid-flow");
|
|
1500
|
-
const n = this.toSession(i, i.user);
|
|
1501
|
-
return this.setSession(n), n;
|
|
1502
|
-
}
|
|
1503
|
-
gcOAuthFlows() {
|
|
1504
|
-
const t = Date.now() - at;
|
|
1505
|
-
for (const [e, s] of this.oauthFlows)
|
|
1506
|
-
s.startedAt < t && this.oauthFlows.delete(e);
|
|
1507
|
-
}
|
|
1508
|
-
/**
|
|
1509
|
-
* Refresh access/refresh пары через текущий refresh_token. Дедуплицирует
|
|
1510
|
-
* параллельные вызовы (один in-flight promise на весь клиент).
|
|
1511
|
-
*
|
|
1512
|
-
* - 401 → refresh_token отозван/невалиден → чистим session, эмитим logout.
|
|
1513
|
-
* - Сеть/5xx → пробрасываем ошибку, session оставляем — юзер не должен
|
|
1514
|
-
* разлогиниваться из-за временной сетевой проблемы.
|
|
1515
|
-
* - Нет session → возвращаем null без сетевого запроса.
|
|
1516
|
-
*/
|
|
1517
|
-
async refresh() {
|
|
1518
|
-
if (await this.hydrated, !this.session) return null;
|
|
1519
|
-
if (this.inflightRefresh) return this.inflightRefresh;
|
|
1520
|
-
const t = this.session.refresh_token, e = this.session.user;
|
|
1521
|
-
return this.inflightRefresh = (async () => {
|
|
1522
|
-
try {
|
|
1523
|
-
const s = await this.api.request(
|
|
1524
|
-
`/api/v1/paywall/${this.paywallId}/auth/refresh`,
|
|
1525
|
-
{
|
|
1526
|
-
method: "POST",
|
|
1527
|
-
body: JSON.stringify({ refresh_token: t })
|
|
1528
|
-
}
|
|
1529
|
-
), i = this.toSession(s, e);
|
|
1530
|
-
return this.setSession(i), e.is_anonymous === !0 && await this.writeAnonRefreshToken(i.refresh_token), i;
|
|
1531
|
-
} catch (s) {
|
|
1532
|
-
if (s instanceof r && s.status === 401)
|
|
1533
|
-
return e.is_anonymous === !0 && await this.clearAnonRefreshToken(), this.setSession(null), null;
|
|
1534
|
-
throw s;
|
|
1535
|
-
} finally {
|
|
1536
|
-
this.inflightRefresh = null;
|
|
1537
|
-
}
|
|
1538
|
-
})(), this.inflightRefresh;
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* Глобальный logout — инвалидирует ВСЕ refresh-токены юзера на всех
|
|
1542
|
-
* устройствах/контекстах через GoTrue `/logout?scope=global`. Используется
|
|
1543
|
-
* для compromise-account флоу («подозрительная активность, разлогинить
|
|
1544
|
-
* везде»).
|
|
1545
|
-
*
|
|
1546
|
-
* Local-side: чистим текущую session, остальные контексты (другие вкладки
|
|
1547
|
-
* / extension popup и background) подхватят logout через storage-watch
|
|
1548
|
-
* автоматически. Active access-токены в других контекстах останутся валидны
|
|
1549
|
-
* до их естественного истечения (1 час max), но refresh уже не сработает —
|
|
1550
|
-
* после первого `getAccessToken()` каждый контекст разлогинится сам.
|
|
1551
|
-
*
|
|
1552
|
-
* Безопасность: бэк не принимает целевой user_id — резолвит юзера из
|
|
1553
|
-
* Bearer, нельзя разлогинить чужой аккаунт.
|
|
1554
|
-
*/
|
|
1555
|
-
async revokeAllSessions() {
|
|
1556
|
-
await this.hydrated;
|
|
1557
|
-
const t = this.session?.access_token;
|
|
1558
|
-
if (!t)
|
|
1559
|
-
throw new r("not_authenticated", "no active session");
|
|
1560
|
-
await this.api.request(
|
|
1561
|
-
`/api/v1/paywall/${this.paywallId}/auth/revoke-all`,
|
|
1562
|
-
{
|
|
1563
|
-
method: "POST",
|
|
1564
|
-
headers: { Authorization: `Bearer ${t}` }
|
|
1565
|
-
}
|
|
1566
|
-
), this.setSession(null);
|
|
1567
|
-
}
|
|
1568
|
-
/**
|
|
1569
|
-
* Signout: чистит локальную session СРАЗУ (UX — мгновенный logout без
|
|
1570
|
-
* ожидания сети), потом best-effort POST /auth/signout с текущим access.
|
|
1571
|
-
* Ошибка сети/5xx тут уже не критична — на бэке токен и так истечёт.
|
|
1572
|
-
*
|
|
1573
|
-
* Anon-aware: по умолчанию anonRefreshToken сохраняется. Это позволяет
|
|
1574
|
-
* после signOut() позвать signInAnonymously() и попасть в ТОТ ЖЕ
|
|
1575
|
-
* анон-аккаунт без капчи (см. resumeAnonymous). Поведение предсказуемое
|
|
1576
|
-
* для UX'а «гость → залогинился → разлогинился → снова гость с теми же
|
|
1577
|
-
* балансами».
|
|
1578
|
-
*
|
|
1579
|
-
* `forgetAnonymous: true` — полное забытие, вместе с anonRefreshToken.
|
|
1580
|
-
* Нужно для сценариев типа «свич аккаунта на устройстве» или жалоб на
|
|
1581
|
-
* приватность («очисти все мои следы»).
|
|
1582
|
-
*/
|
|
1583
|
-
async signOut(t = {}) {
|
|
1584
|
-
await this.hydrated;
|
|
1585
|
-
const e = this.session?.access_token, s = this.session?.user.is_anonymous === !0;
|
|
1586
|
-
if (this.setSession(null), t.forgetAnonymous && await this.clearAnonRefreshToken(), !!e && !(s && !t.forgetAnonymous))
|
|
1587
|
-
try {
|
|
1588
|
-
await this.api.request(
|
|
1589
|
-
`/api/v1/paywall/${this.paywallId}/auth/signout`,
|
|
1590
|
-
{
|
|
1591
|
-
method: "POST",
|
|
1592
|
-
headers: { Authorization: `Bearer ${e}` }
|
|
1593
|
-
}
|
|
1594
|
-
);
|
|
1595
|
-
} catch {
|
|
1596
|
-
}
|
|
1597
|
-
}
|
|
1598
|
-
/**
|
|
1599
|
-
* Подписка на изменения session: signin/signup/refresh/signOut/expired-401.
|
|
1600
|
-
* Колбек вызывается с текущим snapshot через microtask (если session есть)
|
|
1601
|
-
* + на каждое реальное изменение. Возвращает unsubscribe.
|
|
1602
|
-
*/
|
|
1603
|
-
onAuthChange(t) {
|
|
1604
|
-
if (this.listeners.add(t), this.session) {
|
|
1605
|
-
const e = this.session;
|
|
1606
|
-
queueMicrotask(() => {
|
|
1607
|
-
this.listeners.has(t) && t(e);
|
|
1608
|
-
});
|
|
1609
|
-
}
|
|
1610
|
-
return () => {
|
|
1611
|
-
this.listeners.delete(t);
|
|
1612
|
-
};
|
|
1613
|
-
}
|
|
1614
|
-
isFresh(t) {
|
|
1615
|
-
return t.expires_at - Date.now() > it;
|
|
1616
|
-
}
|
|
1617
|
-
toSession(t, e) {
|
|
1618
|
-
const s = t.expires_at != null ? t.expires_at * 1e3 : Date.now() + t.expires_in * 1e3;
|
|
1619
|
-
return {
|
|
1620
|
-
access_token: t.access_token,
|
|
1621
|
-
refresh_token: t.refresh_token,
|
|
1622
|
-
expires_at: s,
|
|
1623
|
-
user: e
|
|
1624
|
-
};
|
|
1625
|
-
}
|
|
1626
|
-
setSession(t, e = {}) {
|
|
1627
|
-
if (this.destroyed) return;
|
|
1628
|
-
const s = this.session;
|
|
1629
|
-
this.session = t, e.skipPersist || this.persist(), ht(s, t) || this.emit();
|
|
1630
|
-
}
|
|
1631
|
-
emit() {
|
|
1632
|
-
for (const t of this.listeners)
|
|
1633
|
-
try {
|
|
1634
|
-
t(this.session);
|
|
1635
|
-
} catch (e) {
|
|
1636
|
-
console.warn("[paywall] onAuthChange listener threw", e);
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
storageKey() {
|
|
1640
|
-
return y.authSession(this.paywallId);
|
|
1641
|
-
}
|
|
1642
|
-
async hydrate() {
|
|
1643
|
-
try {
|
|
1644
|
-
const t = await this.storage.getItem(this.storageKey());
|
|
1645
|
-
if (!t) return;
|
|
1646
|
-
const e = JSON.parse(t);
|
|
1647
|
-
if (!e || typeof e.access_token != "string" || typeof e.refresh_token != "string" || typeof e.expires_at != "number" || !e.user)
|
|
1648
|
-
return;
|
|
1649
|
-
this.session = e, this.emit();
|
|
1650
|
-
} catch {
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
// Используется как race-fallback в getAccessToken: между construction'ом
|
|
1654
|
-
// (когда storage был пуст) и onChanged-доставкой могло произойти signin
|
|
1655
|
-
// в другом контексте. Не дублирует watch — тот про push, этот про pull.
|
|
1656
|
-
async rehydrateFromStorage() {
|
|
1657
|
-
try {
|
|
1658
|
-
const t = await this.storage.getItem(this.storageKey());
|
|
1659
|
-
if (!t) return;
|
|
1660
|
-
const e = JSON.parse(t);
|
|
1661
|
-
if (!e || typeof e.access_token != "string" || typeof e.refresh_token != "string" || typeof e.expires_at != "number" || !e.user)
|
|
1662
|
-
return;
|
|
1663
|
-
this.setSession(e, { skipPersist: !0 });
|
|
1664
|
-
} catch {
|
|
1665
|
-
}
|
|
1666
|
-
}
|
|
1667
|
-
/**
|
|
1668
|
-
* Освобождает ресурсы AuthClient'а: отписывает storage-watch, чистит
|
|
1669
|
-
* listener'ы, выставляет destroyed-флаг. После destroy все async-операции
|
|
1670
|
-
* (inflight refresh, OAuth popup, applyExternalSession) early-return'ят
|
|
1671
|
-
* через `isDestroyed()` guard'ы — никаких write-back'ов в storage,
|
|
1672
|
-
* никаких эмитов на пустые listener'ы.
|
|
1673
|
-
*
|
|
1674
|
-
* destroy() идемпотентен: повторный вызов — no-op.
|
|
1675
|
-
*/
|
|
1676
|
-
destroy() {
|
|
1677
|
-
this.destroyed = !0, this.storageUnwatch && (this.storageUnwatch(), this.storageUnwatch = null), this.listeners.clear(), this.inflightRefresh = null;
|
|
1678
|
-
}
|
|
1679
|
-
/** Sync-проверка: был ли вызван destroy(). Полезно для UI / тестов. */
|
|
1680
|
-
isDestroyed() {
|
|
1681
|
-
return this.destroyed;
|
|
1682
|
-
}
|
|
1683
|
-
async persist() {
|
|
1684
|
-
try {
|
|
1685
|
-
this.session ? await this.storage.setItem(
|
|
1686
|
-
this.storageKey(),
|
|
1687
|
-
JSON.stringify(this.session)
|
|
1688
|
-
) : await this.storage.removeItem(this.storageKey());
|
|
1689
|
-
} catch {
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
async readAnonRefreshToken() {
|
|
1693
|
-
try {
|
|
1694
|
-
const t = await this.storage.getItem(y.anonRefreshToken(this.paywallId));
|
|
1695
|
-
return typeof t == "string" && t.length > 0 ? t : null;
|
|
1696
|
-
} catch {
|
|
1697
|
-
return null;
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
async writeAnonRefreshToken(t) {
|
|
1701
|
-
try {
|
|
1702
|
-
await this.storage.setItem(
|
|
1703
|
-
y.anonRefreshToken(this.paywallId),
|
|
1704
|
-
t
|
|
1705
|
-
);
|
|
1706
|
-
} catch {
|
|
1707
|
-
}
|
|
1708
|
-
}
|
|
1709
|
-
async clearAnonRefreshToken() {
|
|
1710
|
-
try {
|
|
1711
|
-
await this.storage.removeItem(
|
|
1712
|
-
y.anonRefreshToken(this.paywallId)
|
|
1713
|
-
);
|
|
1714
|
-
} catch {
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
/**
|
|
1718
|
-
* Читает stable visitor_id из storage если он там уже есть. НЕ генерит:
|
|
1719
|
-
* AuthClient может быть инстанцирован раньше BillingClient, а синтетический
|
|
1720
|
-
* visitor_id без касания пейвола не имеет смысла (нет гостевых покупок,
|
|
1721
|
-
* которые надо бы линковать). undefined → бэк сам пропустит ветку
|
|
1722
|
-
* "merge guest purchases".
|
|
1723
|
-
*/
|
|
1724
|
-
async readVisitorId() {
|
|
1725
|
-
try {
|
|
1726
|
-
const t = await this.storage.getItem(y.visitorId);
|
|
1727
|
-
return typeof t == "string" && t.length >= 16 ? t : void 0;
|
|
1728
|
-
} catch {
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
}
|
|
1732
|
-
}
|
|
1733
|
-
const nt = 5 * 6e4, rt = 500;
|
|
1734
|
-
function ot(a, t) {
|
|
1735
|
-
return new Promise((e, s) => {
|
|
1736
|
-
let i = !1;
|
|
1737
|
-
const n = () => {
|
|
1738
|
-
i = !0, window.removeEventListener("message", o), clearInterval(u), clearTimeout(l);
|
|
1739
|
-
}, o = (d) => {
|
|
1740
|
-
if (i) return;
|
|
1741
|
-
const h = d.data;
|
|
1742
|
-
if (!(!h || h.type !== "pw-oauth") && h.messageId === t) {
|
|
1743
|
-
if (h.status === "success" && h.code) {
|
|
1744
|
-
n();
|
|
1745
|
-
try {
|
|
1746
|
-
a.close();
|
|
1747
|
-
} catch {
|
|
1748
|
-
}
|
|
1749
|
-
e(h.code);
|
|
1750
|
-
} else if (h.status === "error") {
|
|
1751
|
-
n();
|
|
1752
|
-
try {
|
|
1753
|
-
a.close();
|
|
1754
|
-
} catch {
|
|
1755
|
-
}
|
|
1756
|
-
s(
|
|
1757
|
-
new r(
|
|
1758
|
-
"oauth_failed",
|
|
1759
|
-
h.description || h.error || "OAuth provider returned error"
|
|
1760
|
-
)
|
|
1761
|
-
);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
}, u = setInterval(() => {
|
|
1765
|
-
if (i) return;
|
|
1766
|
-
let d;
|
|
1767
|
-
try {
|
|
1768
|
-
d = a.closed;
|
|
1769
|
-
} catch {
|
|
1770
|
-
return;
|
|
1771
|
-
}
|
|
1772
|
-
d && (n(), s(new r("oauth_cancelled", "auth popup was closed")));
|
|
1773
|
-
}, rt), l = setTimeout(() => {
|
|
1774
|
-
if (!i) {
|
|
1775
|
-
n();
|
|
1776
|
-
try {
|
|
1777
|
-
a.close();
|
|
1778
|
-
} catch {
|
|
1779
|
-
}
|
|
1780
|
-
s(new r("oauth_timeout", "OAuth flow timed out"));
|
|
1781
|
-
}
|
|
1782
|
-
}, nt);
|
|
1783
|
-
window.addEventListener("message", o);
|
|
1784
|
-
});
|
|
1785
|
-
}
|
|
1786
|
-
function ht(a, t) {
|
|
1787
|
-
return a === t ? !0 : !a || !t ? !1 : a.access_token === t.access_token && a.refresh_token === t.refresh_token && a.expires_at === t.expires_at && a.user.id === t.user.id && a.user.email === t.user.email;
|
|
1788
|
-
}
|
|
1789
|
-
const ct = 1500, lt = 20, k = 200;
|
|
1790
|
-
class ft {
|
|
1791
|
-
constructor(t) {
|
|
1792
|
-
this.buffer = [], this.flushTimer = null, this.destroyed = !1, this.unloadHandler = null, this.visibilityHandler = null, this.opts = t, this.isEnabled() && this.attachUnloadHandlers();
|
|
1793
|
-
}
|
|
1794
|
-
isEnabled() {
|
|
1795
|
-
return this.opts.enabled !== !1;
|
|
1796
|
-
}
|
|
1797
|
-
track(t, e) {
|
|
1798
|
-
if (this.destroyed || !this.isEnabled() || typeof t != "string" || t.length === 0) return;
|
|
1799
|
-
this.buffer.push({ type: t, ts: Date.now(), props: e });
|
|
1800
|
-
const s = this.opts.maxBufferSize ?? lt;
|
|
1801
|
-
if (this.buffer.length >= s) {
|
|
1802
|
-
this.flush();
|
|
1803
|
-
return;
|
|
1804
|
-
}
|
|
1805
|
-
this.buffer.length > k && (this.buffer = this.buffer.slice(-k)), this.scheduleFlush();
|
|
1806
|
-
}
|
|
1807
|
-
scheduleFlush() {
|
|
1808
|
-
if (this.flushTimer || this.destroyed) return;
|
|
1809
|
-
const t = this.opts.flushIntervalMs ?? ct;
|
|
1810
|
-
this.flushTimer = setTimeout(() => {
|
|
1811
|
-
this.flushTimer = null, this.flush();
|
|
1812
|
-
}, t);
|
|
1813
|
-
}
|
|
1814
|
-
async flush() {
|
|
1815
|
-
if (this.buffer.length === 0) return;
|
|
1816
|
-
this.flushTimer && (clearTimeout(this.flushTimer), this.flushTimer = null);
|
|
1817
|
-
const t = this.buffer;
|
|
1818
|
-
this.buffer = [];
|
|
1819
|
-
try {
|
|
1820
|
-
const e = await this.opts.getVisitorId(), s = this.opts.getUserId?.() ?? null, i = JSON.stringify({ events: t }), n = this.opts.fetch ?? (typeof fetch < "u" ? fetch : void 0);
|
|
1821
|
-
if (!n) return;
|
|
1822
|
-
await n(this.opts.endpoint, {
|
|
1823
|
-
method: "POST",
|
|
1824
|
-
credentials: "omit",
|
|
1825
|
-
keepalive: !0,
|
|
1826
|
-
// если страница закроется в этот момент — браузер всё равно дотянет
|
|
1827
|
-
headers: this.buildHeaders(e, s),
|
|
1828
|
-
body: i
|
|
1829
|
-
});
|
|
1830
|
-
} catch {
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
/**
|
|
1834
|
-
* Отправка через navigator.sendBeacon — для unload/pagehide. Гарантированно
|
|
1835
|
-
* долетает (POST с keepalive тоже почти, но beacon сделан именно под это).
|
|
1836
|
-
* Headers ставить нельзя (спецификация), поэтому SDK metadata едет в body
|
|
1837
|
-
* как fallback-поля, которые сервер читает в дополнение к headers.
|
|
1838
|
-
*/
|
|
1839
|
-
flushBeacon() {
|
|
1840
|
-
if (this.buffer.length === 0) return;
|
|
1841
|
-
const t = this.buffer;
|
|
1842
|
-
this.buffer = [];
|
|
1843
|
-
const e = this.opts.getCachedVisitorId?.() ?? null, s = this.opts.getUserId?.() ?? null;
|
|
1844
|
-
if (!e) {
|
|
1845
|
-
this.buffer.unshift(...t), this.flush();
|
|
1846
|
-
return;
|
|
1847
|
-
}
|
|
1848
|
-
const i = JSON.stringify({
|
|
1849
|
-
events: t,
|
|
1850
|
-
// body-level дубликаты для beacon-flow, читаются сервером как fallback.
|
|
1851
|
-
visitor_id: e,
|
|
1852
|
-
user_id: s,
|
|
1853
|
-
sdk_version: m,
|
|
1854
|
-
paywall_id: this.opts.paywallId,
|
|
1855
|
-
capabilities: this.opts.capabilities?.join(",") ?? ""
|
|
1856
|
-
}), n = this.opts.sendBeacon ?? (typeof navigator < "u" && typeof navigator.sendBeacon == "function" ? navigator.sendBeacon.bind(navigator) : null);
|
|
1857
|
-
if (!n) {
|
|
1858
|
-
this.buffer.unshift(...t), this.flush();
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
try {
|
|
1862
|
-
n(this.opts.endpoint, i) || (this.buffer.unshift(...t), this.flush());
|
|
1863
|
-
} catch {
|
|
1864
|
-
this.buffer.unshift(...t), this.flush();
|
|
1865
|
-
}
|
|
1866
|
-
}
|
|
1867
|
-
buildHeaders(t, e) {
|
|
1868
|
-
const s = {
|
|
1869
|
-
"Content-Type": "application/json",
|
|
1870
|
-
"X-SDK-Version": m,
|
|
1871
|
-
"X-Paywall-Id": this.opts.paywallId,
|
|
1872
|
-
"X-Visitor-Id": t
|
|
1873
|
-
};
|
|
1874
|
-
return this.opts.capabilities?.length && (s["X-SDK-Capabilities"] = this.opts.capabilities.join(",")), e && (s["X-User-Id"] = e), s;
|
|
1875
|
-
}
|
|
1876
|
-
attachUnloadHandlers() {
|
|
1877
|
-
typeof window > "u" || (this.unloadHandler = () => this.flushBeacon(), this.visibilityHandler = () => {
|
|
1878
|
-
typeof document < "u" && document.visibilityState === "hidden" && this.flushBeacon();
|
|
1879
|
-
}, window.addEventListener("pagehide", this.unloadHandler), typeof document < "u" && document.addEventListener("visibilitychange", this.visibilityHandler));
|
|
1880
|
-
}
|
|
1881
|
-
detachUnloadHandlers() {
|
|
1882
|
-
typeof window > "u" || (this.unloadHandler && window.removeEventListener("pagehide", this.unloadHandler), this.visibilityHandler && typeof document < "u" && document.removeEventListener("visibilitychange", this.visibilityHandler), this.unloadHandler = null, this.visibilityHandler = null);
|
|
1883
|
-
}
|
|
1884
|
-
destroy() {
|
|
1885
|
-
this.destroyed || (this.destroyed = !0, this.flushTimer && (clearTimeout(this.flushTimer), this.flushTimer = null), this.flush(), this.detachUnloadHandlers());
|
|
1886
|
-
}
|
|
1887
|
-
}
|
|
1
|
+
import { A as r, a as s, b as t, B as i, E as o, P as f, Q as l, S as n, c as S, d as E, e as d, f as A, h as c, o as p, r as O, i as g } from "./chunks/index-CLB1AgLg.js";
|
|
1888
2
|
export {
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
3
|
+
r as ApiClient,
|
|
4
|
+
s as ApiGatewayClient,
|
|
5
|
+
t as AuthClient,
|
|
6
|
+
i as BillingClient,
|
|
7
|
+
o as EventTracker,
|
|
8
|
+
f as PaywallError,
|
|
9
|
+
l as QuotaExceededError,
|
|
10
|
+
n as SDK_VERSION,
|
|
11
|
+
S as STORAGE_KEYS,
|
|
12
|
+
E as createStorage,
|
|
13
|
+
d as ensureVisitorId,
|
|
14
|
+
A as findApplicableOffer,
|
|
15
|
+
c as generateVisitorId,
|
|
16
|
+
p as offerStartStorageKey,
|
|
17
|
+
O as readBrowserOfferStart,
|
|
18
|
+
g as resolveOffer
|
|
1901
19
|
};
|
|
1902
20
|
//# sourceMappingURL=core.js.map
|