@huaqiu/dsh-auth 0.5.1 → 0.5.3
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/lib/client.js +767 -34
- package/lib/client.js.map +1 -1
- package/package.json +2 -2
- package/src/client/client.ts +63 -0
- package/src/client/i18n.ts +37 -0
- package/src/client/index.tsx +15 -0
- package/src/client/lib.ts +111 -0
- package/src/client/ui/sidebar-action.tsx +477 -20
- package/src/client/ui/subscription-dialog.ts +183 -0
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { memo, useEffect, useLayoutEffect, useRef, useState, useSyncExternalStore, type CSSProperties } from 'react'
|
|
27
27
|
import { createPortal } from 'react-dom'
|
|
28
28
|
import { getAuth, getAuthState, getHqEdge, setProfile, subscribeAuth } from '../auth-state.js'
|
|
29
|
-
import { buildProfileUrl } from '../lib.js'
|
|
29
|
+
import { buildProfileUrl, ERC_PRODUCT_CODE, GEN_PRODUCT_CODE, type SubscriptionQuota } from '../lib.js'
|
|
30
30
|
import { useIsDark, useLocale } from '../ui-env.js'
|
|
31
31
|
import { useT } from '../i18n.js'
|
|
32
32
|
import { HQ_ICON } from './hq-icon.jsx'
|
|
@@ -49,6 +49,25 @@ interface Palette {
|
|
|
49
49
|
dangerHover: string
|
|
50
50
|
avatarBg: string
|
|
51
51
|
shadow: string
|
|
52
|
+
/** Profile-header card background (prototype `bg-white/[0.03]`). */
|
|
53
|
+
card: string
|
|
54
|
+
/** 1px hairline used on cards inside the popup. */
|
|
55
|
+
cardBorder: string
|
|
56
|
+
/** Quota metric card background (prototype `bg-black/20`). */
|
|
57
|
+
metric: string
|
|
58
|
+
metricHover: string
|
|
59
|
+
/** Huaqiu brand red (#e62e2d). */
|
|
60
|
+
primary: string
|
|
61
|
+
primaryHover: string
|
|
62
|
+
/** Badge tint: primary at ~10% (prototype `bg-hq-primary/10`). */
|
|
63
|
+
primarySoft: string
|
|
64
|
+
/** Progress-bar track (prototype `dark:bg-gray-800`). */
|
|
65
|
+
track: string
|
|
66
|
+
/** GEN / ERC progress gradients (prototype blue→indigo, purple→pink). */
|
|
67
|
+
genGradient: string
|
|
68
|
+
ercGradient: string
|
|
69
|
+
/** "Online" dot green. */
|
|
70
|
+
online: string
|
|
52
71
|
}
|
|
53
72
|
|
|
54
73
|
const LIGHT_PALETTE: Palette = {
|
|
@@ -60,19 +79,41 @@ const LIGHT_PALETTE: Palette = {
|
|
|
60
79
|
danger: 'var(--dsw-alias-state-error-primary, #d4380d)',
|
|
61
80
|
dangerHover: 'rgba(216, 56, 13, 0.08)',
|
|
62
81
|
avatarBg: 'var(--dsw-alias-bg-layer-2, #eef2f7)',
|
|
63
|
-
shadow: '0
|
|
82
|
+
shadow: '0 20px 40px -15px rgba(0, 0, 0, 0.12), 0 0 1px 1px rgba(0, 0, 0, 0.05)',
|
|
83
|
+
card: '#f7f8fa',
|
|
84
|
+
cardBorder: '#e8ebf0',
|
|
85
|
+
metric: '#f7f8fa',
|
|
86
|
+
metricHover: '#eef1f5',
|
|
87
|
+
primary: '#e62e2d',
|
|
88
|
+
primaryHover: '#cc2423',
|
|
89
|
+
primarySoft: 'rgba(230, 46, 45, 0.08)',
|
|
90
|
+
track: '#e4e7ec',
|
|
91
|
+
genGradient: 'linear-gradient(90deg, #3b82f6, #6366f1)',
|
|
92
|
+
ercGradient: 'linear-gradient(90deg, #a855f7, #ec4899)',
|
|
93
|
+
online: '#10b981',
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
const DARK_PALETTE: Palette = {
|
|
67
|
-
surface: 'var(--dsw-alias-bg-overlay, #
|
|
68
|
-
border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.
|
|
97
|
+
surface: 'var(--dsw-alias-bg-overlay, #1e1b24)',
|
|
98
|
+
border: 'var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.10))',
|
|
69
99
|
text: 'var(--dsw-alias-label-primary, #e6eaf0)',
|
|
70
100
|
muted: 'var(--dsw-alias-label-secondary, #8b95a5)',
|
|
71
101
|
hover: 'var(--dsw-alias-interactive-bg-hover, rgba(255, 255, 255, 0.08))',
|
|
72
102
|
danger: 'var(--dsw-alias-state-error-primary, #ff7875)',
|
|
73
103
|
dangerHover: 'rgba(255, 120, 117, 0.14)',
|
|
74
104
|
avatarBg: 'var(--dsw-alias-bg-layer-2, rgba(255, 255, 255, 0.10))',
|
|
75
|
-
shadow: '0
|
|
105
|
+
shadow: '0 20px 40px -15px rgba(0, 0, 0, 0.7), 0 0 1px 1px rgba(255, 255, 255, 0.1)',
|
|
106
|
+
card: 'rgba(255, 255, 255, 0.03)',
|
|
107
|
+
cardBorder: 'rgba(255, 255, 255, 0.08)',
|
|
108
|
+
metric: 'rgba(0, 0, 0, 0.20)',
|
|
109
|
+
metricHover: 'rgba(0, 0, 0, 0.30)',
|
|
110
|
+
primary: '#e62e2d',
|
|
111
|
+
primaryHover: '#cc2423',
|
|
112
|
+
primarySoft: 'rgba(230, 46, 45, 0.10)',
|
|
113
|
+
track: 'rgba(255, 255, 255, 0.10)',
|
|
114
|
+
genGradient: 'linear-gradient(90deg, #3b82f6, #6366f1)',
|
|
115
|
+
ercGradient: 'linear-gradient(90deg, #a855f7, #ec4899)',
|
|
116
|
+
online: '#10b981',
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
const TRIGGER_BASE: CSSProperties = {
|
|
@@ -95,11 +136,11 @@ const TRIGGER_BASE: CSSProperties = {
|
|
|
95
136
|
const MENU_BASE: CSSProperties = {
|
|
96
137
|
position: 'fixed',
|
|
97
138
|
zIndex: 2147483000,
|
|
98
|
-
|
|
99
|
-
padding:
|
|
139
|
+
width: 320,
|
|
140
|
+
padding: 12,
|
|
100
141
|
borderWidth: 1,
|
|
101
142
|
borderStyle: 'solid',
|
|
102
|
-
borderRadius:
|
|
143
|
+
borderRadius: 16,
|
|
103
144
|
fontFamily: 'inherit',
|
|
104
145
|
fontSize: 13,
|
|
105
146
|
}
|
|
@@ -129,17 +170,21 @@ const MENU_ITEM_BASE: CSSProperties = {
|
|
|
129
170
|
|
|
130
171
|
/**
|
|
131
172
|
* One menu row. Hover is tracked in state: the client bundle ships no CSS
|
|
132
|
-
* file, so inline styles cannot express `:hover`.
|
|
173
|
+
* file, so inline styles cannot express `:hover`. `trailing` renders a
|
|
174
|
+
* chevron on the right (prototype nav rows), `danger` switches to the danger
|
|
175
|
+
* color scheme (prototype logout).
|
|
133
176
|
*/
|
|
134
177
|
function MenuItem({
|
|
135
178
|
label,
|
|
136
179
|
icon,
|
|
180
|
+
trailing,
|
|
137
181
|
danger,
|
|
138
182
|
palette,
|
|
139
183
|
onSelect,
|
|
140
184
|
}: {
|
|
141
185
|
label: string
|
|
142
186
|
icon: React.JSX.Element
|
|
187
|
+
trailing?: React.JSX.Element
|
|
143
188
|
danger?: boolean
|
|
144
189
|
palette: Palette
|
|
145
190
|
onSelect: () => void
|
|
@@ -151,6 +196,7 @@ function MenuItem({
|
|
|
151
196
|
role="menuitem"
|
|
152
197
|
style={{
|
|
153
198
|
...MENU_ITEM_BASE,
|
|
199
|
+
justifyContent: 'space-between',
|
|
154
200
|
color: danger ? palette.danger : palette.text,
|
|
155
201
|
background: hovered ? (danger ? palette.dangerHover : palette.hover) : 'transparent',
|
|
156
202
|
}}
|
|
@@ -158,8 +204,21 @@ function MenuItem({
|
|
|
158
204
|
onMouseLeave={() => setHovered(false)}
|
|
159
205
|
onClick={onSelect}
|
|
160
206
|
>
|
|
161
|
-
{
|
|
162
|
-
|
|
207
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 10, minWidth: 0 }}>
|
|
208
|
+
{icon}
|
|
209
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
|
|
210
|
+
</span>
|
|
211
|
+
{trailing ? (
|
|
212
|
+
<span
|
|
213
|
+
style={{
|
|
214
|
+
display: 'flex',
|
|
215
|
+
flex: '0 0 auto',
|
|
216
|
+
color: hovered ? (danger ? palette.danger : palette.text) : palette.muted,
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
{trailing}
|
|
220
|
+
</span>
|
|
221
|
+
) : null}
|
|
163
222
|
</button>
|
|
164
223
|
)
|
|
165
224
|
}
|
|
@@ -207,6 +266,308 @@ function LogoutIcon(): React.JSX.Element {
|
|
|
207
266
|
)
|
|
208
267
|
}
|
|
209
268
|
|
|
269
|
+
/** GEN metric icon (lucide `cpu`, blue like the prototype). */
|
|
270
|
+
function CpuIcon(): React.JSX.Element {
|
|
271
|
+
return (
|
|
272
|
+
<svg
|
|
273
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
274
|
+
width={13}
|
|
275
|
+
height={13}
|
|
276
|
+
viewBox="0 0 24 24"
|
|
277
|
+
fill="none"
|
|
278
|
+
stroke="#3b82f6"
|
|
279
|
+
strokeWidth={2}
|
|
280
|
+
strokeLinecap="round"
|
|
281
|
+
strokeLinejoin="round"
|
|
282
|
+
aria-hidden
|
|
283
|
+
style={{ flex: '0 0 auto' }}
|
|
284
|
+
>
|
|
285
|
+
<rect x="4" y="4" width="16" height="16" rx="2" />
|
|
286
|
+
<rect x="9" y="9" width="6" height="6" />
|
|
287
|
+
<path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2" />
|
|
288
|
+
</svg>
|
|
289
|
+
)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** ERC metric icon (lucide `waves`, purple like the prototype). */
|
|
293
|
+
function WaveIcon(): React.JSX.Element {
|
|
294
|
+
return (
|
|
295
|
+
<svg
|
|
296
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
297
|
+
width={13}
|
|
298
|
+
height={13}
|
|
299
|
+
viewBox="0 0 24 24"
|
|
300
|
+
fill="none"
|
|
301
|
+
stroke="#a855f7"
|
|
302
|
+
strokeWidth={2}
|
|
303
|
+
strokeLinecap="round"
|
|
304
|
+
strokeLinejoin="round"
|
|
305
|
+
aria-hidden
|
|
306
|
+
style={{ flex: '0 0 auto' }}
|
|
307
|
+
>
|
|
308
|
+
<path d="M2 12c1.5-3 3-3 4.5 0s3 3 4.5 0 3-3 4.5 0 3 3 4.5 0" />
|
|
309
|
+
<path d="M2 17c1.5-3 3-3 4.5 0s3 3 4.5 0 3-3 4.5 0 3 3 4.5 0" />
|
|
310
|
+
</svg>
|
|
311
|
+
)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Upgrade-button crown (solid amber like the prototype). */
|
|
315
|
+
function CrownIcon(): React.JSX.Element {
|
|
316
|
+
return (
|
|
317
|
+
<svg
|
|
318
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
319
|
+
width={14}
|
|
320
|
+
height={14}
|
|
321
|
+
viewBox="0 0 24 24"
|
|
322
|
+
fill="#fcd34d"
|
|
323
|
+
aria-hidden
|
|
324
|
+
style={{ flex: '0 0 auto' }}
|
|
325
|
+
>
|
|
326
|
+
<path d="M2 18h20l-1.5-9-5.5 4L12 5.5 9 13l-5.5-4L2 18z" />
|
|
327
|
+
</svg>
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Trailing chevron for nav rows (lucide `chevron-right`). */
|
|
332
|
+
function ChevronRightIcon(): React.JSX.Element {
|
|
333
|
+
return (
|
|
334
|
+
<svg
|
|
335
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
336
|
+
width={12}
|
|
337
|
+
height={12}
|
|
338
|
+
viewBox="0 0 24 24"
|
|
339
|
+
fill="none"
|
|
340
|
+
stroke="currentColor"
|
|
341
|
+
strokeWidth={2}
|
|
342
|
+
strokeLinecap="round"
|
|
343
|
+
strokeLinejoin="round"
|
|
344
|
+
aria-hidden
|
|
345
|
+
style={{ flex: '0 0 auto' }}
|
|
346
|
+
>
|
|
347
|
+
<polyline points="9 18 15 12 9 6" />
|
|
348
|
+
</svg>
|
|
349
|
+
)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** Quota metric card: icon + label, mono count and a gradient progress bar. */
|
|
353
|
+
function QuotaMetric({
|
|
354
|
+
label,
|
|
355
|
+
icon,
|
|
356
|
+
quota,
|
|
357
|
+
gradient,
|
|
358
|
+
palette,
|
|
359
|
+
}: {
|
|
360
|
+
label: string
|
|
361
|
+
icon: React.JSX.Element
|
|
362
|
+
quota: SubscriptionQuota | null
|
|
363
|
+
gradient: string
|
|
364
|
+
palette: Palette
|
|
365
|
+
}): React.JSX.Element {
|
|
366
|
+
const t = useT()
|
|
367
|
+
const [hovered, setHovered] = useState(false)
|
|
368
|
+
const used = quota?.currentQuota
|
|
369
|
+
const total = quota?.totalQuota
|
|
370
|
+
const percent = quota?.hasSubscription && typeof total === 'number' && total > 0
|
|
371
|
+
? Math.max(0, Math.min(100, Math.round(((used ?? 0) / total) * 100)))
|
|
372
|
+
: 0
|
|
373
|
+
const value = quota
|
|
374
|
+
? (quota.hasSubscription
|
|
375
|
+
? (typeof total === 'number'
|
|
376
|
+
? t('subscription.quotaCount', { used: used ?? 0, total })
|
|
377
|
+
: String(used ?? 0))
|
|
378
|
+
: t('subscription.unsubscribed'))
|
|
379
|
+
: '—'
|
|
380
|
+
return (
|
|
381
|
+
<div
|
|
382
|
+
role="presentation"
|
|
383
|
+
style={{
|
|
384
|
+
background: hovered ? palette.metricHover : palette.metric,
|
|
385
|
+
borderRadius: 12,
|
|
386
|
+
border: `1px solid ${palette.cardBorder}`,
|
|
387
|
+
padding: 10,
|
|
388
|
+
boxSizing: 'border-box',
|
|
389
|
+
transition: 'background 0.15s ease',
|
|
390
|
+
}}
|
|
391
|
+
onMouseEnter={() => setHovered(true)}
|
|
392
|
+
onMouseLeave={() => setHovered(false)}
|
|
393
|
+
>
|
|
394
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8, fontSize: 12, marginBottom: 6 }}>
|
|
395
|
+
<span style={{ display: 'flex', alignItems: 'center', gap: 6, fontWeight: 500, color: palette.text, minWidth: 0 }}>
|
|
396
|
+
{icon}
|
|
397
|
+
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{label}</span>
|
|
398
|
+
</span>
|
|
399
|
+
<span
|
|
400
|
+
style={{
|
|
401
|
+
fontWeight: 700,
|
|
402
|
+
color: quota?.hasSubscription ? palette.text : palette.danger,
|
|
403
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
|
|
404
|
+
fontSize: 12,
|
|
405
|
+
whiteSpace: 'nowrap',
|
|
406
|
+
flexShrink: 0,
|
|
407
|
+
}}
|
|
408
|
+
title={value}
|
|
409
|
+
>
|
|
410
|
+
{value}
|
|
411
|
+
</span>
|
|
412
|
+
</div>
|
|
413
|
+
<div style={{ width: '100%', height: 6, borderRadius: 999, background: palette.track, overflow: 'hidden' }}>
|
|
414
|
+
<div style={{ width: `${percent}%`, height: '100%', borderRadius: 999, background: gradient }} />
|
|
415
|
+
</div>
|
|
416
|
+
</div>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Profile header card (prototype): name + plan badge + phone line on the
|
|
422
|
+
* left, an "online" pill on the right. No redundant avatar — the trigger
|
|
423
|
+
* already shows it.
|
|
424
|
+
*/
|
|
425
|
+
function ProfileHeaderCard({
|
|
426
|
+
displayName,
|
|
427
|
+
packageName,
|
|
428
|
+
phone,
|
|
429
|
+
palette,
|
|
430
|
+
}: {
|
|
431
|
+
displayName: string
|
|
432
|
+
packageName: string | null
|
|
433
|
+
phone: string | null
|
|
434
|
+
palette: Palette
|
|
435
|
+
}): React.JSX.Element {
|
|
436
|
+
const t = useT()
|
|
437
|
+
return (
|
|
438
|
+
<div
|
|
439
|
+
role="presentation"
|
|
440
|
+
style={{
|
|
441
|
+
padding: 10,
|
|
442
|
+
borderRadius: 12,
|
|
443
|
+
background: palette.card,
|
|
444
|
+
border: `1px solid ${palette.cardBorder}`,
|
|
445
|
+
display: 'flex',
|
|
446
|
+
alignItems: 'center',
|
|
447
|
+
justifyContent: 'space-between',
|
|
448
|
+
gap: 8,
|
|
449
|
+
marginBottom: 12,
|
|
450
|
+
boxSizing: 'border-box',
|
|
451
|
+
}}
|
|
452
|
+
>
|
|
453
|
+
<div style={{ minWidth: 0, flex: '1 1 auto' }}>
|
|
454
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
|
|
455
|
+
<span
|
|
456
|
+
style={{
|
|
457
|
+
fontSize: 14,
|
|
458
|
+
fontWeight: 700,
|
|
459
|
+
color: palette.text,
|
|
460
|
+
overflow: 'hidden',
|
|
461
|
+
textOverflow: 'ellipsis',
|
|
462
|
+
whiteSpace: 'nowrap',
|
|
463
|
+
}}
|
|
464
|
+
>
|
|
465
|
+
{displayName}
|
|
466
|
+
</span>
|
|
467
|
+
{packageName ? (
|
|
468
|
+
<span
|
|
469
|
+
style={{
|
|
470
|
+
fontSize: 10,
|
|
471
|
+
background: palette.primarySoft,
|
|
472
|
+
color: palette.primary,
|
|
473
|
+
padding: '2px 6px',
|
|
474
|
+
borderRadius: 4,
|
|
475
|
+
fontWeight: 500,
|
|
476
|
+
border: `1px solid ${palette.primarySoft}`,
|
|
477
|
+
whiteSpace: 'nowrap',
|
|
478
|
+
flexShrink: 0,
|
|
479
|
+
}}
|
|
480
|
+
>
|
|
481
|
+
{packageName}
|
|
482
|
+
</span>
|
|
483
|
+
) : null}
|
|
484
|
+
</div>
|
|
485
|
+
{phone ? (
|
|
486
|
+
<p
|
|
487
|
+
style={{
|
|
488
|
+
margin: 0,
|
|
489
|
+
marginTop: 2,
|
|
490
|
+
fontSize: 12,
|
|
491
|
+
color: palette.muted,
|
|
492
|
+
overflow: 'hidden',
|
|
493
|
+
textOverflow: 'ellipsis',
|
|
494
|
+
whiteSpace: 'nowrap',
|
|
495
|
+
}}
|
|
496
|
+
>
|
|
497
|
+
{phone}
|
|
498
|
+
</p>
|
|
499
|
+
) : null}
|
|
500
|
+
</div>
|
|
501
|
+
<span
|
|
502
|
+
style={{
|
|
503
|
+
fontSize: 10,
|
|
504
|
+
background: 'rgba(16, 185, 129, 0.10)',
|
|
505
|
+
color: palette.online,
|
|
506
|
+
padding: '2px 8px',
|
|
507
|
+
borderRadius: 999,
|
|
508
|
+
fontWeight: 500,
|
|
509
|
+
border: '1px solid rgba(16, 185, 129, 0.20)',
|
|
510
|
+
display: 'flex',
|
|
511
|
+
alignItems: 'center',
|
|
512
|
+
gap: 5,
|
|
513
|
+
flexShrink: 0,
|
|
514
|
+
}}
|
|
515
|
+
>
|
|
516
|
+
<span style={{ width: 6, height: 6, borderRadius: 6, background: palette.online }} />
|
|
517
|
+
{t('subscription.online')}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Full-width red-gradient upgrade button (prototype CTA). */
|
|
524
|
+
function UpgradeButton({
|
|
525
|
+
subscribing,
|
|
526
|
+
palette,
|
|
527
|
+
onClick,
|
|
528
|
+
}: {
|
|
529
|
+
subscribing: boolean
|
|
530
|
+
palette: Palette
|
|
531
|
+
onClick: () => void
|
|
532
|
+
}): React.JSX.Element {
|
|
533
|
+
const t = useT()
|
|
534
|
+
const [hovered, setHovered] = useState(false)
|
|
535
|
+
return (
|
|
536
|
+
<button
|
|
537
|
+
type="button"
|
|
538
|
+
style={{
|
|
539
|
+
width: '100%',
|
|
540
|
+
marginTop: 10,
|
|
541
|
+
padding: '9px 12px',
|
|
542
|
+
borderRadius: 12,
|
|
543
|
+
border: 'none',
|
|
544
|
+
background: subscribing
|
|
545
|
+
? `linear-gradient(90deg, ${palette.primary}, #e11d48)`
|
|
546
|
+
: hovered
|
|
547
|
+
? `linear-gradient(90deg, ${palette.primaryHover}, #be123c)`
|
|
548
|
+
: `linear-gradient(90deg, ${palette.primary}, #e11d48)`,
|
|
549
|
+
color: '#ffffff',
|
|
550
|
+
fontSize: 12,
|
|
551
|
+
fontWeight: 600,
|
|
552
|
+
display: 'flex',
|
|
553
|
+
alignItems: 'center',
|
|
554
|
+
justifyContent: 'center',
|
|
555
|
+
gap: 8,
|
|
556
|
+
cursor: subscribing ? 'default' : 'pointer',
|
|
557
|
+
opacity: subscribing ? 0.72 : 1,
|
|
558
|
+
boxShadow: '0 8px 20px rgba(230, 46, 45, 0.22)',
|
|
559
|
+
transition: 'background 0.15s ease, opacity 0.15s ease',
|
|
560
|
+
}}
|
|
561
|
+
onMouseEnter={() => setHovered(true)}
|
|
562
|
+
onMouseLeave={() => setHovered(false)}
|
|
563
|
+
onClick={onClick}
|
|
564
|
+
>
|
|
565
|
+
<CrownIcon />
|
|
566
|
+
{subscribing ? t('subscription.subscribing') : t('subscription.upgrade')}
|
|
567
|
+
</button>
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
210
571
|
export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ wide }: SidebarFooterActionOwnerProps): React.JSX.Element | null {
|
|
211
572
|
const authState = useSyncExternalStore(subscribeAuth, getAuthState)
|
|
212
573
|
const auth = getAuth()
|
|
@@ -217,6 +578,8 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
|
|
|
217
578
|
const [menuStyle, setMenuStyle] = useState<CSSProperties | null>(null)
|
|
218
579
|
const [avatarBroken, setAvatarBroken] = useState(false)
|
|
219
580
|
const [hovered, setHovered] = useState(false)
|
|
581
|
+
const [quota, setQuota] = useState<{ gen: SubscriptionQuota | null; erc: SubscriptionQuota | null } | null>(null)
|
|
582
|
+
const [subscribing, setSubscribing] = useState(false)
|
|
220
583
|
const triggerRef = useRef<HTMLButtonElement | null>(null)
|
|
221
584
|
const menuRef = useRef<HTMLDivElement | null>(null)
|
|
222
585
|
|
|
@@ -270,8 +633,29 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
|
|
|
270
633
|
if (!authenticated) setMenuOpen(false)
|
|
271
634
|
}, [authenticated])
|
|
272
635
|
|
|
636
|
+
// Live quota: EVERY time the menu opens (and every time the auth state
|
|
637
|
+
// flips to authenticated) re-fetch the active subscription for GEN and ERC
|
|
638
|
+
// DIRECTLY from eda.cn — the sidebar must never show a stale count.
|
|
639
|
+
useEffect(() => {
|
|
640
|
+
if (!menuOpen || !authenticated) {
|
|
641
|
+
if (!menuOpen) setQuota(null)
|
|
642
|
+
return
|
|
643
|
+
}
|
|
644
|
+
let cancelled = false
|
|
645
|
+
void Promise.all([
|
|
646
|
+
auth?.getSubscriptionQuota?.(GEN_PRODUCT_CODE).catch(() => null) ?? Promise.resolve(null),
|
|
647
|
+
auth?.getSubscriptionQuota?.(ERC_PRODUCT_CODE).catch(() => null) ?? Promise.resolve(null),
|
|
648
|
+
]).then(([gen, erc]) => {
|
|
649
|
+
if (!cancelled) setQuota({ gen, erc })
|
|
650
|
+
})
|
|
651
|
+
return () => { cancelled = true }
|
|
652
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
653
|
+
}, [menuOpen, authenticated])
|
|
654
|
+
|
|
273
655
|
// Anchor the portalled menu to the trigger before paint: the sidebar footer
|
|
274
656
|
// sits at the bottom edge, so the menu grows UPWARD from the trigger's top.
|
|
657
|
+
// Width is FIXED (320, prototype `w-80`) — the sidebar footer is narrow, so
|
|
658
|
+
// the popup intentionally overhangs it instead of collapsing to its width.
|
|
275
659
|
useLayoutEffect(() => {
|
|
276
660
|
if (!menuOpen || !triggerRef.current) return
|
|
277
661
|
const rect = triggerRef.current.getBoundingClientRect()
|
|
@@ -283,9 +667,8 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
|
|
|
283
667
|
boxShadow: palette.shadow,
|
|
284
668
|
left: Math.max(8, Math.round(rect.left)),
|
|
285
669
|
bottom: Math.max(8, Math.round(window.innerHeight - rect.top + 8)),
|
|
286
|
-
...(wide ? { width: Math.round(rect.width) } : {}),
|
|
287
670
|
})
|
|
288
|
-
}, [menuOpen,
|
|
671
|
+
}, [menuOpen, avatar, palette])
|
|
289
672
|
|
|
290
673
|
// Close on: outside click, Escape, resize or scroll (the anchor moved).
|
|
291
674
|
useEffect(() => {
|
|
@@ -340,6 +723,19 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
|
|
|
340
723
|
const title = authenticated ? t('sidebar.accountTitle') : t('sidebar.loginTitle')
|
|
341
724
|
const triggerBackground = menuOpen || hovered ? palette.hover : 'transparent'
|
|
342
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Open the subscription page. Host mode → the EDA host's native dialog
|
|
728
|
+
* (DialogService.OpenUrl, preferred size 1400×740); standalone DSH → the
|
|
729
|
+
* in-app iframe dialog at the same preferred size. No credential → no-op.
|
|
730
|
+
*/
|
|
731
|
+
const openSubscription = (): void => {
|
|
732
|
+
if (subscribing) return
|
|
733
|
+
setSubscribing(true)
|
|
734
|
+
void auth.openSubscriptionDialog()
|
|
735
|
+
.catch(() => { /* host without dialog service — menu stays usable */ })
|
|
736
|
+
.finally(() => setSubscribing(false))
|
|
737
|
+
}
|
|
738
|
+
|
|
343
739
|
return (
|
|
344
740
|
<div style={{ position: 'relative', width: '100%' }}>
|
|
345
741
|
<button
|
|
@@ -401,14 +797,75 @@ export const HuaqiuAuthSidebarAction = memo(function HuaqiuAuthSidebarAction({ w
|
|
|
401
797
|
{menuOpen && menuStyle
|
|
402
798
|
? createPortal(
|
|
403
799
|
<div ref={menuRef} role="menu" style={menuStyle}>
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
label={t('menu.profile')}
|
|
409
|
-
icon={<UserIcon />}
|
|
800
|
+
<ProfileHeaderCard
|
|
801
|
+
displayName={displayName ?? t('sidebar.account')}
|
|
802
|
+
packageName={quota?.gen?.packageName ?? quota?.erc?.packageName ?? null}
|
|
803
|
+
phone={authState.phone ?? null}
|
|
410
804
|
palette={palette}
|
|
411
|
-
|
|
805
|
+
/>
|
|
806
|
+
<div
|
|
807
|
+
role="presentation"
|
|
808
|
+
style={{
|
|
809
|
+
...MENU_HEADER_BASE,
|
|
810
|
+
paddingTop: 0,
|
|
811
|
+
paddingBottom: 6,
|
|
812
|
+
fontSize: 11,
|
|
813
|
+
fontWeight: 600,
|
|
814
|
+
letterSpacing: 0.4,
|
|
815
|
+
textTransform: 'uppercase',
|
|
816
|
+
color: palette.muted,
|
|
817
|
+
}}
|
|
818
|
+
>
|
|
819
|
+
{t('subscription.quotaHeader')}
|
|
820
|
+
</div>
|
|
821
|
+
<div role="presentation" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
822
|
+
<QuotaMetric
|
|
823
|
+
label={t('subscription.gen')}
|
|
824
|
+
icon={<CpuIcon />}
|
|
825
|
+
quota={quota?.gen ?? null}
|
|
826
|
+
gradient={palette.genGradient}
|
|
827
|
+
palette={palette}
|
|
828
|
+
/>
|
|
829
|
+
<QuotaMetric
|
|
830
|
+
label={t('subscription.erc')}
|
|
831
|
+
icon={<WaveIcon />}
|
|
832
|
+
quota={quota?.erc ?? null}
|
|
833
|
+
gradient={palette.ercGradient}
|
|
834
|
+
palette={palette}
|
|
835
|
+
/>
|
|
836
|
+
</div>
|
|
837
|
+
<UpgradeButton
|
|
838
|
+
subscribing={subscribing}
|
|
839
|
+
palette={palette}
|
|
840
|
+
onClick={() => {
|
|
841
|
+
if (subscribing) return
|
|
842
|
+
openSubscription()
|
|
843
|
+
}}
|
|
844
|
+
/>
|
|
845
|
+
<div
|
|
846
|
+
role="presentation"
|
|
847
|
+
style={{
|
|
848
|
+
height: 1,
|
|
849
|
+
margin: '12px 2px',
|
|
850
|
+
background: palette.border,
|
|
851
|
+
}}
|
|
852
|
+
/>
|
|
853
|
+
<div role="presentation" style={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
854
|
+
<MenuItem
|
|
855
|
+
label={t('menu.profile')}
|
|
856
|
+
icon={<UserIcon />}
|
|
857
|
+
trailing={<ChevronRightIcon />}
|
|
858
|
+
palette={palette}
|
|
859
|
+
onSelect={openProfile}
|
|
860
|
+
/>
|
|
861
|
+
</div>
|
|
862
|
+
<div
|
|
863
|
+
role="presentation"
|
|
864
|
+
style={{
|
|
865
|
+
height: 1,
|
|
866
|
+
margin: '12px 2px',
|
|
867
|
+
background: palette.border,
|
|
868
|
+
}}
|
|
412
869
|
/>
|
|
413
870
|
<MenuItem
|
|
414
871
|
label={t('menu.logout')}
|