xtj-login-kit 0.3.1
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/dist/chunks/index-Cuix-7nW.js +949 -0
- package/dist/chunks/ui-C58EkuFV.js +268 -0
- package/dist/index.d.ts +101 -0
- package/dist/index.js +7 -0
- package/dist/react.d.ts +25 -0
- package/dist/react.js +297 -0
- package/dist/session.js +9 -0
- package/dist/vue.d.ts +26 -0
- package/dist/vue.js +261 -0
- package/package.json +54 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
class AuthError extends Error {
|
|
2
|
+
constructor(code, message, details = void 0) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "AuthError";
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.details = details;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const publicCode = (error) => {
|
|
10
|
+
if (error?.code === "INVALID_APP_CODE") return "INVALID_APP_CODE";
|
|
11
|
+
if (error?.code === "INVALID_GATEWAY_CONFIG") return "INVALID_GATEWAY_CONFIG";
|
|
12
|
+
if (error?.code === "TIMEOUT") return "TIMEOUT";
|
|
13
|
+
if (error?.code === "DUPLICATE_INSTANCE") return "DUPLICATE_INSTANCE";
|
|
14
|
+
if (error?.code === "UNSUPPORTED_ENVIRONMENT") return "UNSUPPORTED_ENVIRONMENT";
|
|
15
|
+
if (error?.code === "NETWORK_ERROR") return "NETWORK_ERROR";
|
|
16
|
+
if (error?.code?.startsWith?.("GATEWAY")) return "GATEWAY_ERROR";
|
|
17
|
+
return "GENERIC_ERROR";
|
|
18
|
+
};
|
|
19
|
+
const toLoginError = (error, stage, retryable = true) => ({
|
|
20
|
+
code: publicCode(error),
|
|
21
|
+
stage,
|
|
22
|
+
message: error?.message || "登录失败,请重试",
|
|
23
|
+
retryable
|
|
24
|
+
});
|
|
25
|
+
const DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
26
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
27
|
+
const DEFAULT_TRANSACTION_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
28
|
+
const MIN_POLL_INTERVAL_MS = 1e3;
|
|
29
|
+
const MAX_POLL_INTERVAL_MS = 6e4;
|
|
30
|
+
const MIN_REQUEST_TIMEOUT_MS = 1e3;
|
|
31
|
+
const MAX_REQUEST_TIMEOUT_MS = 6e4;
|
|
32
|
+
const MIN_TRANSACTION_TIMEOUT_MS = 1e4;
|
|
33
|
+
const MAX_TRANSACTION_TIMEOUT_MS = 15 * 60 * 1e3;
|
|
34
|
+
const MAX_POLL_RETRY_DELAY_MS = 15e3;
|
|
35
|
+
const normalizeDuration = (value, fallback, minimum, maximum) => {
|
|
36
|
+
if (value === void 0 || value === null) return fallback;
|
|
37
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
38
|
+
return Math.min(maximum, Math.max(minimum, Math.round(value)));
|
|
39
|
+
};
|
|
40
|
+
const normalizePollInterval = (value) => normalizeDuration(value, DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS);
|
|
41
|
+
const normalizeRequestTimeout = (value) => normalizeDuration(value, DEFAULT_REQUEST_TIMEOUT_MS, MIN_REQUEST_TIMEOUT_MS, MAX_REQUEST_TIMEOUT_MS);
|
|
42
|
+
const normalizeTransactionTimeout = (value) => normalizeDuration(value, DEFAULT_TRANSACTION_TIMEOUT_MS, MIN_TRANSACTION_TIMEOUT_MS, MAX_TRANSACTION_TIMEOUT_MS);
|
|
43
|
+
const getCode = (value) => String(value?.code ?? "");
|
|
44
|
+
const isAbortError$1 = (error) => error?.name === "AbortError";
|
|
45
|
+
const isRecord$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
|
+
const requireConfigString = (value, field) => {
|
|
47
|
+
const normalized = typeof value === "string" ? value.trim() : "";
|
|
48
|
+
if (!normalized) {
|
|
49
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", `必须配置 ${field}`);
|
|
50
|
+
}
|
|
51
|
+
return normalized;
|
|
52
|
+
};
|
|
53
|
+
const resolveBaseUrl = (value) => {
|
|
54
|
+
const baseUrl = requireConfigString(value, "gatewayOptions.baseUrl");
|
|
55
|
+
try {
|
|
56
|
+
const url = new URL(baseUrl);
|
|
57
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("unsupported protocol");
|
|
58
|
+
} catch {
|
|
59
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", "gatewayOptions.baseUrl 必须是有效的 HTTP(S) 地址");
|
|
60
|
+
}
|
|
61
|
+
return baseUrl;
|
|
62
|
+
};
|
|
63
|
+
const resolvePaths = (value, requireMiniProgramScheme) => {
|
|
64
|
+
if (!isRecord$1(value)) {
|
|
65
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", "必须配置 gatewayOptions.paths");
|
|
66
|
+
}
|
|
67
|
+
const paths = {
|
|
68
|
+
genLoginId: requireConfigString(value.genLoginId, "gatewayOptions.paths.genLoginId"),
|
|
69
|
+
checkLogin: requireConfigString(value.checkLogin, "gatewayOptions.paths.checkLogin"),
|
|
70
|
+
miniProgramScheme: typeof value.miniProgramScheme === "string" ? value.miniProgramScheme.trim() : ""
|
|
71
|
+
};
|
|
72
|
+
if (requireMiniProgramScheme && !paths.miniProgramScheme) {
|
|
73
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", "小程序登录必须配置 gatewayOptions.paths.miniProgramScheme");
|
|
74
|
+
}
|
|
75
|
+
return paths;
|
|
76
|
+
};
|
|
77
|
+
const toUrl = (baseUrl, path, params) => {
|
|
78
|
+
const url = new URL(path, `${baseUrl.replace(/\/$/, "")}/`);
|
|
79
|
+
for (const [key, value] of Object.entries(params || {})) {
|
|
80
|
+
if (value !== void 0 && value !== null && value !== "") {
|
|
81
|
+
url.searchParams.set(key, String(value));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return url;
|
|
85
|
+
};
|
|
86
|
+
const readJson = async (response, label) => {
|
|
87
|
+
let data;
|
|
88
|
+
try {
|
|
89
|
+
data = await response.json();
|
|
90
|
+
} catch {
|
|
91
|
+
throw new AuthError("GATEWAY_ERROR", `${label} 返回了无效响应`);
|
|
92
|
+
}
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
throw new AuthError("GATEWAY_ERROR", `${label} 请求失败(HTTP ${response.status})`, {
|
|
95
|
+
status: response.status,
|
|
96
|
+
response: data
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return data;
|
|
100
|
+
};
|
|
101
|
+
const fetchWithTimeout = async ({ fetchImpl, url, init, timeoutMs, signal, label }) => {
|
|
102
|
+
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
103
|
+
let timeoutId = null;
|
|
104
|
+
let onAbort = null;
|
|
105
|
+
if (controller) {
|
|
106
|
+
onAbort = () => controller.abort();
|
|
107
|
+
if (signal) {
|
|
108
|
+
if (signal.aborted) controller.abort();
|
|
109
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const request = fetchImpl(url, {
|
|
114
|
+
...init,
|
|
115
|
+
signal: controller?.signal || signal
|
|
116
|
+
}).then((response) => readJson(response, label));
|
|
117
|
+
const timeout = new Promise((_, reject) => {
|
|
118
|
+
const timeoutForRace = setTimeout(() => {
|
|
119
|
+
controller?.abort();
|
|
120
|
+
reject(new AuthError("NETWORK_ERROR", `${label} 请求超时`));
|
|
121
|
+
}, timeoutMs);
|
|
122
|
+
timeoutId = timeoutForRace;
|
|
123
|
+
});
|
|
124
|
+
return await Promise.race([request, timeout]);
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (isAbortError$1(error) || controller?.signal.aborted) {
|
|
127
|
+
if (signal?.aborted) throw error;
|
|
128
|
+
throw new AuthError("NETWORK_ERROR", `${label} 请求超时`);
|
|
129
|
+
}
|
|
130
|
+
if (error instanceof AuthError) throw error;
|
|
131
|
+
throw new AuthError("NETWORK_ERROR", error instanceof Error ? error.message : `${label} 请求失败`);
|
|
132
|
+
} finally {
|
|
133
|
+
if (timeoutId !== null) clearTimeout(timeoutId);
|
|
134
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const ensureFetch = (fetchImpl) => {
|
|
138
|
+
if (typeof fetchImpl !== "function") {
|
|
139
|
+
throw new AuthError("UNSUPPORTED_ENVIRONMENT", "当前环境不支持 fetch");
|
|
140
|
+
}
|
|
141
|
+
return fetchImpl;
|
|
142
|
+
};
|
|
143
|
+
const resolveMiniProgramDestination = (options) => {
|
|
144
|
+
const target = requireConfigString(options.schemeTarget, "gatewayOptions.schemeTarget");
|
|
145
|
+
const path = requireConfigString(options.schemeMiniPath, "gatewayOptions.schemeMiniPath").replace(/^\/+/, "");
|
|
146
|
+
if (!path) {
|
|
147
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", "gatewayOptions.schemeMiniPath 不能只包含斜杠");
|
|
148
|
+
}
|
|
149
|
+
return { target, path };
|
|
150
|
+
};
|
|
151
|
+
const createLoginGateway = (options = {}) => {
|
|
152
|
+
const fetchImpl = ensureFetch(options.fetchImpl || globalThis.fetch?.bind(globalThis));
|
|
153
|
+
const usesMiniProgram = options.loginMethod === "mini-program";
|
|
154
|
+
const baseUrl = resolveBaseUrl(options.baseUrl);
|
|
155
|
+
const paths = resolvePaths(options.paths, usesMiniProgram);
|
|
156
|
+
if (usesMiniProgram) resolveMiniProgramDestination(options);
|
|
157
|
+
const requestTimeoutMs = normalizeRequestTimeout(options.requestTimeoutMs);
|
|
158
|
+
const codeType = options.codeType || void 0;
|
|
159
|
+
const get = (path, params, init, label, signal, timeoutMs) => fetchWithTimeout({
|
|
160
|
+
fetchImpl,
|
|
161
|
+
url: toUrl(baseUrl, path, params),
|
|
162
|
+
init: { method: "GET", ...init, cache: "no-store" },
|
|
163
|
+
timeoutMs: normalizeRequestTimeout(timeoutMs ?? requestTimeoutMs),
|
|
164
|
+
signal,
|
|
165
|
+
label
|
|
166
|
+
});
|
|
167
|
+
return {
|
|
168
|
+
async genLoginId({ appCode, signal, timeoutMs } = {}) {
|
|
169
|
+
if (appCode === void 0 || appCode === null || appCode === "") {
|
|
170
|
+
throw new AuthError("INVALID_APP_CODE", "必须提供登录网关发放的 appCode");
|
|
171
|
+
}
|
|
172
|
+
const data = await get(
|
|
173
|
+
paths.genLoginId,
|
|
174
|
+
{ app_code: appCode, code_img: 1, ...codeType ? { code_type: codeType } : {} },
|
|
175
|
+
void 0,
|
|
176
|
+
"生成登录二维码",
|
|
177
|
+
signal,
|
|
178
|
+
timeoutMs
|
|
179
|
+
);
|
|
180
|
+
const loginId = data?.data?.login_id;
|
|
181
|
+
const loginImg = data?.data?.login_img;
|
|
182
|
+
if (!loginId || !loginImg) {
|
|
183
|
+
throw new AuthError("GATEWAY_ERROR", data?.msg || "登录网关未返回二维码");
|
|
184
|
+
}
|
|
185
|
+
return { loginId: String(loginId), loginImg: String(loginImg) };
|
|
186
|
+
},
|
|
187
|
+
async checkLogin({ loginId, signal, timeoutMs } = {}) {
|
|
188
|
+
if (!loginId) throw new AuthError("GATEWAY_ERROR", "登录事务缺少 loginId");
|
|
189
|
+
const data = await get(paths.checkLogin, { login_id: loginId }, void 0, "检查登录状态", signal, timeoutMs);
|
|
190
|
+
const code = getCode(data);
|
|
191
|
+
if (code === "200" || code === "201") return { status: "unscanned", code, data: data.data };
|
|
192
|
+
if (code === "202") return { status: "scanned", code, data: data.data };
|
|
193
|
+
if (code === "203") {
|
|
194
|
+
if (!data?.data?.token) {
|
|
195
|
+
throw new AuthError("GATEWAY_ERROR", "登录网关返回成功状态但缺少 token");
|
|
196
|
+
}
|
|
197
|
+
return { status: "confirmed", code, data: { ...data.data } };
|
|
198
|
+
}
|
|
199
|
+
if (code === "204") return { status: "expired", code, data: data.data };
|
|
200
|
+
if (code === "400") {
|
|
201
|
+
return { status: "failed", code, data: data.data, message: data.msg || "登录失败" };
|
|
202
|
+
}
|
|
203
|
+
throw new AuthError("GATEWAY_ERROR", data?.msg || `未知登录状态 ${code || "empty"}`, {
|
|
204
|
+
code,
|
|
205
|
+
response: data
|
|
206
|
+
});
|
|
207
|
+
},
|
|
208
|
+
async getMiniProgramScheme({ loginId, signal, timeoutMs } = {}) {
|
|
209
|
+
if (!loginId) throw new AuthError("GATEWAY_ERROR", "登录事务缺少 loginId");
|
|
210
|
+
if (!paths.miniProgramScheme) {
|
|
211
|
+
throw new AuthError("INVALID_GATEWAY_CONFIG", "小程序登录必须配置 gatewayOptions.paths.miniProgramScheme");
|
|
212
|
+
}
|
|
213
|
+
const destination = resolveMiniProgramDestination(options);
|
|
214
|
+
const data = await get(
|
|
215
|
+
paths.miniProgramScheme,
|
|
216
|
+
{
|
|
217
|
+
to: destination.target,
|
|
218
|
+
path: destination.path,
|
|
219
|
+
query: `scene=${loginId}`,
|
|
220
|
+
env_version: codeType || "release"
|
|
221
|
+
},
|
|
222
|
+
void 0,
|
|
223
|
+
"生成微信小程序跳转链接",
|
|
224
|
+
signal,
|
|
225
|
+
timeoutMs
|
|
226
|
+
);
|
|
227
|
+
if (!data?.data || typeof data.data !== "string") {
|
|
228
|
+
throw new AuthError("GATEWAY_ERROR", data?.msg || "登录网关未返回微信跳转链接");
|
|
229
|
+
}
|
|
230
|
+
return data.data;
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
const LOGIN_STATUS = Object.freeze({
|
|
235
|
+
IDLE: "idle",
|
|
236
|
+
LOADING: "loading",
|
|
237
|
+
QR_READY: "qr-ready",
|
|
238
|
+
SCANNED: "scanned",
|
|
239
|
+
AUTHENTICATED: "authenticated",
|
|
240
|
+
EXPIRED: "expired",
|
|
241
|
+
CANCELLED: "cancelled",
|
|
242
|
+
ERROR: "error"
|
|
243
|
+
});
|
|
244
|
+
const HIDDEN_GRACE_MS = 180;
|
|
245
|
+
const MAX_CONSECUTIVE_POLL_ERRORS = 5;
|
|
246
|
+
const ACTIVE_STATUS_KINDS = Object.freeze([
|
|
247
|
+
LOGIN_STATUS.LOADING,
|
|
248
|
+
LOGIN_STATUS.QR_READY,
|
|
249
|
+
LOGIN_STATUS.SCANNED
|
|
250
|
+
]);
|
|
251
|
+
const VALID_POLL_RESULTS = /* @__PURE__ */ new Set(["unscanned", "scanned", "confirmed", "expired", "failed"]);
|
|
252
|
+
const getDocument = (value) => value || (typeof document === "undefined" ? null : document);
|
|
253
|
+
const getWindow = (value) => value || (typeof window === "undefined" ? null : window);
|
|
254
|
+
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
255
|
+
const isAbortError = (error) => error?.name === "AbortError";
|
|
256
|
+
const abortError = () => Object.assign(new Error("Aborted"), { name: "AbortError" });
|
|
257
|
+
const wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
258
|
+
if (signal?.aborted) {
|
|
259
|
+
reject(abortError());
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const onAbort = () => {
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
signal?.removeEventListener("abort", onAbort);
|
|
265
|
+
reject(abortError());
|
|
266
|
+
};
|
|
267
|
+
const done = () => {
|
|
268
|
+
signal?.removeEventListener("abort", onAbort);
|
|
269
|
+
resolve();
|
|
270
|
+
};
|
|
271
|
+
const timer = setTimeout(done, ms);
|
|
272
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
273
|
+
});
|
|
274
|
+
const isStronglyHidden = (root) => {
|
|
275
|
+
if (!root) return false;
|
|
276
|
+
if (root.isConnected === false) return true;
|
|
277
|
+
let node = root;
|
|
278
|
+
while (node && node.nodeType === 1) {
|
|
279
|
+
if (node.hidden || node.getAttribute?.("aria-hidden") === "true") return true;
|
|
280
|
+
const style = node.ownerDocument?.defaultView?.getComputedStyle?.(node);
|
|
281
|
+
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
282
|
+
node = node.parentElement;
|
|
283
|
+
}
|
|
284
|
+
const rect = root.getBoundingClientRect?.();
|
|
285
|
+
return Boolean(rect && rect.width === 0 && rect.height === 0);
|
|
286
|
+
};
|
|
287
|
+
const getScope = (windowRef, documentRef) => windowRef || documentRef || null;
|
|
288
|
+
const activeScopes = /* @__PURE__ */ new WeakMap();
|
|
289
|
+
class LoginSession {
|
|
290
|
+
constructor(options = {}) {
|
|
291
|
+
if (options.appCode === void 0 || options.appCode === null || options.appCode === "") {
|
|
292
|
+
throw new AuthError("INVALID_APP_CODE", "必须提供登录网关发放的 appCode");
|
|
293
|
+
}
|
|
294
|
+
this.pollIntervalMs = normalizePollInterval(options.pollIntervalMs);
|
|
295
|
+
this.requestTimeoutMs = normalizeRequestTimeout(
|
|
296
|
+
options.requestTimeoutMs ?? options.gatewayOptions?.requestTimeoutMs
|
|
297
|
+
);
|
|
298
|
+
this.timeoutMs = normalizeTransactionTimeout(options.timeoutMs);
|
|
299
|
+
this.appCode = options.appCode;
|
|
300
|
+
this.platform = options.platform || "web";
|
|
301
|
+
this.loginMethod = options.loginMethod || "qr";
|
|
302
|
+
this.gateway = createLoginGateway({
|
|
303
|
+
...options.gatewayOptions,
|
|
304
|
+
loginMethod: this.loginMethod,
|
|
305
|
+
requestTimeoutMs: this.requestTimeoutMs
|
|
306
|
+
});
|
|
307
|
+
this.windowRef = getWindow(options.windowRef);
|
|
308
|
+
this.documentRef = getDocument(options.documentRef);
|
|
309
|
+
this.scope = getScope(this.windowRef, this.documentRef);
|
|
310
|
+
this.onStatus = options.onStatus || (() => {
|
|
311
|
+
});
|
|
312
|
+
this.onSuccess = options.onSuccess || (() => {
|
|
313
|
+
});
|
|
314
|
+
this.onError = options.onError || (() => {
|
|
315
|
+
});
|
|
316
|
+
this.debug = options.debug === true;
|
|
317
|
+
this.status = { kind: LOGIN_STATUS.IDLE, platform: this.platform };
|
|
318
|
+
this.loginId = "";
|
|
319
|
+
this.loginImg = "";
|
|
320
|
+
this.startedAt = 0;
|
|
321
|
+
this.controller = null;
|
|
322
|
+
this.pendingPromise = null;
|
|
323
|
+
this.generation = 0;
|
|
324
|
+
this.errorNotified = false;
|
|
325
|
+
this.claimed = false;
|
|
326
|
+
this.cancelled = false;
|
|
327
|
+
this.destroyed = false;
|
|
328
|
+
this.mounted = false;
|
|
329
|
+
this.result = null;
|
|
330
|
+
this.root = null;
|
|
331
|
+
this.hiddenTimer = null;
|
|
332
|
+
this.containerHidden = false;
|
|
333
|
+
this.pageHidden = false;
|
|
334
|
+
this.awaitingExternalReturn = false;
|
|
335
|
+
this.miniProgramScheme = "";
|
|
336
|
+
this.miniProgramSchemePromise = null;
|
|
337
|
+
this.miniProgramLaunchPromise = null;
|
|
338
|
+
this.miniProgramNavigationObserved = false;
|
|
339
|
+
this.suspendedForPagehide = false;
|
|
340
|
+
this.checkImmediately = false;
|
|
341
|
+
this.deadlineTimer = null;
|
|
342
|
+
this.visibilityCheckScheduled = false;
|
|
343
|
+
this.visibilityFrame = null;
|
|
344
|
+
this.visibilityTimer = null;
|
|
345
|
+
this.visibilityObserverTargets = [];
|
|
346
|
+
this.observingDisconnectedRoot = false;
|
|
347
|
+
this.mutationObserver = null;
|
|
348
|
+
this.resizeObserver = null;
|
|
349
|
+
this.visibilityHandler = () => {
|
|
350
|
+
const hidden = this.documentRef?.visibilityState === "hidden";
|
|
351
|
+
if (hidden && this.awaitingExternalReturn) this.miniProgramNavigationObserved = true;
|
|
352
|
+
this.debugLog("visibilitychange", { hidden });
|
|
353
|
+
if (!hidden && (this.awaitingExternalReturn || this.miniProgramNavigationObserved)) {
|
|
354
|
+
this.completeExternalReturn("visibilitychange");
|
|
355
|
+
}
|
|
356
|
+
if (!hidden && this.pageHidden && this.suspendedForPagehide && this.mounted && !this.destroyed) {
|
|
357
|
+
this.pageHidden = false;
|
|
358
|
+
this.suspendedForPagehide = false;
|
|
359
|
+
this.scheduleVisibilityCheck();
|
|
360
|
+
if (this.loginId) void this.resumePolling();
|
|
361
|
+
else void this.start();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
if (!hidden) this.checkImmediately = true;
|
|
365
|
+
this.scheduleVisibilityCheck();
|
|
366
|
+
};
|
|
367
|
+
this.pageHideHandler = (event) => {
|
|
368
|
+
if (!ACTIVE_STATUS_KINDS.includes(this.status.kind)) return;
|
|
369
|
+
this.pageHidden = true;
|
|
370
|
+
if (this.awaitingExternalReturn) this.miniProgramNavigationObserved = true;
|
|
371
|
+
this.debugLog("pagehide", {
|
|
372
|
+
persisted: event?.persisted === true,
|
|
373
|
+
externalReturn: this.awaitingExternalReturn
|
|
374
|
+
});
|
|
375
|
+
this.suspendForPagehide(this.awaitingExternalReturn ? "external-return" : "pagehide");
|
|
376
|
+
};
|
|
377
|
+
this.pageShowHandler = (event) => {
|
|
378
|
+
this.debugLog("pageshow", {
|
|
379
|
+
persisted: event?.persisted === true,
|
|
380
|
+
suspended: this.suspendedForPagehide
|
|
381
|
+
});
|
|
382
|
+
if (this.mounted && !this.destroyed && this.suspendedForPagehide) {
|
|
383
|
+
this.pageHidden = false;
|
|
384
|
+
this.completeExternalReturn("pageshow");
|
|
385
|
+
this.suspendedForPagehide = false;
|
|
386
|
+
this.scheduleVisibilityCheck();
|
|
387
|
+
if (this.loginId) void this.resumePolling();
|
|
388
|
+
else void this.start();
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (this.mounted && !this.destroyed) {
|
|
392
|
+
this.checkImmediately = true;
|
|
393
|
+
this.scheduleVisibilityCheck();
|
|
394
|
+
}
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
snapshot() {
|
|
398
|
+
return {
|
|
399
|
+
...this.status,
|
|
400
|
+
loginImg: this.loginImg || void 0
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
emit(kind, extra = {}) {
|
|
404
|
+
const previous = this.status.kind;
|
|
405
|
+
this.status = { kind, platform: this.platform, ...extra };
|
|
406
|
+
this.debugLog("status", { from: previous, to: kind, stage: extra.stage });
|
|
407
|
+
if ([LOGIN_STATUS.AUTHENTICATED, LOGIN_STATUS.EXPIRED, LOGIN_STATUS.ERROR, LOGIN_STATUS.CANCELLED].includes(kind)) {
|
|
408
|
+
this.clearDeadline();
|
|
409
|
+
this.release();
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
this.onStatus(this.snapshot());
|
|
413
|
+
} catch {
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
mount(root) {
|
|
417
|
+
if (this.mounted) return this;
|
|
418
|
+
this.mounted = true;
|
|
419
|
+
this.root = root || null;
|
|
420
|
+
this.destroyed = false;
|
|
421
|
+
this.debugLog("mount", { hasRoot: Boolean(this.root) });
|
|
422
|
+
this.setupObservers();
|
|
423
|
+
void this.start();
|
|
424
|
+
return this;
|
|
425
|
+
}
|
|
426
|
+
unmount() {
|
|
427
|
+
if (this.destroyed) return;
|
|
428
|
+
this.debugLog("unmount", { reason: "component-unmount" });
|
|
429
|
+
this.destroyed = true;
|
|
430
|
+
this.mounted = false;
|
|
431
|
+
this.cancel("组件已卸载", { silent: true });
|
|
432
|
+
this.cleanupObservers();
|
|
433
|
+
this.root = null;
|
|
434
|
+
}
|
|
435
|
+
claim() {
|
|
436
|
+
if (!this.scope) return true;
|
|
437
|
+
const owner = activeScopes.get(this.scope);
|
|
438
|
+
if (owner && owner !== this) return false;
|
|
439
|
+
activeScopes.set(this.scope, this);
|
|
440
|
+
this.claimed = true;
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
release() {
|
|
444
|
+
if (this.scope && activeScopes.get(this.scope) === this) activeScopes.delete(this.scope);
|
|
445
|
+
this.claimed = false;
|
|
446
|
+
}
|
|
447
|
+
start() {
|
|
448
|
+
if (this.destroyed) return Promise.resolve(null);
|
|
449
|
+
if (this.pendingPromise) return this.pendingPromise;
|
|
450
|
+
if (this.status.kind === LOGIN_STATUS.AUTHENTICATED) return Promise.resolve(null);
|
|
451
|
+
this.errorNotified = false;
|
|
452
|
+
if (!this.claim()) {
|
|
453
|
+
const error = new AuthError("DUPLICATE_INSTANCE", "当前页面已经存在活跃的登录组件");
|
|
454
|
+
this.notifyError(error, "init", false);
|
|
455
|
+
return Promise.resolve(null);
|
|
456
|
+
}
|
|
457
|
+
this.cancelled = false;
|
|
458
|
+
this.awaitingExternalReturn = false;
|
|
459
|
+
this.suspendedForPagehide = false;
|
|
460
|
+
this.pageHidden = false;
|
|
461
|
+
this.result = null;
|
|
462
|
+
this.loginId = "";
|
|
463
|
+
this.loginImg = "";
|
|
464
|
+
this.miniProgramScheme = "";
|
|
465
|
+
this.miniProgramSchemePromise = null;
|
|
466
|
+
this.miniProgramLaunchPromise = null;
|
|
467
|
+
this.miniProgramNavigationObserved = false;
|
|
468
|
+
this.checkImmediately = false;
|
|
469
|
+
this.startedAt = Date.now();
|
|
470
|
+
this.controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
471
|
+
const generation = ++this.generation;
|
|
472
|
+
this.armDeadline(generation);
|
|
473
|
+
this.debugLog("start", { generation });
|
|
474
|
+
const promise = this.run(generation);
|
|
475
|
+
this.pendingPromise = promise;
|
|
476
|
+
promise.then(
|
|
477
|
+
() => {
|
|
478
|
+
if (this.pendingPromise === promise) this.pendingPromise = null;
|
|
479
|
+
},
|
|
480
|
+
() => {
|
|
481
|
+
if (this.pendingPromise === promise) this.pendingPromise = null;
|
|
482
|
+
}
|
|
483
|
+
);
|
|
484
|
+
return promise;
|
|
485
|
+
}
|
|
486
|
+
isCurrent(generation) {
|
|
487
|
+
return generation === this.generation && !this.cancelled && !this.destroyed;
|
|
488
|
+
}
|
|
489
|
+
async run(generation) {
|
|
490
|
+
this.debugLog("request-gen-login-id", { generation });
|
|
491
|
+
this.emit(LOGIN_STATUS.LOADING, { message: "正在生成安全二维码", stage: "init" });
|
|
492
|
+
try {
|
|
493
|
+
const generated = await this.gateway.genLoginId({
|
|
494
|
+
appCode: this.appCode,
|
|
495
|
+
platform: this.platform,
|
|
496
|
+
loginMethod: this.loginMethod,
|
|
497
|
+
signal: this.controller?.signal,
|
|
498
|
+
timeoutMs: this.requestTimeoutMs
|
|
499
|
+
});
|
|
500
|
+
if (!this.isCurrent(generation)) return null;
|
|
501
|
+
if (!generated?.loginId || !generated?.loginImg) {
|
|
502
|
+
throw new AuthError("GATEWAY_ERROR", "登录网关未返回二维码");
|
|
503
|
+
}
|
|
504
|
+
this.loginId = String(generated.loginId);
|
|
505
|
+
this.loginImg = String(generated.loginImg);
|
|
506
|
+
this.debugLog("login-id-ready", { generation, loginId: this.loginId });
|
|
507
|
+
if (this.loginMethod === "mini-program") {
|
|
508
|
+
this.emit(LOGIN_STATUS.LOADING, { message: "正在准备微信登录", stage: "mini-program" });
|
|
509
|
+
try {
|
|
510
|
+
await this.prepareMiniProgramScheme(generation);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return null;
|
|
513
|
+
this.notifyError(error, "mini-program", true);
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (!this.isCurrent(generation)) return null;
|
|
518
|
+
this.emit(LOGIN_STATUS.QR_READY, { message: "请使用微信扫码登录", stage: "poll" });
|
|
519
|
+
return await this.poll(generation);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return null;
|
|
522
|
+
this.notifyError(error, "init", true);
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async poll(generation) {
|
|
527
|
+
let consecutiveErrors = 0;
|
|
528
|
+
let nextDelayMs = this.pollIntervalMs;
|
|
529
|
+
while (this.isCurrent(generation)) {
|
|
530
|
+
const remaining = this.timeoutMs - (Date.now() - this.startedAt);
|
|
531
|
+
if (remaining <= 0) {
|
|
532
|
+
this.notifyError(new AuthError("TIMEOUT", "登录事务已超时,请重新发起登录"), "poll", true);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
if (this.documentRef?.visibilityState === "hidden" || this.pageHidden || this.containerHidden) {
|
|
536
|
+
try {
|
|
537
|
+
await wait(Math.min(this.pollIntervalMs, remaining), this.controller?.signal);
|
|
538
|
+
} catch (error) {
|
|
539
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return;
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (!this.checkImmediately) {
|
|
545
|
+
try {
|
|
546
|
+
await wait(Math.min(nextDelayMs, remaining), this.controller?.signal);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return;
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
this.checkImmediately = false;
|
|
553
|
+
if (!this.isCurrent(generation)) return;
|
|
554
|
+
try {
|
|
555
|
+
const result = await this.gateway.checkLogin({
|
|
556
|
+
loginId: this.loginId,
|
|
557
|
+
platform: this.platform,
|
|
558
|
+
signal: this.controller?.signal,
|
|
559
|
+
timeoutMs: this.requestTimeoutMs
|
|
560
|
+
});
|
|
561
|
+
if (!this.isCurrent(generation)) return;
|
|
562
|
+
if (Date.now() - this.startedAt >= this.timeoutMs) {
|
|
563
|
+
this.notifyError(new AuthError("TIMEOUT", "登录事务已超时,请重新发起登录"), "poll", true);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (!VALID_POLL_RESULTS.has(result?.status)) {
|
|
567
|
+
throw new AuthError("GATEWAY_ERROR", "登录网关返回了未知状态");
|
|
568
|
+
}
|
|
569
|
+
consecutiveErrors = 0;
|
|
570
|
+
nextDelayMs = this.pollIntervalMs;
|
|
571
|
+
if (result?.status === "unscanned") {
|
|
572
|
+
this.emit(LOGIN_STATUS.QR_READY, { message: "请使用微信扫码登录", stage: "poll" });
|
|
573
|
+
} else if (result?.status === "scanned") {
|
|
574
|
+
this.emit(LOGIN_STATUS.SCANNED, { message: "扫码成功,请在手机上确认", stage: "poll" });
|
|
575
|
+
} else if (result?.status === "expired") {
|
|
576
|
+
this.emit(LOGIN_STATUS.EXPIRED, { message: "二维码已过期,请点击刷新", stage: "poll" });
|
|
577
|
+
return;
|
|
578
|
+
} else if (result?.status === "failed") {
|
|
579
|
+
this.notifyError(new AuthError("GATEWAY_ERROR", result.message || "登录失败,请重试"), "poll", true);
|
|
580
|
+
return;
|
|
581
|
+
} else if (result?.status === "confirmed") {
|
|
582
|
+
return this.complete(result.data, generation);
|
|
583
|
+
}
|
|
584
|
+
} catch (error) {
|
|
585
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return;
|
|
586
|
+
const remainingAfterError = this.timeoutMs - (Date.now() - this.startedAt);
|
|
587
|
+
if (remainingAfterError <= 0) {
|
|
588
|
+
this.notifyError(new AuthError("TIMEOUT", "登录事务已超时,请重新发起登录"), "poll", true);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
consecutiveErrors += 1;
|
|
592
|
+
if (consecutiveErrors >= MAX_CONSECUTIVE_POLL_ERRORS) {
|
|
593
|
+
this.notifyError(error, "poll", true);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
nextDelayMs = Math.min(
|
|
597
|
+
MAX_POLL_RETRY_DELAY_MS,
|
|
598
|
+
this.pollIntervalMs * 2 ** Math.min(consecutiveErrors, 4)
|
|
599
|
+
);
|
|
600
|
+
this.debugLog("poll-retry", { consecutiveErrors, nextDelayMs });
|
|
601
|
+
this.emit(this.status.kind === LOGIN_STATUS.SCANNED ? LOGIN_STATUS.SCANNED : LOGIN_STATUS.QR_READY, {
|
|
602
|
+
message: "登录服务暂时不可用,正在重试",
|
|
603
|
+
stage: "poll",
|
|
604
|
+
recovering: true
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
complete(loginData, generation) {
|
|
610
|
+
if (!isRecord(loginData) || typeof loginData.token !== "string" || !loginData.token) {
|
|
611
|
+
this.notifyError(new AuthError("GATEWAY_ERROR", "登录结果缺少 token"), "poll", false);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (!this.isCurrent(generation)) return;
|
|
615
|
+
const result = {
|
|
616
|
+
login: Object.freeze({ ...loginData })
|
|
617
|
+
};
|
|
618
|
+
this.result = Object.freeze(result);
|
|
619
|
+
this.emit(LOGIN_STATUS.AUTHENTICATED, { message: "登录成功", stage: "poll" });
|
|
620
|
+
const callbackResult = this.result;
|
|
621
|
+
this.result = null;
|
|
622
|
+
try {
|
|
623
|
+
this.onSuccess(callbackResult);
|
|
624
|
+
} catch {
|
|
625
|
+
}
|
|
626
|
+
return callbackResult;
|
|
627
|
+
}
|
|
628
|
+
notifyError(error, stage, retryable = true) {
|
|
629
|
+
if (this.errorNotified) return;
|
|
630
|
+
this.errorNotified = true;
|
|
631
|
+
this.cancelled = true;
|
|
632
|
+
this.generation += 1;
|
|
633
|
+
this.controller?.abort();
|
|
634
|
+
this.pendingPromise = null;
|
|
635
|
+
this.awaitingExternalReturn = false;
|
|
636
|
+
this.suspendedForPagehide = false;
|
|
637
|
+
const result = toLoginError(error, stage, retryable);
|
|
638
|
+
this.emit(LOGIN_STATUS.ERROR, result);
|
|
639
|
+
try {
|
|
640
|
+
this.onError(result);
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
retry() {
|
|
645
|
+
if (this.destroyed) return Promise.resolve(null);
|
|
646
|
+
this.cancelled = true;
|
|
647
|
+
this.generation += 1;
|
|
648
|
+
this.controller?.abort();
|
|
649
|
+
this.pendingPromise = null;
|
|
650
|
+
this.release();
|
|
651
|
+
this.containerHidden = false;
|
|
652
|
+
this.pageHidden = false;
|
|
653
|
+
this.suspendedForPagehide = false;
|
|
654
|
+
this.awaitingExternalReturn = false;
|
|
655
|
+
this.status = { kind: LOGIN_STATUS.IDLE, platform: this.platform };
|
|
656
|
+
this.debugLog("retry");
|
|
657
|
+
return this.start();
|
|
658
|
+
}
|
|
659
|
+
suspendForPagehide(reason = "pagehide") {
|
|
660
|
+
this.suspendedForPagehide = true;
|
|
661
|
+
this.cancelled = true;
|
|
662
|
+
this.generation += 1;
|
|
663
|
+
this.controller?.abort();
|
|
664
|
+
this.pendingPromise = null;
|
|
665
|
+
this.clearDeadline();
|
|
666
|
+
this.debugLog("suspend", { reason, hasLoginId: Boolean(this.loginId) });
|
|
667
|
+
}
|
|
668
|
+
completeExternalReturn(reason) {
|
|
669
|
+
if (!this.awaitingExternalReturn && !this.miniProgramNavigationObserved) return false;
|
|
670
|
+
this.awaitingExternalReturn = false;
|
|
671
|
+
this.miniProgramNavigationObserved = false;
|
|
672
|
+
this.debugLog("external-return-complete", { reason });
|
|
673
|
+
return true;
|
|
674
|
+
}
|
|
675
|
+
resumePolling() {
|
|
676
|
+
if (this.destroyed || !this.mounted || !this.loginId || this.pendingPromise || !ACTIVE_STATUS_KINDS.includes(this.status.kind)) return Promise.resolve(null);
|
|
677
|
+
const remaining = this.timeoutMs - (Date.now() - this.startedAt);
|
|
678
|
+
if (remaining <= 0) {
|
|
679
|
+
this.cancelled = false;
|
|
680
|
+
this.notifyError(new AuthError("TIMEOUT", "登录事务已超时,请重新发起登录"), "poll", true);
|
|
681
|
+
return Promise.resolve(null);
|
|
682
|
+
}
|
|
683
|
+
this.cancelled = false;
|
|
684
|
+
this.controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
685
|
+
this.checkImmediately = true;
|
|
686
|
+
const generation = ++this.generation;
|
|
687
|
+
this.armDeadline(generation, remaining);
|
|
688
|
+
this.debugLog("resume", { generation, loginId: this.loginId });
|
|
689
|
+
const promise = this.resume(generation);
|
|
690
|
+
this.pendingPromise = promise;
|
|
691
|
+
promise.then(
|
|
692
|
+
() => {
|
|
693
|
+
if (this.pendingPromise === promise) this.pendingPromise = null;
|
|
694
|
+
},
|
|
695
|
+
() => {
|
|
696
|
+
if (this.pendingPromise === promise) this.pendingPromise = null;
|
|
697
|
+
}
|
|
698
|
+
);
|
|
699
|
+
return promise;
|
|
700
|
+
}
|
|
701
|
+
async resume(generation) {
|
|
702
|
+
if (this.loginMethod === "mini-program" && !this.miniProgramScheme) {
|
|
703
|
+
this.emit(LOGIN_STATUS.LOADING, { message: "正在准备微信登录", stage: "mini-program" });
|
|
704
|
+
try {
|
|
705
|
+
await this.prepareMiniProgramScheme(generation);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
if (!this.isCurrent(generation) || isAbortError(error)) return null;
|
|
708
|
+
this.notifyError(error, "mini-program", true);
|
|
709
|
+
return null;
|
|
710
|
+
}
|
|
711
|
+
if (!this.isCurrent(generation)) return null;
|
|
712
|
+
this.emit(LOGIN_STATUS.QR_READY, { message: "请使用微信扫码登录", stage: "poll" });
|
|
713
|
+
}
|
|
714
|
+
return this.poll(generation);
|
|
715
|
+
}
|
|
716
|
+
prepareMiniProgramScheme(generation = this.generation) {
|
|
717
|
+
if (this.miniProgramScheme) return Promise.resolve(this.miniProgramScheme);
|
|
718
|
+
if (this.miniProgramSchemePromise) return this.miniProgramSchemePromise;
|
|
719
|
+
if (!this.loginId) return Promise.reject(new AuthError("GATEWAY_ERROR", "登录事务尚未准备好"));
|
|
720
|
+
const loginId = this.loginId;
|
|
721
|
+
const operation = (async () => {
|
|
722
|
+
const scheme = await this.gateway.getMiniProgramScheme({
|
|
723
|
+
loginId,
|
|
724
|
+
platform: this.platform,
|
|
725
|
+
signal: this.controller?.signal,
|
|
726
|
+
timeoutMs: this.requestTimeoutMs
|
|
727
|
+
});
|
|
728
|
+
if (!this.isCurrent(generation) || loginId !== this.loginId) throw abortError();
|
|
729
|
+
if (typeof scheme !== "string" || !scheme.trim()) {
|
|
730
|
+
throw new AuthError("GATEWAY_ERROR", "登录网关未返回微信小程序跳转链接");
|
|
731
|
+
}
|
|
732
|
+
this.miniProgramScheme = scheme;
|
|
733
|
+
this.debugLog("mini-program-scheme-ready", { hasLoginId: true });
|
|
734
|
+
return scheme;
|
|
735
|
+
})();
|
|
736
|
+
this.miniProgramSchemePromise = operation;
|
|
737
|
+
operation.then(
|
|
738
|
+
() => {
|
|
739
|
+
if (this.miniProgramSchemePromise === operation) this.miniProgramSchemePromise = null;
|
|
740
|
+
},
|
|
741
|
+
() => {
|
|
742
|
+
if (this.miniProgramSchemePromise === operation) this.miniProgramSchemePromise = null;
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
return operation;
|
|
746
|
+
}
|
|
747
|
+
openMiniProgram() {
|
|
748
|
+
if (this.miniProgramLaunchPromise) return this.miniProgramLaunchPromise;
|
|
749
|
+
let operation;
|
|
750
|
+
try {
|
|
751
|
+
if (!ACTIVE_STATUS_KINDS.includes(this.status.kind) || this.cancelled || this.destroyed) {
|
|
752
|
+
throw new AuthError("GATEWAY_ERROR", "当前登录事务不可用,请重新发起登录");
|
|
753
|
+
}
|
|
754
|
+
if (!this.loginId) throw new AuthError("GATEWAY_ERROR", "登录事务尚未准备好");
|
|
755
|
+
const launch = (scheme) => {
|
|
756
|
+
if (!this.windowRef?.location?.assign) {
|
|
757
|
+
throw new AuthError("UNSUPPORTED_ENVIRONMENT", "当前环境无法拉起微信小程序");
|
|
758
|
+
}
|
|
759
|
+
this.miniProgramNavigationObserved = false;
|
|
760
|
+
this.awaitingExternalReturn = true;
|
|
761
|
+
this.debugLog("external-return-start", { hasLoginId: true, cachedScheme: true });
|
|
762
|
+
this.windowRef.location.assign(scheme);
|
|
763
|
+
return scheme;
|
|
764
|
+
};
|
|
765
|
+
operation = this.miniProgramScheme ? Promise.resolve(launch(this.miniProgramScheme)) : this.prepareMiniProgramScheme().then(launch);
|
|
766
|
+
} catch (error) {
|
|
767
|
+
operation = Promise.reject(error);
|
|
768
|
+
}
|
|
769
|
+
const guardedOperation = operation.catch((error) => {
|
|
770
|
+
this.awaitingExternalReturn = false;
|
|
771
|
+
if (!this.cancelled) this.notifyError(error, "mini-program", true);
|
|
772
|
+
throw error;
|
|
773
|
+
});
|
|
774
|
+
this.miniProgramLaunchPromise = guardedOperation;
|
|
775
|
+
guardedOperation.then(
|
|
776
|
+
() => {
|
|
777
|
+
if (this.miniProgramLaunchPromise === guardedOperation) this.miniProgramLaunchPromise = null;
|
|
778
|
+
},
|
|
779
|
+
() => {
|
|
780
|
+
if (this.miniProgramLaunchPromise === guardedOperation) this.miniProgramLaunchPromise = null;
|
|
781
|
+
}
|
|
782
|
+
);
|
|
783
|
+
return guardedOperation;
|
|
784
|
+
}
|
|
785
|
+
canRetryMiniProgramLaunch() {
|
|
786
|
+
return Boolean(
|
|
787
|
+
this.miniProgramScheme && !this.miniProgramNavigationObserved && ACTIVE_STATUS_KINDS.includes(this.status.kind)
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
armDeadline(generation, delayMs = this.timeoutMs) {
|
|
791
|
+
this.clearDeadline();
|
|
792
|
+
this.deadlineTimer = setTimeout(() => {
|
|
793
|
+
this.deadlineTimer = null;
|
|
794
|
+
if (!this.isCurrent(generation)) return;
|
|
795
|
+
this.debugLog("transaction-deadline", { timeoutMs: this.timeoutMs });
|
|
796
|
+
this.notifyError(
|
|
797
|
+
new AuthError("TIMEOUT", "登录事务已超时,请重新发起登录"),
|
|
798
|
+
this.status.stage || "poll",
|
|
799
|
+
true
|
|
800
|
+
);
|
|
801
|
+
}, Math.max(0, delayMs));
|
|
802
|
+
}
|
|
803
|
+
clearDeadline() {
|
|
804
|
+
if (this.deadlineTimer !== null) clearTimeout(this.deadlineTimer);
|
|
805
|
+
this.deadlineTimer = null;
|
|
806
|
+
}
|
|
807
|
+
setupObservers() {
|
|
808
|
+
const doc = this.documentRef;
|
|
809
|
+
const win = this.windowRef;
|
|
810
|
+
doc?.addEventListener?.("visibilitychange", this.visibilityHandler);
|
|
811
|
+
win?.addEventListener?.("pagehide", this.pageHideHandler);
|
|
812
|
+
win?.addEventListener?.("pageshow", this.pageShowHandler);
|
|
813
|
+
const MutationObserverImpl = win?.MutationObserver || globalThis.MutationObserver;
|
|
814
|
+
if (typeof MutationObserverImpl === "function" && this.root) {
|
|
815
|
+
this.mutationObserver = new MutationObserverImpl(() => this.scheduleVisibilityCheck());
|
|
816
|
+
this.observeVisibilityTargets();
|
|
817
|
+
}
|
|
818
|
+
const ResizeObserverImpl = win?.ResizeObserver || globalThis.ResizeObserver;
|
|
819
|
+
if (typeof ResizeObserverImpl === "function" && this.root) {
|
|
820
|
+
this.resizeObserver = new ResizeObserverImpl(() => this.scheduleVisibilityCheck());
|
|
821
|
+
this.resizeObserver.observe(this.root);
|
|
822
|
+
}
|
|
823
|
+
this.checkVisibility();
|
|
824
|
+
}
|
|
825
|
+
cleanupObservers() {
|
|
826
|
+
this.documentRef?.removeEventListener?.("visibilitychange", this.visibilityHandler);
|
|
827
|
+
this.windowRef?.removeEventListener?.("pagehide", this.pageHideHandler);
|
|
828
|
+
this.windowRef?.removeEventListener?.("pageshow", this.pageShowHandler);
|
|
829
|
+
this.mutationObserver?.disconnect();
|
|
830
|
+
this.resizeObserver?.disconnect();
|
|
831
|
+
this.mutationObserver = null;
|
|
832
|
+
this.resizeObserver = null;
|
|
833
|
+
this.visibilityObserverTargets = [];
|
|
834
|
+
this.observingDisconnectedRoot = false;
|
|
835
|
+
if (this.visibilityFrame !== null) {
|
|
836
|
+
this.windowRef?.cancelAnimationFrame?.(this.visibilityFrame);
|
|
837
|
+
this.visibilityFrame = null;
|
|
838
|
+
}
|
|
839
|
+
if (this.visibilityTimer !== null) {
|
|
840
|
+
clearTimeout(this.visibilityTimer);
|
|
841
|
+
this.visibilityTimer = null;
|
|
842
|
+
}
|
|
843
|
+
this.visibilityCheckScheduled = false;
|
|
844
|
+
if (this.hiddenTimer !== null) clearTimeout(this.hiddenTimer);
|
|
845
|
+
this.hiddenTimer = null;
|
|
846
|
+
}
|
|
847
|
+
observeVisibilityTargets() {
|
|
848
|
+
if (!this.mutationObserver || !this.root) return;
|
|
849
|
+
const targets = [];
|
|
850
|
+
let node = this.root;
|
|
851
|
+
while (node && node.nodeType === 1) {
|
|
852
|
+
targets.push(node);
|
|
853
|
+
node = node.parentElement;
|
|
854
|
+
}
|
|
855
|
+
const disconnected = this.root.isConnected === false;
|
|
856
|
+
const recoveryTarget = disconnected ? this.documentRef?.body : null;
|
|
857
|
+
if (recoveryTarget && !targets.includes(recoveryTarget)) targets.push(recoveryTarget);
|
|
858
|
+
if (disconnected === this.observingDisconnectedRoot && targets.length === this.visibilityObserverTargets.length && targets.every((target, index) => target === this.visibilityObserverTargets[index])) return;
|
|
859
|
+
this.mutationObserver.disconnect();
|
|
860
|
+
for (const target of targets) {
|
|
861
|
+
if (disconnected && target === recoveryTarget && target !== this.root) {
|
|
862
|
+
this.mutationObserver.observe(target, { childList: true, subtree: true });
|
|
863
|
+
} else {
|
|
864
|
+
this.mutationObserver.observe(target, {
|
|
865
|
+
childList: true,
|
|
866
|
+
attributes: true,
|
|
867
|
+
attributeFilter: ["hidden", "aria-hidden", "style", "class"]
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
this.visibilityObserverTargets = targets;
|
|
872
|
+
this.observingDisconnectedRoot = disconnected;
|
|
873
|
+
}
|
|
874
|
+
scheduleVisibilityCheck() {
|
|
875
|
+
if (!this.mounted || this.destroyed || !this.root || this.visibilityCheckScheduled) return;
|
|
876
|
+
this.visibilityCheckScheduled = true;
|
|
877
|
+
const run = () => {
|
|
878
|
+
this.visibilityCheckScheduled = false;
|
|
879
|
+
this.visibilityFrame = null;
|
|
880
|
+
this.visibilityTimer = null;
|
|
881
|
+
this.checkVisibility();
|
|
882
|
+
};
|
|
883
|
+
if (typeof this.windowRef?.requestAnimationFrame === "function") {
|
|
884
|
+
const frame = this.windowRef.requestAnimationFrame(run);
|
|
885
|
+
if (this.visibilityCheckScheduled) this.visibilityFrame = frame;
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
this.visibilityTimer = setTimeout(run, 16);
|
|
889
|
+
}
|
|
890
|
+
checkVisibility() {
|
|
891
|
+
if (!this.mounted || this.destroyed || !this.root) return;
|
|
892
|
+
this.observeVisibilityTargets();
|
|
893
|
+
if (!isStronglyHidden(this.root)) {
|
|
894
|
+
if (this.hiddenTimer !== null) clearTimeout(this.hiddenTimer);
|
|
895
|
+
this.hiddenTimer = null;
|
|
896
|
+
if (this.containerHidden) {
|
|
897
|
+
this.containerHidden = false;
|
|
898
|
+
this.checkImmediately = true;
|
|
899
|
+
this.debugLog("container-visible");
|
|
900
|
+
}
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (this.containerHidden || this.hiddenTimer !== null) return;
|
|
904
|
+
this.hiddenTimer = setTimeout(() => {
|
|
905
|
+
this.hiddenTimer = null;
|
|
906
|
+
if (!isStronglyHidden(this.root) || this.destroyed) return;
|
|
907
|
+
this.containerHidden = true;
|
|
908
|
+
this.debugLog("container-hidden", { hasLoginId: Boolean(this.loginId) });
|
|
909
|
+
}, HIDDEN_GRACE_MS);
|
|
910
|
+
}
|
|
911
|
+
cancel(reason = "登录已取消", { silent = false } = {}) {
|
|
912
|
+
if (this.cancelled && this.status.kind === LOGIN_STATUS.CANCELLED) return;
|
|
913
|
+
this.debugLog("cancel", { reason, silent });
|
|
914
|
+
this.cancelled = true;
|
|
915
|
+
this.generation += 1;
|
|
916
|
+
this.controller?.abort();
|
|
917
|
+
this.pendingPromise = null;
|
|
918
|
+
this.clearDeadline();
|
|
919
|
+
this.release();
|
|
920
|
+
this.suspendedForPagehide = false;
|
|
921
|
+
this.awaitingExternalReturn = false;
|
|
922
|
+
this.miniProgramScheme = "";
|
|
923
|
+
this.miniProgramSchemePromise = null;
|
|
924
|
+
this.miniProgramLaunchPromise = null;
|
|
925
|
+
this.miniProgramNavigationObserved = false;
|
|
926
|
+
this.pageHidden = false;
|
|
927
|
+
if (!silent && !this.destroyed) this.emit(LOGIN_STATUS.CANCELLED, { message: reason });
|
|
928
|
+
}
|
|
929
|
+
debugLog(event, details = {}) {
|
|
930
|
+
if (!this.debug) return;
|
|
931
|
+
const logger = this.windowRef?.console || (typeof console === "undefined" ? null : console);
|
|
932
|
+
logger?.info?.("[XTJ LoginKit]", event, {
|
|
933
|
+
status: this.status.kind,
|
|
934
|
+
generation: this.generation,
|
|
935
|
+
...details
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const createLoginSession = (options) => new LoginSession(options);
|
|
940
|
+
export {
|
|
941
|
+
AuthError as A,
|
|
942
|
+
DEFAULT_POLL_INTERVAL_MS as D,
|
|
943
|
+
LOGIN_STATUS as L,
|
|
944
|
+
DEFAULT_REQUEST_TIMEOUT_MS as a,
|
|
945
|
+
DEFAULT_TRANSACTION_TIMEOUT_MS as b,
|
|
946
|
+
createLoginSession as c,
|
|
947
|
+
LoginSession as d,
|
|
948
|
+
toLoginError as t
|
|
949
|
+
};
|