@ruzhiai/runid-react 0.1.0 → 0.1.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/index.d.ts +2 -1
- package/dist/index.js +412 -60
- package/package.json +2 -2
- package/src/index.tsx +696 -138
package/src/index.tsx
CHANGED
|
@@ -4,30 +4,32 @@ import {
|
|
|
4
4
|
useState,
|
|
5
5
|
useCallback,
|
|
6
6
|
useEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
type CSSProperties,
|
|
8
9
|
type ReactNode,
|
|
9
10
|
} from 'react';
|
|
10
11
|
|
|
11
12
|
import {
|
|
12
|
-
Tabs,
|
|
13
13
|
Button,
|
|
14
|
+
Flex,
|
|
14
15
|
Input,
|
|
16
|
+
Select,
|
|
15
17
|
Space,
|
|
16
18
|
Checkbox,
|
|
17
19
|
Modal,
|
|
18
20
|
Typography,
|
|
19
21
|
Spin,
|
|
20
22
|
message,
|
|
21
|
-
|
|
23
|
+
Tooltip,
|
|
22
24
|
} from 'antd';
|
|
23
25
|
import {
|
|
26
|
+
ArrowRightOutlined,
|
|
24
27
|
PhoneOutlined,
|
|
25
28
|
MailOutlined,
|
|
26
29
|
MessageOutlined,
|
|
27
30
|
SafetyOutlined,
|
|
28
31
|
WechatOutlined,
|
|
29
32
|
UserOutlined,
|
|
30
|
-
SendOutlined,
|
|
31
33
|
} from '@ant-design/icons';
|
|
32
34
|
|
|
33
35
|
import {
|
|
@@ -54,6 +56,34 @@ export * from '@ruzhiai/runid-sdk';
|
|
|
54
56
|
|
|
55
57
|
const { Text, Title, Paragraph } = Typography;
|
|
56
58
|
|
|
59
|
+
function getBrowserUserAgent(): string {
|
|
60
|
+
if (typeof navigator === 'undefined') return '';
|
|
61
|
+
return navigator.userAgent || '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isWecomEmbeddedBrowser(): boolean {
|
|
65
|
+
return /wxwork/i.test(getBrowserUserAgent());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isMobileBrowser(): boolean {
|
|
69
|
+
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(getBrowserUserAgent());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pickWecomAutoRedirectURL(urls: WecomAuthURLs): string | undefined {
|
|
73
|
+
if (!isWecomEmbeddedBrowser()) return undefined;
|
|
74
|
+
return urls.mobile_auth_url ?? urls.app_auth_url ?? urls.auth_url;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function pickWecomBrowserURL(urls: WecomAuthURLs): string | undefined {
|
|
78
|
+
if (isWecomEmbeddedBrowser()) {
|
|
79
|
+
return urls.mobile_auth_url ?? urls.app_auth_url ?? urls.auth_url;
|
|
80
|
+
}
|
|
81
|
+
if (isMobileBrowser()) {
|
|
82
|
+
return urls.app_auth_url ?? urls.mobile_auth_url ?? urls.auth_url;
|
|
83
|
+
}
|
|
84
|
+
return urls.auth_url;
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
interface RunIDAuthContextValue {
|
|
58
88
|
isAuthenticated: boolean;
|
|
59
89
|
isLoading: boolean;
|
|
@@ -271,6 +301,7 @@ export interface RunIDLoginWidgetProps {
|
|
|
271
301
|
returnUrl?: string;
|
|
272
302
|
defaultMethod?: AuthMethod;
|
|
273
303
|
locale?: 'zh-CN' | 'en-US';
|
|
304
|
+
theme?: 'light' | 'dark';
|
|
274
305
|
onSuccess: (response: LoginResponse) => void | Promise<void>;
|
|
275
306
|
onError?: (error: RunIDAuthError) => void;
|
|
276
307
|
onOAuthRedirect?: (method: 'wechat' | 'wecom', url: string) => void;
|
|
@@ -278,6 +309,69 @@ export interface RunIDLoginWidgetProps {
|
|
|
278
309
|
style?: CSSProperties;
|
|
279
310
|
}
|
|
280
311
|
|
|
312
|
+
type ThemeTokens = {
|
|
313
|
+
surface: string;
|
|
314
|
+
surfaceBorder: string;
|
|
315
|
+
text: string;
|
|
316
|
+
textSecondary: string;
|
|
317
|
+
inputIcon: string;
|
|
318
|
+
noticeBg: string;
|
|
319
|
+
noticeBorder: string;
|
|
320
|
+
noticeText: string;
|
|
321
|
+
qrBg: string;
|
|
322
|
+
activeTabColor: string;
|
|
323
|
+
inactiveTabColor: string;
|
|
324
|
+
activeTabShadow: string;
|
|
325
|
+
buttonShadow: string;
|
|
326
|
+
socialBtnColor: string;
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const THEME: Record<'light' | 'dark', ThemeTokens> = {
|
|
330
|
+
light: {
|
|
331
|
+
surface: '#f5f7fa',
|
|
332
|
+
surfaceBorder: 'transparent',
|
|
333
|
+
text: '#0f1629',
|
|
334
|
+
textSecondary: '#6b7aa0',
|
|
335
|
+
inputIcon: '#94a0bf',
|
|
336
|
+
noticeBg: '#fff8e1',
|
|
337
|
+
noticeBorder: '#ffe082',
|
|
338
|
+
noticeText: '#8d6e00',
|
|
339
|
+
qrBg: '#fff',
|
|
340
|
+
activeTabColor: '#fff',
|
|
341
|
+
inactiveTabColor: '#6b7aa0',
|
|
342
|
+
activeTabShadow: '0 2px 8px rgba(15,22,41,0.08)',
|
|
343
|
+
buttonShadow: '0 8px 20px rgba(99, 102, 241, 0.2)',
|
|
344
|
+
socialBtnColor: '#6b7aa0',
|
|
345
|
+
},
|
|
346
|
+
dark: {
|
|
347
|
+
surface: 'rgba(255,255,255,0.06)',
|
|
348
|
+
surfaceBorder: 'rgba(255,255,255,0.08)',
|
|
349
|
+
text: '#f4f4f5',
|
|
350
|
+
textSecondary: '#a1a1aa',
|
|
351
|
+
inputIcon: '#71717a',
|
|
352
|
+
noticeBg: 'rgba(234,179,8,0.1)',
|
|
353
|
+
noticeBorder: 'rgba(234,179,8,0.25)',
|
|
354
|
+
noticeText: '#fbbf24',
|
|
355
|
+
qrBg: '#18181b',
|
|
356
|
+
activeTabColor: '#fff',
|
|
357
|
+
inactiveTabColor: '#a1a1aa',
|
|
358
|
+
activeTabShadow: '0 2px 8px rgba(0,0,0,0.3)',
|
|
359
|
+
buttonShadow: 'none',
|
|
360
|
+
socialBtnColor: '#a1a1aa',
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
function resolveTheme(explicit?: 'light' | 'dark'): 'light' | 'dark' {
|
|
365
|
+
if (explicit) return explicit;
|
|
366
|
+
if (typeof document !== 'undefined') {
|
|
367
|
+
const htmlTheme = document.documentElement.getAttribute('data-rd-theme')
|
|
368
|
+
|| document.documentElement.getAttribute('data-theme');
|
|
369
|
+
if (htmlTheme === 'dark') return 'dark';
|
|
370
|
+
if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) return 'dark';
|
|
371
|
+
}
|
|
372
|
+
return 'light';
|
|
373
|
+
}
|
|
374
|
+
|
|
281
375
|
type CountdownState = {
|
|
282
376
|
[key in 'phone' | 'email' | 'whatsapp']: number;
|
|
283
377
|
};
|
|
@@ -307,7 +401,14 @@ const TEXT: Record<'zh-CN' | 'en-US', Record<string, string>> = {
|
|
|
307
401
|
emailPlaceholder: '请输入邮箱',
|
|
308
402
|
whatsappPlaceholder: '请输入 WhatsApp 号码',
|
|
309
403
|
codePlaceholder: '请输入验证码',
|
|
404
|
+
invalidPhone: '请输入正确的手机号',
|
|
405
|
+
invalidEmail: '请输入正确的邮箱地址',
|
|
406
|
+
invalidWhatsapp: '请输入正确的 WhatsApp 号码',
|
|
407
|
+
requiredCode: '请输入验证码',
|
|
408
|
+
sendCodeFailed: '发送验证码失败',
|
|
409
|
+
loginFailed: '登录失败',
|
|
310
410
|
mfaHint: '该账号已开启二次验证,请完成下一步认证。',
|
|
411
|
+
methodNotConfigured: '该登录方式未配置,暂不可用',
|
|
311
412
|
},
|
|
312
413
|
'en-US': {
|
|
313
414
|
wechat: 'WeChat',
|
|
@@ -333,7 +434,14 @@ const TEXT: Record<'zh-CN' | 'en-US', Record<string, string>> = {
|
|
|
333
434
|
emailPlaceholder: 'Enter email',
|
|
334
435
|
whatsappPlaceholder: 'Enter WhatsApp number',
|
|
335
436
|
codePlaceholder: 'Enter verification code',
|
|
437
|
+
invalidPhone: 'Enter a valid phone number',
|
|
438
|
+
invalidEmail: 'Enter a valid email address',
|
|
439
|
+
invalidWhatsapp: 'Enter a valid WhatsApp number',
|
|
440
|
+
requiredCode: 'Enter verification code',
|
|
441
|
+
sendCodeFailed: 'Failed to send code',
|
|
442
|
+
loginFailed: 'Sign-in failed',
|
|
336
443
|
mfaHint: 'MFA is required before login. Please complete second-factor verification.',
|
|
444
|
+
methodNotConfigured: 'This login method is not configured',
|
|
337
445
|
},
|
|
338
446
|
};
|
|
339
447
|
|
|
@@ -345,11 +453,11 @@ function resolveInitialMethod(config: LoginConfig, requested?: AuthMethod): Auth
|
|
|
345
453
|
if (requested && isAllowedMethod(config, requested)) {
|
|
346
454
|
return requested;
|
|
347
455
|
}
|
|
456
|
+
if (config.client.allowed_methods.includes('wecom')) return 'wecom';
|
|
348
457
|
if (config.client.allowed_methods.includes('phone')) return 'phone';
|
|
349
458
|
if (config.client.allowed_methods.includes('email')) return 'email';
|
|
350
|
-
if (config.client.allowed_methods.includes('
|
|
351
|
-
|
|
352
|
-
return 'whatsapp';
|
|
459
|
+
if (config.client.allowed_methods.includes('whatsapp')) return 'whatsapp';
|
|
460
|
+
return 'wechat';
|
|
353
461
|
}
|
|
354
462
|
|
|
355
463
|
function isMfaResponse(response: unknown): response is { mfa_required: boolean; account_id: string } {
|
|
@@ -360,6 +468,47 @@ function isMfaResponse(response: unknown): response is { mfa_required: boolean;
|
|
|
360
468
|
);
|
|
361
469
|
}
|
|
362
470
|
|
|
471
|
+
function phoneDigits(value: string): string {
|
|
472
|
+
return value.replace(/\D/g, '');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function normalizeCountryPhone(countryCode: string, value: string): string {
|
|
476
|
+
return `${countryCode}${phoneDigits(value)}`;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function normalizeInternationalPhone(value: string): string {
|
|
480
|
+
const trimmed = value.trim();
|
|
481
|
+
const digits = phoneDigits(trimmed);
|
|
482
|
+
return trimmed.startsWith('+') ? `+${digits}` : digits;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function isValidCountryPhone(countryCode: string, value: string): boolean {
|
|
486
|
+
const digits = phoneDigits(value);
|
|
487
|
+
if (!digits) return false;
|
|
488
|
+
if (countryCode === '+86') return /^1\d{10}$/.test(digits);
|
|
489
|
+
if (countryCode === '+1') return digits.length === 10;
|
|
490
|
+
if (countryCode === '+44') return digits.length >= 10 && digits.length <= 11;
|
|
491
|
+
if (countryCode === '+81') return digits.length >= 10 && digits.length <= 11;
|
|
492
|
+
if (countryCode === '+65') return digits.length === 8;
|
|
493
|
+
return digits.length >= 6 && digits.length <= 20;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function isValidInternationalPhone(value: string): boolean {
|
|
497
|
+
const digits = phoneDigits(value);
|
|
498
|
+
return digits.length >= 6 && digits.length <= 20;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isValidEmail(value: string): boolean {
|
|
502
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
type LoginMethodOption = {
|
|
506
|
+
key: AuthMethod;
|
|
507
|
+
visible: boolean;
|
|
508
|
+
icon: JSX.Element;
|
|
509
|
+
label: string;
|
|
510
|
+
};
|
|
511
|
+
|
|
363
512
|
const initialCountdown: CountdownState = {
|
|
364
513
|
phone: 0,
|
|
365
514
|
email: 0,
|
|
@@ -371,6 +520,7 @@ export function RunIDLoginWidget({
|
|
|
371
520
|
returnUrl,
|
|
372
521
|
defaultMethod,
|
|
373
522
|
locale = 'zh-CN',
|
|
523
|
+
theme: themeProp,
|
|
374
524
|
onSuccess,
|
|
375
525
|
onError,
|
|
376
526
|
onOAuthRedirect,
|
|
@@ -378,33 +528,17 @@ export function RunIDLoginWidget({
|
|
|
378
528
|
style,
|
|
379
529
|
}: RunIDLoginWidgetProps) {
|
|
380
530
|
const [messageApi, contextHolder] = message.useMessage();
|
|
381
|
-
const
|
|
531
|
+
const t = THEME[resolveTheme(themeProp)];
|
|
532
|
+
const auth = useMemo(() => new RunIDAuth({
|
|
382
533
|
...config,
|
|
383
534
|
defaultReturnURL: returnUrl,
|
|
384
535
|
onAuthSuccess: () => {},
|
|
385
536
|
onAuthError: (error: RunIDAuthError) => {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
messageApi.error(msg);
|
|
537
|
+
if (error.status !== 401) {
|
|
538
|
+
onError?.(error);
|
|
389
539
|
}
|
|
390
|
-
onError?.(error);
|
|
391
540
|
},
|
|
392
|
-
}));
|
|
393
|
-
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
setAuth(new RunIDAuth({
|
|
396
|
-
...config,
|
|
397
|
-
defaultReturnURL: returnUrl,
|
|
398
|
-
onAuthSuccess: () => {},
|
|
399
|
-
onAuthError: (error: RunIDAuthError) => {
|
|
400
|
-
const msg = parseAuthErrorMessage(error.payload) || error.message;
|
|
401
|
-
if (msg) {
|
|
402
|
-
messageApi.error(msg);
|
|
403
|
-
}
|
|
404
|
-
onError?.(error);
|
|
405
|
-
},
|
|
406
|
-
}));
|
|
407
|
-
}, [config.baseURL, config.client_id, messageApi, onError, returnUrl]);
|
|
541
|
+
}), [config.issuer, config.apiBaseURL, config.baseURL, config.client_id, onError, returnUrl]);
|
|
408
542
|
|
|
409
543
|
const texts = TEXT[locale];
|
|
410
544
|
const [loginConfig, setLoginConfig] = useState<LoginConfig | null>(null);
|
|
@@ -430,6 +564,10 @@ export function RunIDLoginWidget({
|
|
|
430
564
|
const [consentData, setConsentData] = useState<ConsentRequiredResponse | null>(null);
|
|
431
565
|
const [agreed, setAgreed] = useState(false);
|
|
432
566
|
const [processingConsent, setProcessingConsent] = useState(false);
|
|
567
|
+
const [wecomURLs, setWecomURLs] = useState<WecomAuthURLs | null>(null);
|
|
568
|
+
const [loadingWecomQR, setLoadingWecomQR] = useState(false);
|
|
569
|
+
const [showWecomQR, setShowWecomQR] = useState(false);
|
|
570
|
+
const [wecomAutoRedirecting, setWecomAutoRedirecting] = useState(false);
|
|
433
571
|
|
|
434
572
|
useEffect(() => {
|
|
435
573
|
let active = true;
|
|
@@ -463,13 +601,11 @@ export function RunIDLoginWidget({
|
|
|
463
601
|
};
|
|
464
602
|
}, [auth, defaultMethod, onError, returnUrl, messageApi, texts.requiredMethodError]);
|
|
465
603
|
|
|
466
|
-
const
|
|
604
|
+
const primaryMethods = ([
|
|
605
|
+
{ key: 'wecom' as const, visible: isAllowedMethod(loginConfig, 'wecom'), icon: <UserOutlined />, label: texts.wecom },
|
|
467
606
|
{ key: 'phone' as const, visible: isAllowedMethod(loginConfig, 'phone'), icon: <PhoneOutlined />, label: texts.phone },
|
|
468
607
|
{ key: 'email' as const, visible: isAllowedMethod(loginConfig, 'email'), icon: <MailOutlined />, label: texts.email },
|
|
469
|
-
|
|
470
|
-
{ key: 'wecom' as const, visible: isAllowedMethod(loginConfig, 'wecom'), icon: <UserOutlined />, label: texts.wecom },
|
|
471
|
-
{ key: 'whatsapp' as const, visible: isAllowedMethod(loginConfig, 'whatsapp'), icon: <MessageOutlined />, label: texts.whatsapp },
|
|
472
|
-
].filter((item): item is { key: AuthMethod; visible: true; icon: JSX.Element; label: string } => item.visible);
|
|
608
|
+
] satisfies LoginMethodOption[]).filter((item) => item.visible);
|
|
473
609
|
|
|
474
610
|
const setCountDown = useCallback((method: keyof CountdownState, seconds: number) => {
|
|
475
611
|
setCountdown((prev) => ({ ...prev, [method]: seconds }));
|
|
@@ -500,30 +636,42 @@ export function RunIDLoginWidget({
|
|
|
500
636
|
const targetWhatsapp = whatsapp.trim();
|
|
501
637
|
setIsSending((prev) => ({ ...prev, [method]: true }));
|
|
502
638
|
if (method === 'phone') {
|
|
503
|
-
|
|
504
|
-
if (!target) {
|
|
639
|
+
if (!targetPhone) {
|
|
505
640
|
messageApi.error(texts.phonePlaceholder);
|
|
506
641
|
return;
|
|
507
642
|
}
|
|
643
|
+
if (!isValidCountryPhone(countryCode, targetPhone)) {
|
|
644
|
+
messageApi.error(texts.invalidPhone);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const target = normalizeCountryPhone(countryCode, targetPhone);
|
|
508
648
|
await auth.sendPhoneCode(target);
|
|
509
649
|
} else if (method === 'email') {
|
|
510
|
-
if (!targetEmail
|
|
650
|
+
if (!targetEmail) {
|
|
511
651
|
messageApi.error(texts.emailPlaceholder);
|
|
512
652
|
return;
|
|
513
653
|
}
|
|
654
|
+
if (!isValidEmail(targetEmail)) {
|
|
655
|
+
messageApi.error(texts.invalidEmail);
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
514
658
|
await auth.sendEmailCode(targetEmail);
|
|
515
659
|
} else {
|
|
516
660
|
if (!targetWhatsapp) {
|
|
517
661
|
messageApi.error(texts.whatsappPlaceholder);
|
|
518
662
|
return;
|
|
519
663
|
}
|
|
520
|
-
|
|
664
|
+
if (!isValidInternationalPhone(targetWhatsapp)) {
|
|
665
|
+
messageApi.error(texts.invalidWhatsapp);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
await auth.sendWhatsAppCode(normalizeInternationalPhone(targetWhatsapp));
|
|
521
669
|
}
|
|
522
670
|
messageApi.success(texts.sendCode);
|
|
523
671
|
startTimer(method);
|
|
524
672
|
} catch (error) {
|
|
525
673
|
const runErr = error as RunIDAuthError;
|
|
526
|
-
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message ||
|
|
674
|
+
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || texts.sendCodeFailed;
|
|
527
675
|
messageApi.error(msg);
|
|
528
676
|
onError?.(runErr);
|
|
529
677
|
} finally {
|
|
@@ -536,10 +684,23 @@ export function RunIDLoginWidget({
|
|
|
536
684
|
const openOAuth = useCallback(
|
|
537
685
|
async (method: 'wechat' | 'wecom') => {
|
|
538
686
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
687
|
+
if (method === 'wechat') {
|
|
688
|
+
const urls = await auth.getWechatAuthURL(returnUrl);
|
|
689
|
+
const target = urls.auth_url;
|
|
690
|
+
if (onOAuthRedirect) {
|
|
691
|
+
onOAuthRedirect(method, target);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
window.location.assign(target);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const urls = await auth.getWecomAuthURL(returnUrl);
|
|
699
|
+
const target = pickWecomBrowserURL(urls);
|
|
700
|
+
if (!target) {
|
|
701
|
+
messageApi.error(locale === 'zh-CN' ? '企业微信登录地址不可用' : 'WeCom sign-in URL is unavailable');
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
543
704
|
if (onOAuthRedirect) {
|
|
544
705
|
onOAuthRedirect(method, target);
|
|
545
706
|
return;
|
|
@@ -552,9 +713,128 @@ export function RunIDLoginWidget({
|
|
|
552
713
|
onError?.(runErr);
|
|
553
714
|
}
|
|
554
715
|
},
|
|
555
|
-
[auth, onError, onOAuthRedirect, returnUrl
|
|
716
|
+
[auth, locale, messageApi, onError, onOAuthRedirect, returnUrl]
|
|
556
717
|
);
|
|
557
718
|
|
|
719
|
+
const openWecomLogin = useCallback(async () => {
|
|
720
|
+
try {
|
|
721
|
+
const urls = wecomURLs ?? await auth.getWecomAuthURL(returnUrl);
|
|
722
|
+
if (!wecomURLs) {
|
|
723
|
+
setWecomURLs(urls);
|
|
724
|
+
}
|
|
725
|
+
const target = pickWecomBrowserURL(urls);
|
|
726
|
+
if (!target) {
|
|
727
|
+
messageApi.error(locale === 'zh-CN' ? '企业微信登录地址不可用' : 'WeCom sign-in URL is unavailable');
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (onOAuthRedirect) {
|
|
731
|
+
onOAuthRedirect('wecom', target);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
window.location.assign(target);
|
|
735
|
+
} catch (error) {
|
|
736
|
+
const runErr = error as RunIDAuthError;
|
|
737
|
+
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信登录地址失败';
|
|
738
|
+
messageApi.error(msg);
|
|
739
|
+
onError?.(runErr);
|
|
740
|
+
}
|
|
741
|
+
}, [auth, locale, messageApi, onError, onOAuthRedirect, returnUrl, wecomURLs]);
|
|
742
|
+
|
|
743
|
+
const loadWecomQR = useCallback(async () => {
|
|
744
|
+
try {
|
|
745
|
+
setLoadingWecomQR(true);
|
|
746
|
+
const urls = await auth.getWecomAuthURL(returnUrl);
|
|
747
|
+
setWecomURLs(urls);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
const runErr = error as RunIDAuthError;
|
|
750
|
+
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信二维码失败';
|
|
751
|
+
messageApi.error(msg);
|
|
752
|
+
onError?.(runErr);
|
|
753
|
+
} finally {
|
|
754
|
+
setLoadingWecomQR(false);
|
|
755
|
+
}
|
|
756
|
+
}, [auth, messageApi, onError, returnUrl]);
|
|
757
|
+
|
|
758
|
+
useEffect(() => {
|
|
759
|
+
if (!loginConfig || activeMethod !== 'wecom' || !isAllowedMethod(loginConfig, 'wecom')) {
|
|
760
|
+
setWecomAutoRedirecting(false);
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if (!isWecomEmbeddedBrowser()) {
|
|
764
|
+
setWecomAutoRedirecting(false);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
let cancelled = false;
|
|
768
|
+
setWecomAutoRedirecting(true);
|
|
769
|
+
void (async () => {
|
|
770
|
+
try {
|
|
771
|
+
const urls = await auth.getWecomAuthURL(returnUrl);
|
|
772
|
+
if (cancelled) return;
|
|
773
|
+
setWecomURLs(urls);
|
|
774
|
+
const redirect = pickWecomAutoRedirectURL(urls);
|
|
775
|
+
if (redirect) {
|
|
776
|
+
window.location.assign(redirect);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
setWecomAutoRedirecting(false);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (cancelled) return;
|
|
782
|
+
const runErr = error as RunIDAuthError;
|
|
783
|
+
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信登录地址失败';
|
|
784
|
+
messageApi.error(msg);
|
|
785
|
+
onError?.(runErr);
|
|
786
|
+
setWecomAutoRedirecting(false);
|
|
787
|
+
}
|
|
788
|
+
})();
|
|
789
|
+
return () => {
|
|
790
|
+
cancelled = true;
|
|
791
|
+
};
|
|
792
|
+
}, [activeMethod, auth, loginConfig, messageApi, onError, returnUrl]);
|
|
793
|
+
|
|
794
|
+
useEffect(() => {
|
|
795
|
+
if (activeMethod !== 'wecom') {
|
|
796
|
+
setShowWecomQR(false);
|
|
797
|
+
}
|
|
798
|
+
}, [activeMethod]);
|
|
799
|
+
|
|
800
|
+
useEffect(() => {
|
|
801
|
+
if (activeMethod !== 'wecom' || isWecomEmbeddedBrowser() || isMobileBrowser()) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (showWecomQR) {
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
setShowWecomQR(true);
|
|
808
|
+
if (!wecomURLs && !loadingWecomQR) {
|
|
809
|
+
void loadWecomQR();
|
|
810
|
+
}
|
|
811
|
+
}, [activeMethod, loadWecomQR, loadingWecomQR, showWecomQR, wecomURLs]);
|
|
812
|
+
|
|
813
|
+
useEffect(() => {
|
|
814
|
+
if (activeMethod !== 'wecom' || !wecomURLs) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
let cancelled = false;
|
|
818
|
+
const interval = window.setInterval(async () => {
|
|
819
|
+
try {
|
|
820
|
+
const status = await auth.getAuthStatus({ client_id: config.client_id });
|
|
821
|
+
if (cancelled) return;
|
|
822
|
+
if (status.authenticated && status.account_id) {
|
|
823
|
+
window.clearInterval(interval);
|
|
824
|
+
if (!cancelled) {
|
|
825
|
+
await onSuccess(status);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
} catch {
|
|
829
|
+
// Waiting for the OAuth callback to create the hosted RunID session.
|
|
830
|
+
}
|
|
831
|
+
}, 2000);
|
|
832
|
+
return () => {
|
|
833
|
+
cancelled = true;
|
|
834
|
+
window.clearInterval(interval);
|
|
835
|
+
};
|
|
836
|
+
}, [activeMethod, auth, config.client_id, onSuccess, wecomURLs]);
|
|
837
|
+
|
|
558
838
|
const handleVerify = useCallback(async () => {
|
|
559
839
|
if (!loginConfig) {
|
|
560
840
|
messageApi.error(texts.requiredMethodError);
|
|
@@ -566,23 +846,53 @@ export function RunIDLoginWidget({
|
|
|
566
846
|
let response: LoginOrConsentResponse;
|
|
567
847
|
|
|
568
848
|
if (activeMethod === 'phone') {
|
|
569
|
-
|
|
849
|
+
const targetPhone = phone.trim();
|
|
850
|
+
const targetCode = phoneCode.trim();
|
|
851
|
+
if (!targetPhone) {
|
|
570
852
|
messageApi.error(texts.phonePlaceholder);
|
|
571
853
|
return;
|
|
572
854
|
}
|
|
573
|
-
|
|
855
|
+
if (!isValidCountryPhone(countryCode, targetPhone)) {
|
|
856
|
+
messageApi.error(texts.invalidPhone);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (!targetCode) {
|
|
860
|
+
messageApi.error(texts.requiredCode);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
response = await auth.verifyPhone(normalizeCountryPhone(countryCode, targetPhone), targetCode);
|
|
574
864
|
} else if (activeMethod === 'email') {
|
|
575
|
-
|
|
865
|
+
const targetEmail = email.trim();
|
|
866
|
+
const targetCode = emailCode.trim();
|
|
867
|
+
if (!targetEmail) {
|
|
576
868
|
messageApi.error(texts.emailPlaceholder);
|
|
577
869
|
return;
|
|
578
870
|
}
|
|
579
|
-
|
|
871
|
+
if (!isValidEmail(targetEmail)) {
|
|
872
|
+
messageApi.error(texts.invalidEmail);
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (!targetCode) {
|
|
876
|
+
messageApi.error(texts.requiredCode);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
response = await auth.verifyEmail(targetEmail, targetCode);
|
|
580
880
|
} else if (activeMethod === 'whatsapp') {
|
|
581
|
-
|
|
881
|
+
const targetWhatsapp = whatsapp.trim();
|
|
882
|
+
const targetCode = whatsappCode.trim();
|
|
883
|
+
if (!targetWhatsapp) {
|
|
582
884
|
messageApi.error(texts.whatsappPlaceholder);
|
|
583
885
|
return;
|
|
584
886
|
}
|
|
585
|
-
|
|
887
|
+
if (!isValidInternationalPhone(targetWhatsapp)) {
|
|
888
|
+
messageApi.error(texts.invalidWhatsapp);
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (!targetCode) {
|
|
892
|
+
messageApi.error(texts.requiredCode);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
response = await auth.verifyWhatsApp(normalizeInternationalPhone(targetWhatsapp), targetCode);
|
|
586
896
|
} else {
|
|
587
897
|
messageApi.error(texts.requiredMethodError);
|
|
588
898
|
return;
|
|
@@ -602,13 +912,13 @@ export function RunIDLoginWidget({
|
|
|
602
912
|
await onSuccess(response as LoginResponse);
|
|
603
913
|
} catch (error) {
|
|
604
914
|
const runErr = error as RunIDAuthError;
|
|
605
|
-
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message ||
|
|
915
|
+
const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || texts.loginFailed;
|
|
606
916
|
messageApi.error(msg);
|
|
607
917
|
onError?.(runErr);
|
|
608
918
|
} finally {
|
|
609
919
|
setIsSubmitting(false);
|
|
610
920
|
}
|
|
611
|
-
}, [activeMethod, auth, countryCode, email, emailCode, loginConfig, messageApi, onError, onSuccess, phone, phoneCode, texts.emailPlaceholder, texts.phonePlaceholder, texts.requiredMethodError, texts.whatsappPlaceholder, texts.mfaHint, whatsapp, whatsappCode]);
|
|
921
|
+
}, [activeMethod, auth, countryCode, email, emailCode, loginConfig, messageApi, onError, onSuccess, phone, phoneCode, texts.emailPlaceholder, texts.invalidEmail, texts.invalidPhone, texts.invalidWhatsapp, texts.loginFailed, texts.phonePlaceholder, texts.requiredCode, texts.requiredMethodError, texts.whatsappPlaceholder, texts.mfaHint, whatsapp, whatsappCode]);
|
|
612
922
|
|
|
613
923
|
const submitConsent = useCallback(async () => {
|
|
614
924
|
if (!consentData || !agreed) {
|
|
@@ -626,6 +936,7 @@ export function RunIDLoginWidget({
|
|
|
626
936
|
agreedIds,
|
|
627
937
|
consentData.login_method || activeMethod,
|
|
628
938
|
consentData.client_id,
|
|
939
|
+
consentData.consent_challenge,
|
|
629
940
|
);
|
|
630
941
|
setConsentData(null);
|
|
631
942
|
setAgreed(false);
|
|
@@ -658,7 +969,18 @@ export function RunIDLoginWidget({
|
|
|
658
969
|
);
|
|
659
970
|
}
|
|
660
971
|
|
|
661
|
-
|
|
972
|
+
const isInternalProduct = loginConfig.is_internal_product || loginConfig.client.realm === 'internal';
|
|
973
|
+
const wechatConfigured = isAllowedMethod(loginConfig, 'wechat');
|
|
974
|
+
const whatsappConfigured = isAllowedMethod(loginConfig, 'whatsapp');
|
|
975
|
+
const hasConfiguredSocialMethods = wechatConfigured || whatsappConfigured;
|
|
976
|
+
const showSocialMethods = !isInternalProduct && hasConfiguredSocialMethods;
|
|
977
|
+
const countryCodeSelectOptions = [{ value: countryCode, label: countryCode }];
|
|
978
|
+
const wecomEmbeddedBrowser = isWecomEmbeddedBrowser();
|
|
979
|
+
const mobileBrowser = isMobileBrowser();
|
|
980
|
+
const showWecomContinueButton = wecomEmbeddedBrowser || mobileBrowser;
|
|
981
|
+
const showWecomQRCode = activeMethod === 'wecom' && (!showWecomContinueButton || showWecomQR);
|
|
982
|
+
|
|
983
|
+
if (!primaryMethods.length && !hasConfiguredSocialMethods) {
|
|
662
984
|
return (
|
|
663
985
|
<div className={className} style={style}>
|
|
664
986
|
{contextHolder}
|
|
@@ -667,16 +989,6 @@ export function RunIDLoginWidget({
|
|
|
667
989
|
);
|
|
668
990
|
}
|
|
669
991
|
|
|
670
|
-
const tabItems = methods.map((method) => ({
|
|
671
|
-
key: method.key,
|
|
672
|
-
label: (
|
|
673
|
-
<span>
|
|
674
|
-
{method.icon}
|
|
675
|
-
<span style={{ marginLeft: 6 }}>{method.label}</span>
|
|
676
|
-
</span>
|
|
677
|
-
),
|
|
678
|
-
}));
|
|
679
|
-
|
|
680
992
|
const canSubmit = !isSubmitting &&
|
|
681
993
|
[
|
|
682
994
|
activeMethod === 'phone' ? phone.trim() && phoneCode.trim() :
|
|
@@ -690,144 +1002,390 @@ export function RunIDLoginWidget({
|
|
|
690
1002
|
window.open(href, '_blank', 'noopener,noreferrer');
|
|
691
1003
|
};
|
|
692
1004
|
|
|
1005
|
+
const rootClassName = ['runid-login-widget', className].filter(Boolean).join(' ');
|
|
1006
|
+
const inputStyle: CSSProperties = {
|
|
1007
|
+
height: 52,
|
|
1008
|
+
borderRadius: 8,
|
|
1009
|
+
fontSize: 15,
|
|
1010
|
+
};
|
|
1011
|
+
const sendButtonStyle: CSSProperties = {
|
|
1012
|
+
minWidth: 120,
|
|
1013
|
+
height: 52,
|
|
1014
|
+
borderRadius: 8,
|
|
1015
|
+
fontWeight: 500,
|
|
1016
|
+
};
|
|
1017
|
+
const primaryButtonStyle: CSSProperties = {
|
|
1018
|
+
height: 56,
|
|
1019
|
+
borderRadius: 8,
|
|
1020
|
+
fontSize: 16,
|
|
1021
|
+
fontWeight: 600,
|
|
1022
|
+
marginTop: 12,
|
|
1023
|
+
boxShadow: t.buttonShadow,
|
|
1024
|
+
};
|
|
1025
|
+
const oauthButtonStyle: CSSProperties = {
|
|
1026
|
+
height: 56,
|
|
1027
|
+
borderRadius: 8,
|
|
1028
|
+
fontSize: 16,
|
|
1029
|
+
fontWeight: 600,
|
|
1030
|
+
width: '100%',
|
|
1031
|
+
};
|
|
1032
|
+
const methodButtonStyle = (method: AuthMethod): CSSProperties => ({
|
|
1033
|
+
flex: 1,
|
|
1034
|
+
height: 40,
|
|
1035
|
+
borderRadius: 6,
|
|
1036
|
+
border: 'none',
|
|
1037
|
+
boxShadow: activeMethod === method ? t.activeTabShadow : 'none',
|
|
1038
|
+
background: activeMethod === method ? undefined : 'transparent',
|
|
1039
|
+
color: activeMethod === method ? t.activeTabColor : t.inactiveTabColor,
|
|
1040
|
+
fontWeight: activeMethod === method ? 600 : 400,
|
|
1041
|
+
});
|
|
1042
|
+
const socialButtonStyle: CSSProperties = {
|
|
1043
|
+
borderRadius: 8,
|
|
1044
|
+
height: 44,
|
|
1045
|
+
padding: '0 24px',
|
|
1046
|
+
color: t.socialBtnColor,
|
|
1047
|
+
};
|
|
1048
|
+
const submitLabel = isSubmitting ? texts.verifying : texts.login;
|
|
1049
|
+
|
|
693
1050
|
return (
|
|
694
|
-
<div className={
|
|
1051
|
+
<div className={rootClassName} data-active-method={activeMethod} style={style}>
|
|
695
1052
|
{contextHolder}
|
|
696
1053
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
1054
|
+
{primaryMethods.length > 0 && (
|
|
1055
|
+
<Flex
|
|
1056
|
+
gap={4}
|
|
1057
|
+
className="login-method-tabs animate-fade-in-up delay-200"
|
|
1058
|
+
style={{ width: '100%', marginBottom: 24, background: t.surface, border: `1px solid ${t.surfaceBorder}`, padding: 4, borderRadius: 8 }}
|
|
1059
|
+
>
|
|
1060
|
+
{primaryMethods.map((method) => (
|
|
1061
|
+
<Button
|
|
1062
|
+
key={method.key}
|
|
1063
|
+
type={activeMethod === method.key ? 'primary' : 'text'}
|
|
1064
|
+
onClick={() => setActiveMethod(method.key)}
|
|
1065
|
+
className={`login-method-tab login-method-tab-${method.key}`}
|
|
1066
|
+
style={methodButtonStyle(method.key)}
|
|
1067
|
+
icon={method.icon}
|
|
1068
|
+
>
|
|
1069
|
+
{method.label}
|
|
1070
|
+
</Button>
|
|
1071
|
+
))}
|
|
1072
|
+
</Flex>
|
|
1073
|
+
)}
|
|
1074
|
+
|
|
1075
|
+
<div className="login-content-area animate-scale-in" style={{ minHeight: 320 }}>
|
|
705
1076
|
{activeMethod === 'phone' && (
|
|
706
|
-
<
|
|
707
|
-
|
|
708
|
-
<
|
|
709
|
-
|
|
1077
|
+
<Flex vertical gap={24} style={{ padding: '10px 0' }}>
|
|
1078
|
+
{isInternalProduct && (
|
|
1079
|
+
<div style={{ padding: '10px 14px', borderRadius: 6, background: t.noticeBg, border: `1px solid ${t.noticeBorder}`, fontSize: 12, color: t.noticeText, lineHeight: 1.6 }}>
|
|
1080
|
+
{locale === 'zh-CN'
|
|
1081
|
+
? '仅限内部成员使用已登记的手机号登录,未登记的手机号无法登录。'
|
|
1082
|
+
: 'Only registered internal phone numbers can sign in.'}
|
|
1083
|
+
</div>
|
|
1084
|
+
)}
|
|
1085
|
+
<Flex vertical gap={16}>
|
|
1086
|
+
<Flex gap={12}>
|
|
1087
|
+
<Select
|
|
710
1088
|
value={countryCode}
|
|
711
|
-
|
|
712
|
-
|
|
1089
|
+
disabled
|
|
1090
|
+
options={countryCodeSelectOptions}
|
|
1091
|
+
size="large"
|
|
1092
|
+
style={{ width: 100, height: 52, borderRadius: 8 }}
|
|
713
1093
|
/>
|
|
714
1094
|
<Input
|
|
1095
|
+
size="large"
|
|
715
1096
|
value={phone}
|
|
716
|
-
onChange={(e) => setPhone(e.target.value)}
|
|
1097
|
+
onChange={(e) => setPhone(phoneDigits(e.target.value).slice(0, 20))}
|
|
717
1098
|
placeholder={texts.phonePlaceholder}
|
|
1099
|
+
style={{ ...inputStyle, flex: 1 }}
|
|
718
1100
|
/>
|
|
719
|
-
</
|
|
720
|
-
|
|
721
|
-
<Form.Item label={texts.code} required>
|
|
722
|
-
<Space.Compact style={{ width: '100%' }}>
|
|
1101
|
+
</Flex>
|
|
1102
|
+
<Flex gap={12}>
|
|
723
1103
|
<Input
|
|
1104
|
+
size="large"
|
|
724
1105
|
value={phoneCode}
|
|
725
|
-
onChange={(e) => setPhoneCode(e.target.value)}
|
|
1106
|
+
onChange={(e) => setPhoneCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
726
1107
|
placeholder={texts.codePlaceholder}
|
|
1108
|
+
style={{ ...inputStyle, flex: 1 }}
|
|
727
1109
|
/>
|
|
728
1110
|
<Button
|
|
729
|
-
|
|
1111
|
+
size="large"
|
|
730
1112
|
onClick={() => sendCode('phone')}
|
|
731
1113
|
loading={isSending.phone}
|
|
732
1114
|
disabled={countdown.phone > 0}
|
|
1115
|
+
style={sendButtonStyle}
|
|
733
1116
|
>
|
|
734
|
-
{countdown.phone > 0 ? `${
|
|
1117
|
+
{countdown.phone > 0 ? `${countdown.phone}s` : texts.sendCode}
|
|
735
1118
|
</Button>
|
|
736
|
-
</
|
|
737
|
-
</
|
|
738
|
-
|
|
1119
|
+
</Flex>
|
|
1120
|
+
</Flex>
|
|
1121
|
+
<Button
|
|
1122
|
+
type="primary"
|
|
1123
|
+
size="large"
|
|
1124
|
+
block
|
|
1125
|
+
loading={isSubmitting}
|
|
1126
|
+
disabled={!canSubmit}
|
|
1127
|
+
onClick={handleVerify}
|
|
1128
|
+
style={primaryButtonStyle}
|
|
1129
|
+
>
|
|
1130
|
+
<span>{submitLabel}</span>
|
|
1131
|
+
<ArrowRightOutlined style={{ marginLeft: 8 }} />
|
|
1132
|
+
</Button>
|
|
1133
|
+
</Flex>
|
|
739
1134
|
)}
|
|
740
1135
|
|
|
741
1136
|
{activeMethod === 'email' && (
|
|
742
|
-
<
|
|
743
|
-
|
|
1137
|
+
<Flex vertical gap={24} style={{ padding: '10px 0' }}>
|
|
1138
|
+
{isInternalProduct && (
|
|
1139
|
+
<div style={{ padding: '10px 14px', borderRadius: 6, background: t.noticeBg, border: `1px solid ${t.noticeBorder}`, fontSize: 12, color: t.noticeText, lineHeight: 1.6 }}>
|
|
1140
|
+
{locale === 'zh-CN'
|
|
1141
|
+
? '仅限内部成员使用企业邮箱登录,非内部邮箱无法登录。'
|
|
1142
|
+
: 'Only internal organization email accounts can sign in.'}
|
|
1143
|
+
</div>
|
|
1144
|
+
)}
|
|
1145
|
+
<Flex vertical gap={16}>
|
|
744
1146
|
<Input
|
|
1147
|
+
size="large"
|
|
745
1148
|
value={email}
|
|
746
|
-
onChange={(e) => setEmail(e.target.value)}
|
|
1149
|
+
onChange={(e) => setEmail(e.target.value.slice(0, 120))}
|
|
747
1150
|
placeholder={texts.emailPlaceholder}
|
|
1151
|
+
prefix={<MailOutlined style={{ color: t.inputIcon, marginRight: 8 }} />}
|
|
1152
|
+
style={inputStyle}
|
|
748
1153
|
/>
|
|
749
|
-
|
|
750
|
-
<Form.Item label={texts.code} required>
|
|
751
|
-
<Space.Compact style={{ width: '100%' }}>
|
|
1154
|
+
<Flex gap={12}>
|
|
752
1155
|
<Input
|
|
1156
|
+
size="large"
|
|
753
1157
|
value={emailCode}
|
|
754
|
-
onChange={(e) => setEmailCode(e.target.value)}
|
|
1158
|
+
onChange={(e) => setEmailCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
755
1159
|
placeholder={texts.codePlaceholder}
|
|
1160
|
+
style={{ ...inputStyle, flex: 1 }}
|
|
756
1161
|
/>
|
|
757
1162
|
<Button
|
|
1163
|
+
size="large"
|
|
758
1164
|
onClick={() => sendCode('email')}
|
|
759
1165
|
loading={isSending.email}
|
|
760
1166
|
disabled={countdown.email > 0}
|
|
1167
|
+
style={sendButtonStyle}
|
|
761
1168
|
>
|
|
762
|
-
{countdown.email > 0 ? `${
|
|
1169
|
+
{countdown.email > 0 ? `${countdown.email}s` : texts.sendCode}
|
|
763
1170
|
</Button>
|
|
764
|
-
</
|
|
765
|
-
</
|
|
766
|
-
|
|
1171
|
+
</Flex>
|
|
1172
|
+
</Flex>
|
|
1173
|
+
<Button
|
|
1174
|
+
type="primary"
|
|
1175
|
+
size="large"
|
|
1176
|
+
block
|
|
1177
|
+
loading={isSubmitting}
|
|
1178
|
+
disabled={!canSubmit}
|
|
1179
|
+
onClick={handleVerify}
|
|
1180
|
+
style={primaryButtonStyle}
|
|
1181
|
+
>
|
|
1182
|
+
<span>{submitLabel}</span>
|
|
1183
|
+
<ArrowRightOutlined style={{ marginLeft: 8 }} />
|
|
1184
|
+
</Button>
|
|
1185
|
+
</Flex>
|
|
767
1186
|
)}
|
|
768
1187
|
|
|
769
1188
|
{activeMethod === 'whatsapp' && (
|
|
770
|
-
<
|
|
771
|
-
<
|
|
1189
|
+
<Flex vertical gap={24} style={{ padding: '10px 0' }}>
|
|
1190
|
+
<Flex vertical gap={16}>
|
|
772
1191
|
<Input
|
|
1192
|
+
size="large"
|
|
773
1193
|
value={whatsapp}
|
|
774
|
-
onChange={(e) => setWhatsapp(e.target.value)}
|
|
1194
|
+
onChange={(e) => setWhatsapp(phoneDigits(e.target.value).slice(0, 20))}
|
|
775
1195
|
placeholder={texts.whatsappPlaceholder}
|
|
1196
|
+
prefix={<MessageOutlined style={{ color: t.inputIcon, marginRight: 8 }} />}
|
|
1197
|
+
style={inputStyle}
|
|
776
1198
|
/>
|
|
777
|
-
|
|
778
|
-
<Form.Item label={texts.code} required>
|
|
779
|
-
<Space.Compact style={{ width: '100%' }}>
|
|
1199
|
+
<Flex gap={12}>
|
|
780
1200
|
<Input
|
|
1201
|
+
size="large"
|
|
781
1202
|
value={whatsappCode}
|
|
782
|
-
onChange={(e) => setWhatsappCode(e.target.value)}
|
|
1203
|
+
onChange={(e) => setWhatsappCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
|
|
783
1204
|
placeholder={texts.codePlaceholder}
|
|
1205
|
+
style={{ ...inputStyle, flex: 1 }}
|
|
784
1206
|
/>
|
|
785
1207
|
<Button
|
|
1208
|
+
size="large"
|
|
786
1209
|
onClick={() => sendCode('whatsapp')}
|
|
787
1210
|
loading={isSending.whatsapp}
|
|
788
1211
|
disabled={countdown.whatsapp > 0}
|
|
1212
|
+
style={sendButtonStyle}
|
|
789
1213
|
>
|
|
790
|
-
{countdown.whatsapp > 0 ? `${
|
|
1214
|
+
{countdown.whatsapp > 0 ? `${countdown.whatsapp}s` : texts.sendCode}
|
|
791
1215
|
</Button>
|
|
792
|
-
</
|
|
793
|
-
</
|
|
794
|
-
|
|
1216
|
+
</Flex>
|
|
1217
|
+
</Flex>
|
|
1218
|
+
<Button
|
|
1219
|
+
type="primary"
|
|
1220
|
+
size="large"
|
|
1221
|
+
block
|
|
1222
|
+
loading={isSubmitting}
|
|
1223
|
+
disabled={!canSubmit}
|
|
1224
|
+
onClick={handleVerify}
|
|
1225
|
+
style={primaryButtonStyle}
|
|
1226
|
+
>
|
|
1227
|
+
<span>{submitLabel}</span>
|
|
1228
|
+
<ArrowRightOutlined style={{ marginLeft: 8 }} />
|
|
1229
|
+
</Button>
|
|
1230
|
+
</Flex>
|
|
795
1231
|
)}
|
|
796
1232
|
|
|
797
1233
|
{activeMethod === 'wechat' && (
|
|
798
|
-
<
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
1234
|
+
<Flex vertical align="center" justify="center" gap={20} style={{ padding: '44px 0' }}>
|
|
1235
|
+
<Text style={{ color: t.text, fontWeight: 500, textAlign: 'center' }}>
|
|
1236
|
+
{locale === 'zh-CN' ? '点击下方按钮完成微信授权' : 'Continue with WeChat authorization'}
|
|
1237
|
+
</Text>
|
|
1238
|
+
<Button
|
|
1239
|
+
type="primary"
|
|
1240
|
+
size="large"
|
|
1241
|
+
onClick={() => openOAuth('wechat')}
|
|
1242
|
+
icon={<WechatOutlined />}
|
|
1243
|
+
style={{ ...oauthButtonStyle, background: '#07c160', borderColor: '#07c160' }}
|
|
1244
|
+
>
|
|
1245
|
+
{texts.wechat}
|
|
1246
|
+
</Button>
|
|
1247
|
+
</Flex>
|
|
806
1248
|
)}
|
|
807
1249
|
|
|
808
1250
|
{activeMethod === 'wecom' && (
|
|
809
|
-
<
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
1251
|
+
<Flex className="login-wecom-panel" vertical align="center" justify="center" gap={14} style={{ padding: '28px 0 4px' }}>
|
|
1252
|
+
{wecomAutoRedirecting ? (
|
|
1253
|
+
<Flex vertical align="center" justify="center" gap={14} style={{ minHeight: 220 }}>
|
|
1254
|
+
<Spin />
|
|
1255
|
+
<Text style={{ color: t.textSecondary, fontSize: 13, textAlign: 'center' }}>
|
|
1256
|
+
{locale === 'zh-CN' ? '正在进入企业微信登录...' : 'Opening WeCom sign-in...'}
|
|
1257
|
+
</Text>
|
|
1258
|
+
</Flex>
|
|
1259
|
+
) : (
|
|
1260
|
+
<>
|
|
1261
|
+
{showWecomContinueButton && (
|
|
1262
|
+
<>
|
|
1263
|
+
<Button
|
|
1264
|
+
type="primary"
|
|
1265
|
+
size="large"
|
|
1266
|
+
block
|
|
1267
|
+
onClick={openWecomLogin}
|
|
1268
|
+
icon={<UserOutlined />}
|
|
1269
|
+
style={{ ...oauthButtonStyle, background: '#07c160', borderColor: '#07c160' }}
|
|
1270
|
+
>
|
|
1271
|
+
{locale === 'zh-CN' ? '继续企业微信登录' : 'Continue with WeCom'}
|
|
1272
|
+
</Button>
|
|
1273
|
+
<Text style={{ color: t.textSecondary, fontSize: 12, textAlign: 'center' }}>
|
|
1274
|
+
{locale === 'zh-CN' ? '将进入企业微信授权页;已登录客户端时可直接确认' : 'Continue on the WeCom authorization page.'}
|
|
1275
|
+
</Text>
|
|
1276
|
+
</>
|
|
1277
|
+
)}
|
|
1278
|
+
|
|
1279
|
+
{!showWecomContinueButton && (
|
|
1280
|
+
<Text style={{ color: t.textSecondary, fontSize: 13, textAlign: 'center' }}>
|
|
1281
|
+
{locale === 'zh-CN' ? '使用企业微信扫码完成登录' : 'Scan with WeCom to sign in'}
|
|
1282
|
+
</Text>
|
|
1283
|
+
)}
|
|
1284
|
+
|
|
1285
|
+
{showWecomContinueButton && !showWecomQR && (
|
|
1286
|
+
<Button
|
|
1287
|
+
type="link"
|
|
1288
|
+
onClick={() => {
|
|
1289
|
+
setShowWecomQR(true);
|
|
1290
|
+
void loadWecomQR();
|
|
1291
|
+
}}
|
|
1292
|
+
>
|
|
1293
|
+
{locale === 'zh-CN' ? '无法打开时使用扫码登录' : 'Use QR code if the app does not open'}
|
|
1294
|
+
</Button>
|
|
1295
|
+
)}
|
|
1296
|
+
|
|
1297
|
+
{showWecomQRCode && (
|
|
1298
|
+
<>
|
|
1299
|
+
<div
|
|
1300
|
+
className="login-wecom-qr-frame"
|
|
1301
|
+
style={{
|
|
1302
|
+
width: '100%',
|
|
1303
|
+
maxWidth: 340,
|
|
1304
|
+
height: 360,
|
|
1305
|
+
overflow: 'hidden',
|
|
1306
|
+
}}
|
|
1307
|
+
>
|
|
1308
|
+
{(!showWecomQR || loadingWecomQR) && (
|
|
1309
|
+
<Flex align="center" justify="center" style={{ height: 360 }}>
|
|
1310
|
+
<Spin />
|
|
1311
|
+
</Flex>
|
|
1312
|
+
)}
|
|
1313
|
+
{showWecomQR && !loadingWecomQR && wecomURLs?.auth_url && (
|
|
1314
|
+
<iframe
|
|
1315
|
+
className="login-wecom-qr-iframe"
|
|
1316
|
+
title={locale === 'zh-CN' ? '企业微信扫码登录' : 'WeCom sign-in QR code'}
|
|
1317
|
+
src={wecomURLs.auth_url}
|
|
1318
|
+
style={{
|
|
1319
|
+
display: 'block',
|
|
1320
|
+
width: 380,
|
|
1321
|
+
height: 400,
|
|
1322
|
+
border: 0,
|
|
1323
|
+
background: t.qrBg,
|
|
1324
|
+
transform: 'translateX(-20px) scale(0.9)',
|
|
1325
|
+
transformOrigin: 'top center',
|
|
1326
|
+
}}
|
|
1327
|
+
/>
|
|
1328
|
+
)}
|
|
1329
|
+
{showWecomQR && !loadingWecomQR && !wecomURLs?.auth_url && (
|
|
1330
|
+
<Flex vertical align="center" justify="center" gap={12} style={{ height: 360, padding: 24, textAlign: 'center' }}>
|
|
1331
|
+
<Text style={{ color: t.textSecondary }}>
|
|
1332
|
+
{locale === 'zh-CN' ? '企业微信二维码加载失败' : 'Failed to load WeCom QR code'}
|
|
1333
|
+
</Text>
|
|
1334
|
+
<Button onClick={loadWecomQR}>
|
|
1335
|
+
{locale === 'zh-CN' ? '重新加载' : 'Reload'}
|
|
1336
|
+
</Button>
|
|
1337
|
+
</Flex>
|
|
1338
|
+
)}
|
|
1339
|
+
</div>
|
|
1340
|
+
{showWecomContinueButton && (
|
|
1341
|
+
<Text style={{ color: t.textSecondary, fontSize: 13, textAlign: 'center' }}>
|
|
1342
|
+
{locale === 'zh-CN' ? '使用企业微信扫码完成登录' : 'Scan with WeCom to sign in'}
|
|
1343
|
+
</Text>
|
|
1344
|
+
)}
|
|
1345
|
+
</>
|
|
1346
|
+
)}
|
|
1347
|
+
</>
|
|
1348
|
+
)}
|
|
1349
|
+
</Flex>
|
|
817
1350
|
)}
|
|
1351
|
+
</div>
|
|
818
1352
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1353
|
+
{showSocialMethods && (
|
|
1354
|
+
<Flex justify="center" gap={16} style={{ marginTop: 24 }}>
|
|
1355
|
+
{wechatConfigured && (
|
|
1356
|
+
<Tooltip title={undefined}>
|
|
1357
|
+
<span style={{ display: 'inline-flex' }}>
|
|
1358
|
+
<Button
|
|
1359
|
+
type="text"
|
|
1360
|
+
size="large"
|
|
1361
|
+
onClick={() => openOAuth('wechat')}
|
|
1362
|
+
className="login-social-button login-social-button-wechat"
|
|
1363
|
+
icon={<WechatOutlined style={{ fontSize: 18, color: '#94a0bf' }} />}
|
|
1364
|
+
style={socialButtonStyle}
|
|
1365
|
+
>
|
|
1366
|
+
{texts.wechat}
|
|
1367
|
+
</Button>
|
|
1368
|
+
</span>
|
|
1369
|
+
</Tooltip>
|
|
1370
|
+
)}
|
|
1371
|
+
{whatsappConfigured && (
|
|
1372
|
+
<Tooltip title={undefined}>
|
|
1373
|
+
<span style={{ display: 'inline-flex' }}>
|
|
1374
|
+
<Button
|
|
1375
|
+
type={activeMethod === 'whatsapp' ? 'primary' : 'text'}
|
|
1376
|
+
size="large"
|
|
1377
|
+
onClick={() => setActiveMethod('whatsapp')}
|
|
1378
|
+
className="login-social-button login-social-button-whatsapp"
|
|
1379
|
+
icon={<MessageOutlined style={{ fontSize: 18, color: activeMethod === 'whatsapp' ? '#fff' : '#94a0bf' }} />}
|
|
1380
|
+
style={activeMethod === 'whatsapp' ? { ...socialButtonStyle, color: '#fff' } : socialButtonStyle}
|
|
1381
|
+
>
|
|
1382
|
+
{texts.whatsapp}
|
|
1383
|
+
</Button>
|
|
1384
|
+
</span>
|
|
1385
|
+
</Tooltip>
|
|
1386
|
+
)}
|
|
1387
|
+
</Flex>
|
|
1388
|
+
)}
|
|
831
1389
|
|
|
832
1390
|
<Modal
|
|
833
1391
|
open={Boolean(consentData)}
|
|
@@ -837,7 +1395,7 @@ export function RunIDLoginWidget({
|
|
|
837
1395
|
setConsentData(null);
|
|
838
1396
|
setAgreed(false);
|
|
839
1397
|
}}
|
|
840
|
-
|
|
1398
|
+
destroyOnHidden
|
|
841
1399
|
>
|
|
842
1400
|
{consentData && (
|
|
843
1401
|
<Space direction="vertical" size={12} style={{ width: '100%' }}>
|