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.
@@ -0,0 +1,268 @@
1
+ import { L as LOGIN_STATUS } from "./index-Cuix-7nW.js";
2
+ const isMiniProgramMethod = (loginMethod) => loginMethod === "mini-program";
3
+ const MINI_PROGRAM_LAUNCH_RECOVERY_MS = 2500;
4
+ const ALERT_ICON = new URL("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='1.75'%20stroke-linecap='round'%20stroke-linejoin='round'%20class='icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-exclamation-circle'%3e%3cpath%20stroke='none'%20d='M0%200h24v24H0z'%20fill='none'%20/%3e%3cpath%20d='M3%2012a9%209%200%201%200%2018%200a9%209%200%201%200%20-18%200'%20/%3e%3cpath%20d='M12%209v4'%20/%3e%3cpath%20d='M12%2016v.01'%20/%3e%3c/svg%3e", import.meta.url).href;
5
+ const LOADING_ICON = new URL("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='1.75'%20stroke-linecap='round'%20stroke-linejoin='round'%20class='icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-loader'%3e%3cpath%20stroke='none'%20d='M0%200h24v24H0z'%20fill='none'%20/%3e%3cpath%20d='M12%206l0%20-3'%20/%3e%3cpath%20d='M16.25%207.75l2.15%20-2.15'%20/%3e%3cpath%20d='M18%2012l3%200'%20/%3e%3cpath%20d='M16.25%2016.25l2.15%202.15'%20/%3e%3cpath%20d='M12%2018l0%203'%20/%3e%3cpath%20d='M7.75%2016.25l-2.15%202.15'%20/%3e%3cpath%20d='M6%2012l-3%200'%20/%3e%3cpath%20d='M7.75%207.75l-2.15%20-2.15'%20/%3e%3c/svg%3e", import.meta.url).href;
6
+ const REFRESH_ICON = new URL("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='1.75'%20stroke-linecap='round'%20stroke-linejoin='round'%20class='icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-refresh'%3e%3cpath%20stroke='none'%20d='M0%200h24v24H0z'%20fill='none'%20/%3e%3cpath%20d='M20%2011a8.1%208.1%200%200%200%20-15.5%20-2m-.5%20-4v4h4'%20/%3e%3cpath%20d='M4%2013a8.1%208.1%200%200%200%2015.5%202m.5%204v-4h-4'%20/%3e%3c/svg%3e", import.meta.url).href;
7
+ const SUCCESS_ICON = new URL("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='1.75'%20stroke-linecap='round'%20stroke-linejoin='round'%20class='icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-circle-check'%3e%3cpath%20stroke='none'%20d='M0%200h24v24H0z'%20fill='none'%20/%3e%3cpath%20d='M3%2012a9%209%200%201%200%2018%200a9%209%200%201%200%20-18%200'%20/%3e%3cpath%20d='M9%2012l2%202l4%20-4'%20/%3e%3c/svg%3e", import.meta.url).href;
8
+ const WECHAT_ICON = new URL("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='24'%20height='24'%20viewBox='0%200%2024%2024'%20fill='none'%20stroke='currentColor'%20stroke-width='1.75'%20stroke-linecap='round'%20stroke-linejoin='round'%20class='icon%20icon-tabler%20icons-tabler-outline%20icon-tabler-brand-wechat'%3e%3cpath%20stroke='none'%20d='M0%200h24v24H0z'%20fill='none'%20/%3e%3cpath%20d='M16.5%2010c3.038%200%205.5%202.015%205.5%204.5c0%201.397%20-.778%202.645%20-2%203.47l0%202.03l-1.964%20-1.178a6.649%206.649%200%200%201%20-1.536%20.178c-3.038%200%20-5.5%20-2.015%20-5.5%20-4.5s2.462%20-4.5%205.5%20-4.5'%20/%3e%3cpath%20d='M11.197%2015.698c-.69%20.196%20-1.43%20.302%20-2.197%20.302a8.008%208.008%200%200%201%20-2.612%20-.432l-2.388%201.432v-2.801c-1.237%20-1.082%20-2%20-2.564%20-2%20-4.199c0%20-3.314%203.134%20-6%207%20-6c3.782%200%206.863%202.57%207%205.785l0%20.233'%20/%3e%3cpath%20d='M10%208h.01'%20/%3e%3cpath%20d='M7%208h.01'%20/%3e%3cpath%20d='M15%2014h.01'%20/%3e%3cpath%20d='M18%2014h.01'%20/%3e%3c/svg%3e", import.meta.url).href;
9
+ const LOGIN_MOTION_STYLES = `
10
+ @keyframes xtj-login-qr-spin {
11
+ from { transform: translate(-50%, -50%) rotate(0deg); }
12
+ to { transform: translate(-50%, -50%) rotate(360deg); }
13
+ }
14
+
15
+ @keyframes xtj-login-mini-spin {
16
+ from { transform: rotate(0deg); }
17
+ to { transform: rotate(360deg); }
18
+ }
19
+
20
+ .xtj-login__state-icon--loading {
21
+ animation: xtj-login-qr-spin 1s linear infinite;
22
+ will-change: transform;
23
+ }
24
+
25
+ .xtj-login__mini-icon--loading {
26
+ animation: xtj-login-mini-spin 1s linear infinite;
27
+ will-change: transform;
28
+ }
29
+
30
+ .xtj-login__state-icon--refresh {
31
+ transition: transform 120ms ease-out;
32
+ }
33
+
34
+ .xtj-login__state--action:active .xtj-login__state-icon--refresh {
35
+ transform: translate(-50%, -50%) scale(0.86) !important;
36
+ }
37
+
38
+ @media (prefers-reduced-motion: reduce) {
39
+ .xtj-login__state-icon--loading,
40
+ .xtj-login__mini-icon--loading {
41
+ animation: none;
42
+ }
43
+
44
+ .xtj-login__state-icon--refresh {
45
+ transition: none;
46
+ }
47
+ }
48
+ `;
49
+ const QR_LOGIN_STYLE = Object.freeze({
50
+ boxSizing: "border-box",
51
+ position: "relative",
52
+ display: "block",
53
+ width: "min(100%, var(--xtj-login-size, 232px))",
54
+ aspectRatio: "1 / 1",
55
+ overflow: "hidden",
56
+ borderRadius: "6px",
57
+ background: "#ffffff"
58
+ });
59
+ const MINI_PROGRAM_LOGIN_STYLE = Object.freeze({
60
+ boxSizing: "border-box",
61
+ display: "inline-flex",
62
+ width: "fit-content",
63
+ maxWidth: "100%"
64
+ });
65
+ const getLoginRootStyle = (loginMethod) => isMiniProgramMethod(loginMethod) ? MINI_PROGRAM_LOGIN_STYLE : QR_LOGIN_STYLE;
66
+ const qrContentStyle = Object.freeze({
67
+ position: "absolute",
68
+ inset: "0",
69
+ overflow: "hidden",
70
+ background: "#ffffff"
71
+ });
72
+ const QR_IMAGE_STYLE = Object.freeze({
73
+ boxSizing: "border-box",
74
+ position: "absolute",
75
+ inset: "0",
76
+ display: "block",
77
+ width: "100%",
78
+ height: "100%",
79
+ padding: "10px",
80
+ background: "#ffffff",
81
+ objectFit: "contain",
82
+ transition: "filter 160ms ease, opacity 160ms ease, transform 160ms ease"
83
+ });
84
+ const getQrImageStyle = (obscured) => ({
85
+ ...QR_IMAGE_STYLE,
86
+ filter: obscured ? "blur(6px)" : "none",
87
+ opacity: obscured ? 0.5 : 1,
88
+ transform: obscured ? "scale(1.04)" : "scale(1)"
89
+ });
90
+ const qrOverlayStyle = Object.freeze({
91
+ boxSizing: "border-box",
92
+ position: "absolute",
93
+ inset: "0",
94
+ width: "100%",
95
+ height: "100%",
96
+ margin: "0",
97
+ padding: "24px",
98
+ border: "0",
99
+ background: "rgba(255, 255, 255, 0.68)",
100
+ color: "#344054",
101
+ textAlign: "center"
102
+ });
103
+ const qrActionStyle = Object.freeze({
104
+ ...qrOverlayStyle,
105
+ appearance: "none",
106
+ borderRadius: "0",
107
+ font: "inherit",
108
+ cursor: "pointer",
109
+ touchAction: "manipulation",
110
+ WebkitTapHighlightColor: "transparent"
111
+ });
112
+ const qrIconStyle = Object.freeze({
113
+ position: "absolute",
114
+ top: "50%",
115
+ left: "50%",
116
+ display: "block",
117
+ width: "36px",
118
+ height: "36px",
119
+ transform: "translate(-50%, -50%)"
120
+ });
121
+ const getQrLabelStyle = (hasIcon) => ({
122
+ position: "absolute",
123
+ top: hasIcon ? "calc(50% + 29px)" : "50%",
124
+ left: "50%",
125
+ width: "160px",
126
+ maxWidth: "calc(100% - 32px)",
127
+ transform: hasIcon ? "translateX(-50%)" : "translate(-50%, -50%)",
128
+ color: "#667085",
129
+ fontSize: "13px",
130
+ fontWeight: "400",
131
+ lineHeight: "1.5",
132
+ textAlign: "center"
133
+ });
134
+ const getQrPresentation = (status, active = true) => {
135
+ if (!active) return { icon: null, label: "登录未开始" };
136
+ if (status.kind === LOGIN_STATUS.QR_READY && !status.recovering) return null;
137
+ if (status.kind === LOGIN_STATUS.SCANNED) {
138
+ return {
139
+ icon: SUCCESS_ICON,
140
+ label: "请在手机上确认"
141
+ };
142
+ }
143
+ if (status.kind === LOGIN_STATUS.AUTHENTICATED) {
144
+ return { icon: SUCCESS_ICON, label: "登录成功" };
145
+ }
146
+ if (status.kind === LOGIN_STATUS.EXPIRED) {
147
+ return { icon: REFRESH_ICON, iconState: "refresh", label: "二维码已过期", action: "retry" };
148
+ }
149
+ if (status.kind === LOGIN_STATUS.ERROR) {
150
+ return {
151
+ icon: ALERT_ICON,
152
+ label: status.retryable === false ? "登录失败" : "登录失败,点击重试",
153
+ action: status.retryable === false ? null : "retry"
154
+ };
155
+ }
156
+ if (status.kind === LOGIN_STATUS.CANCELLED) {
157
+ return { icon: ALERT_ICON, label: "登录已取消" };
158
+ }
159
+ if (status.kind === LOGIN_STATUS.QR_READY) {
160
+ return { icon: LOADING_ICON, iconState: "loading", label: "网络波动,正在重试" };
161
+ }
162
+ return { icon: LOADING_ICON, iconState: "loading", label: "正在准备二维码" };
163
+ };
164
+ const MINI_BUTTON_STYLE = Object.freeze({
165
+ boxSizing: "border-box",
166
+ display: "inline-flex",
167
+ alignItems: "center",
168
+ justifyContent: "center",
169
+ gap: "7px",
170
+ width: "184px",
171
+ maxWidth: "100%",
172
+ height: "40px",
173
+ margin: "0",
174
+ padding: "0 16px",
175
+ appearance: "none",
176
+ border: "1px solid transparent",
177
+ borderRadius: "6px",
178
+ font: "inherit",
179
+ fontSize: "14px",
180
+ fontWeight: "400",
181
+ lineHeight: "1",
182
+ whiteSpace: "nowrap",
183
+ opacity: "1",
184
+ touchAction: "manipulation",
185
+ transition: "background-color 160ms ease, border-color 160ms ease",
186
+ WebkitTapHighlightColor: "transparent"
187
+ });
188
+ const MINI_BUTTON_TONES = Object.freeze({
189
+ primary: { background: "var(--xtj-login-mini-program, #07c160)", borderColor: "#07ad56", color: "#ffffff" },
190
+ success: { background: "var(--xtj-login-success, #087443)", borderColor: "#07663b", color: "#ffffff" },
191
+ danger: { background: "var(--xtj-login-danger, #b42318)", borderColor: "#991f15", color: "#ffffff" },
192
+ muted: { background: "#f2f4f7", borderColor: "#e4e7ec", color: "#667085" }
193
+ });
194
+ const getMiniProgramButtonStyle = (presentation) => ({
195
+ ...MINI_BUTTON_STYLE,
196
+ ...MINI_BUTTON_TONES[presentation.tone],
197
+ cursor: presentation.disabled ? "default" : "pointer"
198
+ });
199
+ const getMiniProgramIconStyle = (presentation) => ({
200
+ display: "block",
201
+ flex: "0 0 auto",
202
+ width: "19px",
203
+ height: "19px",
204
+ opacity: presentation.tone === "muted" ? 0.62 : 1,
205
+ filter: presentation.tone === "muted" ? "none" : "brightness(0) invert(1)"
206
+ });
207
+ const getMiniProgramPresentation = (status, phase = "idle", active = true) => {
208
+ if (!active) {
209
+ return { icon: WECHAT_ICON, label: "登录未开始", tone: "muted", disabled: true };
210
+ }
211
+ if (status.kind === LOGIN_STATUS.SCANNED) {
212
+ return {
213
+ icon: LOADING_ICON,
214
+ iconState: "loading",
215
+ label: "等待小程序确认",
216
+ tone: "muted",
217
+ disabled: true
218
+ };
219
+ }
220
+ if (status.kind === LOGIN_STATUS.AUTHENTICATED) {
221
+ return { icon: SUCCESS_ICON, label: "登录成功", tone: "success", disabled: true };
222
+ }
223
+ if (status.kind === LOGIN_STATUS.EXPIRED) {
224
+ return { icon: REFRESH_ICON, label: "重新获取登录入口", tone: "primary", disabled: false, action: "retry" };
225
+ }
226
+ if (status.kind === LOGIN_STATUS.ERROR) {
227
+ const retryable = status.retryable !== false;
228
+ return {
229
+ icon: ALERT_ICON,
230
+ label: retryable ? "登录失败,重新获取" : "登录失败",
231
+ tone: "danger",
232
+ disabled: !retryable,
233
+ action: retryable ? "retry" : null
234
+ };
235
+ }
236
+ if (status.kind === LOGIN_STATUS.CANCELLED) {
237
+ return { icon: ALERT_ICON, label: "登录已取消", tone: "muted", disabled: true };
238
+ }
239
+ if (phase === "launching") {
240
+ return { icon: LOADING_ICON, iconState: "loading", label: "正在拉起微信", tone: "muted", disabled: true };
241
+ }
242
+ if (phase === "awaiting") {
243
+ return { icon: LOADING_ICON, iconState: "loading", label: "等待登录确认", tone: "muted", disabled: true };
244
+ }
245
+ if (phase === "retry-launch") {
246
+ return { icon: WECHAT_ICON, label: "重新打开微信", tone: "primary", disabled: false, action: "open" };
247
+ }
248
+ if (status.kind === LOGIN_STATUS.QR_READY) {
249
+ return { icon: WECHAT_ICON, label: "微信登录", tone: "primary", disabled: false, action: "open" };
250
+ }
251
+ return { icon: LOADING_ICON, iconState: "loading", label: "正在准备登录", tone: "muted", disabled: true };
252
+ };
253
+ export {
254
+ LOGIN_MOTION_STYLES as L,
255
+ MINI_PROGRAM_LAUNCH_RECOVERY_MS as M,
256
+ getMiniProgramPresentation as a,
257
+ getMiniProgramIconStyle as b,
258
+ getMiniProgramButtonStyle as c,
259
+ getQrPresentation as d,
260
+ getQrImageStyle as e,
261
+ getQrLabelStyle as f,
262
+ getLoginRootStyle as g,
263
+ qrActionStyle as h,
264
+ isMiniProgramMethod as i,
265
+ qrOverlayStyle as j,
266
+ qrContentStyle as k,
267
+ qrIconStyle as q
268
+ };
@@ -0,0 +1,101 @@
1
+ export type LoginStatusKind =
2
+ | 'idle'
3
+ | 'loading'
4
+ | 'qr-ready'
5
+ | 'scanned'
6
+ | 'authenticated'
7
+ | 'expired'
8
+ | 'cancelled'
9
+ | 'error'
10
+
11
+ export type AuthLoginData = Record<string, unknown> & { token: string }
12
+ export type LoginResult = {
13
+ login: AuthLoginData
14
+ }
15
+
16
+ export type LoginErrorCode =
17
+ | 'INVALID_APP_CODE'
18
+ | 'INVALID_GATEWAY_CONFIG'
19
+ | 'NETWORK_ERROR'
20
+ | 'GATEWAY_ERROR'
21
+ | 'UNSUPPORTED_ENVIRONMENT'
22
+ | 'TIMEOUT'
23
+ | 'DUPLICATE_INSTANCE'
24
+ | 'GENERIC_ERROR'
25
+
26
+ export type LoginError = {
27
+ code: LoginErrorCode
28
+ stage: 'init' | 'poll' | 'mini-program'
29
+ message: string
30
+ retryable: boolean
31
+ }
32
+
33
+ export class AuthError extends Error {
34
+ readonly name: 'AuthError'
35
+ readonly code: string
36
+ readonly details?: unknown
37
+ constructor(code: string, message: string, details?: unknown)
38
+ }
39
+
40
+ export type LoginStatus = {
41
+ kind: LoginStatusKind
42
+ platform: 'web' | 'mobile-web' | string
43
+ message?: string
44
+ stage?: 'init' | 'poll' | 'mini-program'
45
+ code?: LoginErrorCode
46
+ loginImg?: string
47
+ retryable?: boolean
48
+ recovering?: boolean
49
+ }
50
+
51
+ export type LoginGatewayPaths = {
52
+ genLoginId: string
53
+ checkLogin: string
54
+ miniProgramScheme?: string
55
+ }
56
+
57
+ export type LoginGatewayOptions = {
58
+ baseUrl: string
59
+ codeType?: string
60
+ schemeTarget?: string
61
+ schemeMiniPath?: string
62
+ requestTimeoutMs?: number
63
+ fetchImpl?: typeof fetch
64
+ paths: LoginGatewayPaths
65
+ }
66
+
67
+ export type LoginHandle = {
68
+ readonly element: HTMLDivElement | null
69
+ retry(): Promise<LoginResult | null>
70
+ cancel(reason?: string): void
71
+ }
72
+
73
+ export type LoginSessionOptions = {
74
+ gatewayOptions: LoginGatewayOptions
75
+ appCode: string | number
76
+ platform?: 'web' | 'mobile-web' | string
77
+ loginMethod?: 'qr' | 'mini-program' | string
78
+ pollIntervalMs?: number
79
+ requestTimeoutMs?: number
80
+ timeoutMs?: number
81
+ debug?: boolean
82
+ windowRef?: Window
83
+ documentRef?: Document
84
+ onStatus?: (status: LoginStatus) => void
85
+ onSuccess?: (result: LoginResult) => void
86
+ onError?: (error: LoginError) => void
87
+ }
88
+
89
+ export class LoginSession {
90
+ constructor(options: LoginSessionOptions)
91
+ mount(root: HTMLElement | null): this
92
+ unmount(): void
93
+ start(): Promise<LoginResult | null>
94
+ retry(): Promise<LoginResult | null>
95
+ cancel(reason?: string): void
96
+ openMiniProgram(): Promise<string>
97
+ snapshot(): LoginStatus
98
+ }
99
+
100
+ export function createLoginSession(options: LoginSessionOptions): LoginSession
101
+ export const LOGIN_STATUS: Readonly<Record<'IDLE' | 'LOADING' | 'QR_READY' | 'SCANNED' | 'AUTHENTICATED' | 'EXPIRED' | 'CANCELLED' | 'ERROR', LoginStatusKind>>
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ import { A, L, d, c } from "./chunks/index-Cuix-7nW.js";
2
+ export {
3
+ A as AuthError,
4
+ L as LOGIN_STATUS,
5
+ d as LoginSession,
6
+ c as createLoginSession
7
+ };
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties } from 'react'
2
+ import type { LoginError, LoginGatewayOptions, LoginHandle, LoginResult, LoginStatus } from './index.js'
3
+
4
+ export type { LoginHandle } from './index.js'
5
+
6
+ export type LoginProps = {
7
+ appCode: string | number
8
+ active?: boolean
9
+ className?: string
10
+ style?: CSSProperties
11
+ platform?: 'web' | 'mobile-web' | string
12
+ loginMethod?: 'qr' | 'mini-program' | string
13
+ pollIntervalMs?: number
14
+ requestTimeoutMs?: number
15
+ timeoutMs?: number
16
+ debug?: boolean
17
+ gatewayOptions: LoginGatewayOptions
18
+ windowRef?: Window
19
+ documentRef?: Document
20
+ onSuccess?: (result: LoginResult) => void
21
+ onError?: (error: LoginError) => void
22
+ onStatus?: (status: LoginStatus) => void
23
+ }
24
+
25
+ export const Login: import('react').ForwardRefExoticComponent<LoginProps & import('react').RefAttributes<LoginHandle>>