@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/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
- Form,
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('wecom')) return 'wecom';
351
- if (config.client.allowed_methods.includes('wechat')) return 'wechat';
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 [auth, setAuth] = useState(() => new RunIDAuth({
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
- const msg = parseAuthErrorMessage(error.payload) || error.message;
387
- if (msg) {
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 methods = [
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
- { key: 'wechat' as const, visible: isAllowedMethod(loginConfig, 'wechat'), icon: <WechatOutlined />, label: texts.wechat },
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
- const target = `${countryCode}${targetPhone}`;
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.includes('@')) {
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
- await auth.sendWhatsAppCode(targetWhatsapp);
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 || `${texts.sendCode}失败`;
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
- const urls = method === 'wechat'
540
- ? await auth.getWechatAuthURL(returnUrl)
541
- : await auth.getWecomAuthURL(returnUrl);
542
- const target = urls.auth_url;
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, messageApi]
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
- if (!phone.trim() || !phoneCode.trim()) {
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
- response = await auth.verifyPhone(`${countryCode}${phone.trim()}`, phoneCode.trim());
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
- if (!email.trim() || !emailCode.trim()) {
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
- response = await auth.verifyEmail(email.trim(), emailCode.trim());
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
- if (!whatsapp.trim() || !whatsappCode.trim()) {
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
- response = await auth.verifyWhatsApp(whatsapp.trim(), whatsappCode.trim());
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
- if (!methods.length) {
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={className} style={style}>
1051
+ <div className={rootClassName} data-active-method={activeMethod} style={style}>
695
1052
  {contextHolder}
696
1053
 
697
- <Tabs
698
- activeKey={activeMethod}
699
- items={tabItems}
700
- onChange={(value) => setActiveMethod(value as AuthMethod)}
701
- size="large"
702
- />
703
-
704
- <Space direction="vertical" size={16} style={{ width: '100%' }}>
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
- <Space direction="vertical" size={12} style={{ width: '100%' }}>
707
- <Form.Item label={texts.countryCodePrefix} required>
708
- <Space.Compact style={{ width: '100%' }}>
709
- <Input
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
- onChange={(e) => setCountryCode(e.target.value)}
712
- style={{ width: 90 }}
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
- </Space.Compact>
720
- </Form.Item>
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
- icon={<SendOutlined />}
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 ? `${texts.resendPrefix}(${countdown.phone}s)` : texts.sendCode}
1117
+ {countdown.phone > 0 ? `${countdown.phone}s` : texts.sendCode}
735
1118
  </Button>
736
- </Space.Compact>
737
- </Form.Item>
738
- </Space>
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
- <Space direction="vertical" size={12} style={{ width: '100%' }}>
743
- <Form.Item label={texts.email} required>
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
- </Form.Item>
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 ? `${texts.resendPrefix}(${countdown.email}s)` : texts.sendCode}
1169
+ {countdown.email > 0 ? `${countdown.email}s` : texts.sendCode}
763
1170
  </Button>
764
- </Space.Compact>
765
- </Form.Item>
766
- </Space>
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
- <Space direction="vertical" size={12} style={{ width: '100%' }}>
771
- <Form.Item label={texts.whatsapp} required>
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
- </Form.Item>
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 ? `${texts.resendPrefix}(${countdown.whatsapp}s)` : texts.sendCode}
1214
+ {countdown.whatsapp > 0 ? `${countdown.whatsapp}s` : texts.sendCode}
791
1215
  </Button>
792
- </Space.Compact>
793
- </Form.Item>
794
- </Space>
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
- <Button
799
- block
800
- size="large"
801
- onClick={() => openOAuth('wechat')}
802
- icon={<WechatOutlined />}
803
- >
804
- {texts.wechat}
805
- </Button>
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
- <Button
810
- block
811
- size="large"
812
- onClick={() => openOAuth('wecom')}
813
- icon={<UserOutlined />}
814
- >
815
- {texts.wecom}
816
- </Button>
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
- {(activeMethod === 'phone' || activeMethod === 'email' || activeMethod === 'whatsapp') && (
820
- <Button
821
- type="primary"
822
- size="large"
823
- loading={isSubmitting}
824
- disabled={!canSubmit}
825
- onClick={handleVerify}
826
- >
827
- {isSubmitting ? texts.verifying : texts.login}
828
- </Button>
829
- )}
830
- </Space>
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
- destroyOnClose
1398
+ destroyOnHidden
841
1399
  >
842
1400
  {consentData && (
843
1401
  <Space direction="vertical" size={12} style={{ width: '100%' }}>