@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.js CHANGED
@@ -1,11 +1,36 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useContext, useState, useCallback, useEffect, } from 'react';
3
- import { Tabs, Button, Input, Space, Checkbox, Modal, Typography, Spin, message, Form, } from 'antd';
4
- import { PhoneOutlined, MailOutlined, MessageOutlined, SafetyOutlined, WechatOutlined, UserOutlined, SendOutlined, } from '@ant-design/icons';
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { createContext, useContext, useState, useCallback, useEffect, useMemo, } from 'react';
3
+ import { Button, Flex, Input, Select, Space, Checkbox, Modal, Typography, Spin, message, Tooltip, } from 'antd';
4
+ import { ArrowRightOutlined, PhoneOutlined, MailOutlined, MessageOutlined, SafetyOutlined, WechatOutlined, UserOutlined, } from '@ant-design/icons';
5
5
  import { RunIDAuth, isConsentRequiredResponse, parseAuthErrorMessage, } from '@ruzhiai/runid-sdk';
6
6
  export { RunIDAuth };
7
7
  export * from '@ruzhiai/runid-sdk';
8
8
  const { Text, Title, Paragraph } = Typography;
9
+ function getBrowserUserAgent() {
10
+ if (typeof navigator === 'undefined')
11
+ return '';
12
+ return navigator.userAgent || '';
13
+ }
14
+ function isWecomEmbeddedBrowser() {
15
+ return /wxwork/i.test(getBrowserUserAgent());
16
+ }
17
+ function isMobileBrowser() {
18
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(getBrowserUserAgent());
19
+ }
20
+ function pickWecomAutoRedirectURL(urls) {
21
+ if (!isWecomEmbeddedBrowser())
22
+ return undefined;
23
+ return urls.mobile_auth_url ?? urls.app_auth_url ?? urls.auth_url;
24
+ }
25
+ function pickWecomBrowserURL(urls) {
26
+ if (isWecomEmbeddedBrowser()) {
27
+ return urls.mobile_auth_url ?? urls.app_auth_url ?? urls.auth_url;
28
+ }
29
+ if (isMobileBrowser()) {
30
+ return urls.app_auth_url ?? urls.mobile_auth_url ?? urls.auth_url;
31
+ }
32
+ return urls.auth_url;
33
+ }
9
34
  const RunIDAuthContext = createContext(null);
10
35
  export function RunIDAuthProvider({ config, children, autoLoadMe = true, }) {
11
36
  const [auth] = useState(() => new RunIDAuth(config));
@@ -109,6 +134,53 @@ export function useRunIDAuth() {
109
134
  }
110
135
  return context;
111
136
  }
137
+ const THEME = {
138
+ light: {
139
+ surface: '#f5f7fa',
140
+ surfaceBorder: 'transparent',
141
+ text: '#0f1629',
142
+ textSecondary: '#6b7aa0',
143
+ inputIcon: '#94a0bf',
144
+ noticeBg: '#fff8e1',
145
+ noticeBorder: '#ffe082',
146
+ noticeText: '#8d6e00',
147
+ qrBg: '#fff',
148
+ activeTabColor: '#fff',
149
+ inactiveTabColor: '#6b7aa0',
150
+ activeTabShadow: '0 2px 8px rgba(15,22,41,0.08)',
151
+ buttonShadow: '0 8px 20px rgba(99, 102, 241, 0.2)',
152
+ socialBtnColor: '#6b7aa0',
153
+ },
154
+ dark: {
155
+ surface: 'rgba(255,255,255,0.06)',
156
+ surfaceBorder: 'rgba(255,255,255,0.08)',
157
+ text: '#f4f4f5',
158
+ textSecondary: '#a1a1aa',
159
+ inputIcon: '#71717a',
160
+ noticeBg: 'rgba(234,179,8,0.1)',
161
+ noticeBorder: 'rgba(234,179,8,0.25)',
162
+ noticeText: '#fbbf24',
163
+ qrBg: '#18181b',
164
+ activeTabColor: '#fff',
165
+ inactiveTabColor: '#a1a1aa',
166
+ activeTabShadow: '0 2px 8px rgba(0,0,0,0.3)',
167
+ buttonShadow: 'none',
168
+ socialBtnColor: '#a1a1aa',
169
+ },
170
+ };
171
+ function resolveTheme(explicit) {
172
+ if (explicit)
173
+ return explicit;
174
+ if (typeof document !== 'undefined') {
175
+ const htmlTheme = document.documentElement.getAttribute('data-rd-theme')
176
+ || document.documentElement.getAttribute('data-theme');
177
+ if (htmlTheme === 'dark')
178
+ return 'dark';
179
+ if (window.matchMedia?.('(prefers-color-scheme: dark)').matches)
180
+ return 'dark';
181
+ }
182
+ return 'light';
183
+ }
112
184
  const TEXT = {
113
185
  'zh-CN': {
114
186
  wechat: '微信登录',
@@ -134,7 +206,14 @@ const TEXT = {
134
206
  emailPlaceholder: '请输入邮箱',
135
207
  whatsappPlaceholder: '请输入 WhatsApp 号码',
136
208
  codePlaceholder: '请输入验证码',
209
+ invalidPhone: '请输入正确的手机号',
210
+ invalidEmail: '请输入正确的邮箱地址',
211
+ invalidWhatsapp: '请输入正确的 WhatsApp 号码',
212
+ requiredCode: '请输入验证码',
213
+ sendCodeFailed: '发送验证码失败',
214
+ loginFailed: '登录失败',
137
215
  mfaHint: '该账号已开启二次验证,请完成下一步认证。',
216
+ methodNotConfigured: '该登录方式未配置,暂不可用',
138
217
  },
139
218
  'en-US': {
140
219
  wechat: 'WeChat',
@@ -160,7 +239,14 @@ const TEXT = {
160
239
  emailPlaceholder: 'Enter email',
161
240
  whatsappPlaceholder: 'Enter WhatsApp number',
162
241
  codePlaceholder: 'Enter verification code',
242
+ invalidPhone: 'Enter a valid phone number',
243
+ invalidEmail: 'Enter a valid email address',
244
+ invalidWhatsapp: 'Enter a valid WhatsApp number',
245
+ requiredCode: 'Enter verification code',
246
+ sendCodeFailed: 'Failed to send code',
247
+ loginFailed: 'Sign-in failed',
163
248
  mfaHint: 'MFA is required before login. Please complete second-factor verification.',
249
+ methodNotConfigured: 'This login method is not configured',
164
250
  },
165
251
  };
166
252
  function isAllowedMethod(config, method) {
@@ -170,54 +256,73 @@ function resolveInitialMethod(config, requested) {
170
256
  if (requested && isAllowedMethod(config, requested)) {
171
257
  return requested;
172
258
  }
259
+ if (config.client.allowed_methods.includes('wecom'))
260
+ return 'wecom';
173
261
  if (config.client.allowed_methods.includes('phone'))
174
262
  return 'phone';
175
263
  if (config.client.allowed_methods.includes('email'))
176
264
  return 'email';
177
- if (config.client.allowed_methods.includes('wecom'))
178
- return 'wecom';
179
- if (config.client.allowed_methods.includes('wechat'))
180
- return 'wechat';
181
- return 'whatsapp';
265
+ if (config.client.allowed_methods.includes('whatsapp'))
266
+ return 'whatsapp';
267
+ return 'wechat';
182
268
  }
183
269
  function isMfaResponse(response) {
184
270
  return !!(response &&
185
271
  typeof response === 'object' &&
186
272
  response.mfa_required === true);
187
273
  }
274
+ function phoneDigits(value) {
275
+ return value.replace(/\D/g, '');
276
+ }
277
+ function normalizeCountryPhone(countryCode, value) {
278
+ return `${countryCode}${phoneDigits(value)}`;
279
+ }
280
+ function normalizeInternationalPhone(value) {
281
+ const trimmed = value.trim();
282
+ const digits = phoneDigits(trimmed);
283
+ return trimmed.startsWith('+') ? `+${digits}` : digits;
284
+ }
285
+ function isValidCountryPhone(countryCode, value) {
286
+ const digits = phoneDigits(value);
287
+ if (!digits)
288
+ return false;
289
+ if (countryCode === '+86')
290
+ return /^1\d{10}$/.test(digits);
291
+ if (countryCode === '+1')
292
+ return digits.length === 10;
293
+ if (countryCode === '+44')
294
+ return digits.length >= 10 && digits.length <= 11;
295
+ if (countryCode === '+81')
296
+ return digits.length >= 10 && digits.length <= 11;
297
+ if (countryCode === '+65')
298
+ return digits.length === 8;
299
+ return digits.length >= 6 && digits.length <= 20;
300
+ }
301
+ function isValidInternationalPhone(value) {
302
+ const digits = phoneDigits(value);
303
+ return digits.length >= 6 && digits.length <= 20;
304
+ }
305
+ function isValidEmail(value) {
306
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value.trim());
307
+ }
188
308
  const initialCountdown = {
189
309
  phone: 0,
190
310
  email: 0,
191
311
  whatsapp: 0,
192
312
  };
193
- export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'zh-CN', onSuccess, onError, onOAuthRedirect, className, style, }) {
313
+ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'zh-CN', theme: themeProp, onSuccess, onError, onOAuthRedirect, className, style, }) {
194
314
  const [messageApi, contextHolder] = message.useMessage();
195
- const [auth, setAuth] = useState(() => new RunIDAuth({
315
+ const t = THEME[resolveTheme(themeProp)];
316
+ const auth = useMemo(() => new RunIDAuth({
196
317
  ...config,
197
318
  defaultReturnURL: returnUrl,
198
319
  onAuthSuccess: () => { },
199
320
  onAuthError: (error) => {
200
- const msg = parseAuthErrorMessage(error.payload) || error.message;
201
- if (msg) {
202
- messageApi.error(msg);
321
+ if (error.status !== 401) {
322
+ onError?.(error);
203
323
  }
204
- onError?.(error);
205
324
  },
206
- }));
207
- useEffect(() => {
208
- setAuth(new RunIDAuth({
209
- ...config,
210
- defaultReturnURL: returnUrl,
211
- onAuthSuccess: () => { },
212
- onAuthError: (error) => {
213
- const msg = parseAuthErrorMessage(error.payload) || error.message;
214
- if (msg) {
215
- messageApi.error(msg);
216
- }
217
- onError?.(error);
218
- },
219
- }));
220
- }, [config.baseURL, config.client_id, messageApi, onError, returnUrl]);
325
+ }), [config.issuer, config.apiBaseURL, config.baseURL, config.client_id, onError, returnUrl]);
221
326
  const texts = TEXT[locale];
222
327
  const [loginConfig, setLoginConfig] = useState(null);
223
328
  const [loadingConfig, setLoadingConfig] = useState(true);
@@ -239,6 +344,10 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
239
344
  const [consentData, setConsentData] = useState(null);
240
345
  const [agreed, setAgreed] = useState(false);
241
346
  const [processingConsent, setProcessingConsent] = useState(false);
347
+ const [wecomURLs, setWecomURLs] = useState(null);
348
+ const [loadingWecomQR, setLoadingWecomQR] = useState(false);
349
+ const [showWecomQR, setShowWecomQR] = useState(false);
350
+ const [wecomAutoRedirecting, setWecomAutoRedirecting] = useState(false);
242
351
  useEffect(() => {
243
352
  let active = true;
244
353
  const load = async () => {
@@ -271,12 +380,10 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
271
380
  active = false;
272
381
  };
273
382
  }, [auth, defaultMethod, onError, returnUrl, messageApi, texts.requiredMethodError]);
274
- const methods = [
383
+ const primaryMethods = [
384
+ { key: 'wecom', visible: isAllowedMethod(loginConfig, 'wecom'), icon: _jsx(UserOutlined, {}), label: texts.wecom },
275
385
  { key: 'phone', visible: isAllowedMethod(loginConfig, 'phone'), icon: _jsx(PhoneOutlined, {}), label: texts.phone },
276
386
  { key: 'email', visible: isAllowedMethod(loginConfig, 'email'), icon: _jsx(MailOutlined, {}), label: texts.email },
277
- { key: 'wechat', visible: isAllowedMethod(loginConfig, 'wechat'), icon: _jsx(WechatOutlined, {}), label: texts.wechat },
278
- { key: 'wecom', visible: isAllowedMethod(loginConfig, 'wecom'), icon: _jsx(UserOutlined, {}), label: texts.wecom },
279
- { key: 'whatsapp', visible: isAllowedMethod(loginConfig, 'whatsapp'), icon: _jsx(MessageOutlined, {}), label: texts.whatsapp },
280
387
  ].filter((item) => item.visible);
281
388
  const setCountDown = useCallback((method, seconds) => {
282
389
  setCountdown((prev) => ({ ...prev, [method]: seconds }));
@@ -303,18 +410,26 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
303
410
  const targetWhatsapp = whatsapp.trim();
304
411
  setIsSending((prev) => ({ ...prev, [method]: true }));
305
412
  if (method === 'phone') {
306
- const target = `${countryCode}${targetPhone}`;
307
- if (!target) {
413
+ if (!targetPhone) {
308
414
  messageApi.error(texts.phonePlaceholder);
309
415
  return;
310
416
  }
417
+ if (!isValidCountryPhone(countryCode, targetPhone)) {
418
+ messageApi.error(texts.invalidPhone);
419
+ return;
420
+ }
421
+ const target = normalizeCountryPhone(countryCode, targetPhone);
311
422
  await auth.sendPhoneCode(target);
312
423
  }
313
424
  else if (method === 'email') {
314
- if (!targetEmail.includes('@')) {
425
+ if (!targetEmail) {
315
426
  messageApi.error(texts.emailPlaceholder);
316
427
  return;
317
428
  }
429
+ if (!isValidEmail(targetEmail)) {
430
+ messageApi.error(texts.invalidEmail);
431
+ return;
432
+ }
318
433
  await auth.sendEmailCode(targetEmail);
319
434
  }
320
435
  else {
@@ -322,14 +437,18 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
322
437
  messageApi.error(texts.whatsappPlaceholder);
323
438
  return;
324
439
  }
325
- await auth.sendWhatsAppCode(targetWhatsapp);
440
+ if (!isValidInternationalPhone(targetWhatsapp)) {
441
+ messageApi.error(texts.invalidWhatsapp);
442
+ return;
443
+ }
444
+ await auth.sendWhatsAppCode(normalizeInternationalPhone(targetWhatsapp));
326
445
  }
327
446
  messageApi.success(texts.sendCode);
328
447
  startTimer(method);
329
448
  }
330
449
  catch (error) {
331
450
  const runErr = error;
332
- const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || `${texts.sendCode}失败`;
451
+ const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || texts.sendCodeFailed;
333
452
  messageApi.error(msg);
334
453
  onError?.(runErr);
335
454
  }
@@ -339,10 +458,22 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
339
458
  }, [auth, countryCode, countdown, email, messageApi, onError, phone, startTimer, texts, whatsapp]);
340
459
  const openOAuth = useCallback(async (method) => {
341
460
  try {
342
- const urls = method === 'wechat'
343
- ? await auth.getWechatAuthURL(returnUrl)
344
- : await auth.getWecomAuthURL(returnUrl);
345
- const target = urls.auth_url;
461
+ if (method === 'wechat') {
462
+ const urls = await auth.getWechatAuthURL(returnUrl);
463
+ const target = urls.auth_url;
464
+ if (onOAuthRedirect) {
465
+ onOAuthRedirect(method, target);
466
+ return;
467
+ }
468
+ window.location.assign(target);
469
+ return;
470
+ }
471
+ const urls = await auth.getWecomAuthURL(returnUrl);
472
+ const target = pickWecomBrowserURL(urls);
473
+ if (!target) {
474
+ messageApi.error(locale === 'zh-CN' ? '企业微信登录地址不可用' : 'WeCom sign-in URL is unavailable');
475
+ return;
476
+ }
346
477
  if (onOAuthRedirect) {
347
478
  onOAuthRedirect(method, target);
348
479
  return;
@@ -355,7 +486,128 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
355
486
  messageApi.error(msg);
356
487
  onError?.(runErr);
357
488
  }
358
- }, [auth, onError, onOAuthRedirect, returnUrl, messageApi]);
489
+ }, [auth, locale, messageApi, onError, onOAuthRedirect, returnUrl]);
490
+ const openWecomLogin = useCallback(async () => {
491
+ try {
492
+ const urls = wecomURLs ?? await auth.getWecomAuthURL(returnUrl);
493
+ if (!wecomURLs) {
494
+ setWecomURLs(urls);
495
+ }
496
+ const target = pickWecomBrowserURL(urls);
497
+ if (!target) {
498
+ messageApi.error(locale === 'zh-CN' ? '企业微信登录地址不可用' : 'WeCom sign-in URL is unavailable');
499
+ return;
500
+ }
501
+ if (onOAuthRedirect) {
502
+ onOAuthRedirect('wecom', target);
503
+ return;
504
+ }
505
+ window.location.assign(target);
506
+ }
507
+ catch (error) {
508
+ const runErr = error;
509
+ const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信登录地址失败';
510
+ messageApi.error(msg);
511
+ onError?.(runErr);
512
+ }
513
+ }, [auth, locale, messageApi, onError, onOAuthRedirect, returnUrl, wecomURLs]);
514
+ const loadWecomQR = useCallback(async () => {
515
+ try {
516
+ setLoadingWecomQR(true);
517
+ const urls = await auth.getWecomAuthURL(returnUrl);
518
+ setWecomURLs(urls);
519
+ }
520
+ catch (error) {
521
+ const runErr = error;
522
+ const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信二维码失败';
523
+ messageApi.error(msg);
524
+ onError?.(runErr);
525
+ }
526
+ finally {
527
+ setLoadingWecomQR(false);
528
+ }
529
+ }, [auth, messageApi, onError, returnUrl]);
530
+ useEffect(() => {
531
+ if (!loginConfig || activeMethod !== 'wecom' || !isAllowedMethod(loginConfig, 'wecom')) {
532
+ setWecomAutoRedirecting(false);
533
+ return;
534
+ }
535
+ if (!isWecomEmbeddedBrowser()) {
536
+ setWecomAutoRedirecting(false);
537
+ return;
538
+ }
539
+ let cancelled = false;
540
+ setWecomAutoRedirecting(true);
541
+ void (async () => {
542
+ try {
543
+ const urls = await auth.getWecomAuthURL(returnUrl);
544
+ if (cancelled)
545
+ return;
546
+ setWecomURLs(urls);
547
+ const redirect = pickWecomAutoRedirectURL(urls);
548
+ if (redirect) {
549
+ window.location.assign(redirect);
550
+ return;
551
+ }
552
+ setWecomAutoRedirecting(false);
553
+ }
554
+ catch (error) {
555
+ if (cancelled)
556
+ return;
557
+ const runErr = error;
558
+ const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '获取企业微信登录地址失败';
559
+ messageApi.error(msg);
560
+ onError?.(runErr);
561
+ setWecomAutoRedirecting(false);
562
+ }
563
+ })();
564
+ return () => {
565
+ cancelled = true;
566
+ };
567
+ }, [activeMethod, auth, loginConfig, messageApi, onError, returnUrl]);
568
+ useEffect(() => {
569
+ if (activeMethod !== 'wecom') {
570
+ setShowWecomQR(false);
571
+ }
572
+ }, [activeMethod]);
573
+ useEffect(() => {
574
+ if (activeMethod !== 'wecom' || isWecomEmbeddedBrowser() || isMobileBrowser()) {
575
+ return;
576
+ }
577
+ if (showWecomQR) {
578
+ return;
579
+ }
580
+ setShowWecomQR(true);
581
+ if (!wecomURLs && !loadingWecomQR) {
582
+ void loadWecomQR();
583
+ }
584
+ }, [activeMethod, loadWecomQR, loadingWecomQR, showWecomQR, wecomURLs]);
585
+ useEffect(() => {
586
+ if (activeMethod !== 'wecom' || !wecomURLs) {
587
+ return;
588
+ }
589
+ let cancelled = false;
590
+ const interval = window.setInterval(async () => {
591
+ try {
592
+ const status = await auth.getAuthStatus({ client_id: config.client_id });
593
+ if (cancelled)
594
+ return;
595
+ if (status.authenticated && status.account_id) {
596
+ window.clearInterval(interval);
597
+ if (!cancelled) {
598
+ await onSuccess(status);
599
+ }
600
+ }
601
+ }
602
+ catch {
603
+ // Waiting for the OAuth callback to create the hosted RunID session.
604
+ }
605
+ }, 2000);
606
+ return () => {
607
+ cancelled = true;
608
+ window.clearInterval(interval);
609
+ };
610
+ }, [activeMethod, auth, config.client_id, onSuccess, wecomURLs]);
359
611
  const handleVerify = useCallback(async () => {
360
612
  if (!loginConfig) {
361
613
  messageApi.error(texts.requiredMethodError);
@@ -365,25 +617,55 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
365
617
  try {
366
618
  let response;
367
619
  if (activeMethod === 'phone') {
368
- if (!phone.trim() || !phoneCode.trim()) {
620
+ const targetPhone = phone.trim();
621
+ const targetCode = phoneCode.trim();
622
+ if (!targetPhone) {
369
623
  messageApi.error(texts.phonePlaceholder);
370
624
  return;
371
625
  }
372
- response = await auth.verifyPhone(`${countryCode}${phone.trim()}`, phoneCode.trim());
626
+ if (!isValidCountryPhone(countryCode, targetPhone)) {
627
+ messageApi.error(texts.invalidPhone);
628
+ return;
629
+ }
630
+ if (!targetCode) {
631
+ messageApi.error(texts.requiredCode);
632
+ return;
633
+ }
634
+ response = await auth.verifyPhone(normalizeCountryPhone(countryCode, targetPhone), targetCode);
373
635
  }
374
636
  else if (activeMethod === 'email') {
375
- if (!email.trim() || !emailCode.trim()) {
637
+ const targetEmail = email.trim();
638
+ const targetCode = emailCode.trim();
639
+ if (!targetEmail) {
376
640
  messageApi.error(texts.emailPlaceholder);
377
641
  return;
378
642
  }
379
- response = await auth.verifyEmail(email.trim(), emailCode.trim());
643
+ if (!isValidEmail(targetEmail)) {
644
+ messageApi.error(texts.invalidEmail);
645
+ return;
646
+ }
647
+ if (!targetCode) {
648
+ messageApi.error(texts.requiredCode);
649
+ return;
650
+ }
651
+ response = await auth.verifyEmail(targetEmail, targetCode);
380
652
  }
381
653
  else if (activeMethod === 'whatsapp') {
382
- if (!whatsapp.trim() || !whatsappCode.trim()) {
654
+ const targetWhatsapp = whatsapp.trim();
655
+ const targetCode = whatsappCode.trim();
656
+ if (!targetWhatsapp) {
383
657
  messageApi.error(texts.whatsappPlaceholder);
384
658
  return;
385
659
  }
386
- response = await auth.verifyWhatsApp(whatsapp.trim(), whatsappCode.trim());
660
+ if (!isValidInternationalPhone(targetWhatsapp)) {
661
+ messageApi.error(texts.invalidWhatsapp);
662
+ return;
663
+ }
664
+ if (!targetCode) {
665
+ messageApi.error(texts.requiredCode);
666
+ return;
667
+ }
668
+ response = await auth.verifyWhatsApp(normalizeInternationalPhone(targetWhatsapp), targetCode);
387
669
  }
388
670
  else {
389
671
  messageApi.error(texts.requiredMethodError);
@@ -402,14 +684,14 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
402
684
  }
403
685
  catch (error) {
404
686
  const runErr = error;
405
- const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || '登录失败';
687
+ const msg = parseAuthErrorMessage(runErr.payload) || runErr.message || texts.loginFailed;
406
688
  messageApi.error(msg);
407
689
  onError?.(runErr);
408
690
  }
409
691
  finally {
410
692
  setIsSubmitting(false);
411
693
  }
412
- }, [activeMethod, auth, countryCode, email, emailCode, loginConfig, messageApi, onError, onSuccess, phone, phoneCode, texts.emailPlaceholder, texts.phonePlaceholder, texts.requiredMethodError, texts.whatsappPlaceholder, texts.mfaHint, whatsapp, whatsappCode]);
694
+ }, [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]);
413
695
  const submitConsent = useCallback(async () => {
414
696
  if (!consentData || !agreed) {
415
697
  return;
@@ -419,7 +701,7 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
419
701
  const agreedIds = consentData.pending_agreements
420
702
  .map((agreement) => agreement.id)
421
703
  .filter(Boolean);
422
- const response = await auth.consentAndLogin(consentData.account_id, agreedIds, consentData.login_method || activeMethod, consentData.client_id);
704
+ const response = await auth.consentAndLogin(consentData.account_id, agreedIds, consentData.login_method || activeMethod, consentData.client_id, consentData.consent_challenge);
423
705
  setConsentData(null);
424
706
  setAgreed(false);
425
707
  await onSuccess(response);
@@ -440,13 +722,19 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
440
722
  if (!loginConfig) {
441
723
  return (_jsxs("div", { className: className, style: style, children: [contextHolder, _jsx(Text, { children: texts.requiredMethodError })] }));
442
724
  }
443
- if (!methods.length) {
725
+ const isInternalProduct = loginConfig.is_internal_product || loginConfig.client.realm === 'internal';
726
+ const wechatConfigured = isAllowedMethod(loginConfig, 'wechat');
727
+ const whatsappConfigured = isAllowedMethod(loginConfig, 'whatsapp');
728
+ const hasConfiguredSocialMethods = wechatConfigured || whatsappConfigured;
729
+ const showSocialMethods = !isInternalProduct && hasConfiguredSocialMethods;
730
+ const countryCodeSelectOptions = [{ value: countryCode, label: countryCode }];
731
+ const wecomEmbeddedBrowser = isWecomEmbeddedBrowser();
732
+ const mobileBrowser = isMobileBrowser();
733
+ const showWecomContinueButton = wecomEmbeddedBrowser || mobileBrowser;
734
+ const showWecomQRCode = activeMethod === 'wecom' && (!showWecomContinueButton || showWecomQR);
735
+ if (!primaryMethods.length && !hasConfiguredSocialMethods) {
444
736
  return (_jsxs("div", { className: className, style: style, children: [contextHolder, _jsx(Text, { children: texts.requiredMethodError })] }));
445
737
  }
446
- const tabItems = methods.map((method) => ({
447
- key: method.key,
448
- label: (_jsxs("span", { children: [method.icon, _jsx("span", { style: { marginLeft: 6 }, children: method.label })] })),
449
- }));
450
738
  const canSubmit = !isSubmitting &&
451
739
  [
452
740
  activeMethod === 'phone' ? phone.trim() && phoneCode.trim() :
@@ -458,10 +746,74 @@ export function RunIDLoginWidget({ config, returnUrl, defaultMethod, locale = 'z
458
746
  const href = link.startsWith('http') || link.startsWith('/') ? link : `https://${link}`;
459
747
  window.open(href, '_blank', 'noopener,noreferrer');
460
748
  };
461
- return (_jsxs("div", { className: className, style: style, children: [contextHolder, _jsx(Tabs, { activeKey: activeMethod, items: tabItems, onChange: (value) => setActiveMethod(value), size: "large" }), _jsxs(Space, { direction: "vertical", size: 16, style: { width: '100%' }, children: [activeMethod === 'phone' && (_jsxs(Space, { direction: "vertical", size: 12, style: { width: '100%' }, children: [_jsx(Form.Item, { label: texts.countryCodePrefix, required: true, children: _jsxs(Space.Compact, { style: { width: '100%' }, children: [_jsx(Input, { value: countryCode, onChange: (e) => setCountryCode(e.target.value), style: { width: 90 } }), _jsx(Input, { value: phone, onChange: (e) => setPhone(e.target.value), placeholder: texts.phonePlaceholder })] }) }), _jsx(Form.Item, { label: texts.code, required: true, children: _jsxs(Space.Compact, { style: { width: '100%' }, children: [_jsx(Input, { value: phoneCode, onChange: (e) => setPhoneCode(e.target.value), placeholder: texts.codePlaceholder }), _jsx(Button, { icon: _jsx(SendOutlined, {}), onClick: () => sendCode('phone'), loading: isSending.phone, disabled: countdown.phone > 0, children: countdown.phone > 0 ? `${texts.resendPrefix}(${countdown.phone}s)` : texts.sendCode })] }) })] })), activeMethod === 'email' && (_jsxs(Space, { direction: "vertical", size: 12, style: { width: '100%' }, children: [_jsx(Form.Item, { label: texts.email, required: true, children: _jsx(Input, { value: email, onChange: (e) => setEmail(e.target.value), placeholder: texts.emailPlaceholder }) }), _jsx(Form.Item, { label: texts.code, required: true, children: _jsxs(Space.Compact, { style: { width: '100%' }, children: [_jsx(Input, { value: emailCode, onChange: (e) => setEmailCode(e.target.value), placeholder: texts.codePlaceholder }), _jsx(Button, { onClick: () => sendCode('email'), loading: isSending.email, disabled: countdown.email > 0, children: countdown.email > 0 ? `${texts.resendPrefix}(${countdown.email}s)` : texts.sendCode })] }) })] })), activeMethod === 'whatsapp' && (_jsxs(Space, { direction: "vertical", size: 12, style: { width: '100%' }, children: [_jsx(Form.Item, { label: texts.whatsapp, required: true, children: _jsx(Input, { value: whatsapp, onChange: (e) => setWhatsapp(e.target.value), placeholder: texts.whatsappPlaceholder }) }), _jsx(Form.Item, { label: texts.code, required: true, children: _jsxs(Space.Compact, { style: { width: '100%' }, children: [_jsx(Input, { value: whatsappCode, onChange: (e) => setWhatsappCode(e.target.value), placeholder: texts.codePlaceholder }), _jsx(Button, { onClick: () => sendCode('whatsapp'), loading: isSending.whatsapp, disabled: countdown.whatsapp > 0, children: countdown.whatsapp > 0 ? `${texts.resendPrefix}(${countdown.whatsapp}s)` : texts.sendCode })] }) })] })), activeMethod === 'wechat' && (_jsx(Button, { block: true, size: "large", onClick: () => openOAuth('wechat'), icon: _jsx(WechatOutlined, {}), children: texts.wechat })), activeMethod === 'wecom' && (_jsx(Button, { block: true, size: "large", onClick: () => openOAuth('wecom'), icon: _jsx(UserOutlined, {}), children: texts.wecom })), (activeMethod === 'phone' || activeMethod === 'email' || activeMethod === 'whatsapp') && (_jsx(Button, { type: "primary", size: "large", loading: isSubmitting, disabled: !canSubmit, onClick: handleVerify, children: isSubmitting ? texts.verifying : texts.login }))] }), _jsx(Modal, { open: Boolean(consentData), title: _jsx(Title, { level: 4, children: texts.consentTitle }), footer: null, onCancel: () => {
749
+ const rootClassName = ['runid-login-widget', className].filter(Boolean).join(' ');
750
+ const inputStyle = {
751
+ height: 52,
752
+ borderRadius: 8,
753
+ fontSize: 15,
754
+ };
755
+ const sendButtonStyle = {
756
+ minWidth: 120,
757
+ height: 52,
758
+ borderRadius: 8,
759
+ fontWeight: 500,
760
+ };
761
+ const primaryButtonStyle = {
762
+ height: 56,
763
+ borderRadius: 8,
764
+ fontSize: 16,
765
+ fontWeight: 600,
766
+ marginTop: 12,
767
+ boxShadow: t.buttonShadow,
768
+ };
769
+ const oauthButtonStyle = {
770
+ height: 56,
771
+ borderRadius: 8,
772
+ fontSize: 16,
773
+ fontWeight: 600,
774
+ width: '100%',
775
+ };
776
+ const methodButtonStyle = (method) => ({
777
+ flex: 1,
778
+ height: 40,
779
+ borderRadius: 6,
780
+ border: 'none',
781
+ boxShadow: activeMethod === method ? t.activeTabShadow : 'none',
782
+ background: activeMethod === method ? undefined : 'transparent',
783
+ color: activeMethod === method ? t.activeTabColor : t.inactiveTabColor,
784
+ fontWeight: activeMethod === method ? 600 : 400,
785
+ });
786
+ const socialButtonStyle = {
787
+ borderRadius: 8,
788
+ height: 44,
789
+ padding: '0 24px',
790
+ color: t.socialBtnColor,
791
+ };
792
+ const submitLabel = isSubmitting ? texts.verifying : texts.login;
793
+ return (_jsxs("div", { className: rootClassName, "data-active-method": activeMethod, style: style, children: [contextHolder, primaryMethods.length > 0 && (_jsx(Flex, { gap: 4, className: "login-method-tabs animate-fade-in-up delay-200", style: { width: '100%', marginBottom: 24, background: t.surface, border: `1px solid ${t.surfaceBorder}`, padding: 4, borderRadius: 8 }, children: primaryMethods.map((method) => (_jsx(Button, { type: activeMethod === method.key ? 'primary' : 'text', onClick: () => setActiveMethod(method.key), className: `login-method-tab login-method-tab-${method.key}`, style: methodButtonStyle(method.key), icon: method.icon, children: method.label }, method.key))) })), _jsxs("div", { className: "login-content-area animate-scale-in", style: { minHeight: 320 }, children: [activeMethod === 'phone' && (_jsxs(Flex, { vertical: true, gap: 24, style: { padding: '10px 0' }, children: [isInternalProduct && (_jsx("div", { style: { padding: '10px 14px', borderRadius: 6, background: t.noticeBg, border: `1px solid ${t.noticeBorder}`, fontSize: 12, color: t.noticeText, lineHeight: 1.6 }, children: locale === 'zh-CN'
794
+ ? '仅限内部成员使用已登记的手机号登录,未登记的手机号无法登录。'
795
+ : 'Only registered internal phone numbers can sign in.' })), _jsxs(Flex, { vertical: true, gap: 16, children: [_jsxs(Flex, { gap: 12, children: [_jsx(Select, { value: countryCode, disabled: true, options: countryCodeSelectOptions, size: "large", style: { width: 100, height: 52, borderRadius: 8 } }), _jsx(Input, { size: "large", value: phone, onChange: (e) => setPhone(phoneDigits(e.target.value).slice(0, 20)), placeholder: texts.phonePlaceholder, style: { ...inputStyle, flex: 1 } })] }), _jsxs(Flex, { gap: 12, children: [_jsx(Input, { size: "large", value: phoneCode, onChange: (e) => setPhoneCode(e.target.value.replace(/\D/g, '').slice(0, 6)), placeholder: texts.codePlaceholder, style: { ...inputStyle, flex: 1 } }), _jsx(Button, { size: "large", onClick: () => sendCode('phone'), loading: isSending.phone, disabled: countdown.phone > 0, style: sendButtonStyle, children: countdown.phone > 0 ? `${countdown.phone}s` : texts.sendCode })] })] }), _jsxs(Button, { type: "primary", size: "large", block: true, loading: isSubmitting, disabled: !canSubmit, onClick: handleVerify, style: primaryButtonStyle, children: [_jsx("span", { children: submitLabel }), _jsx(ArrowRightOutlined, { style: { marginLeft: 8 } })] })] })), activeMethod === 'email' && (_jsxs(Flex, { vertical: true, gap: 24, style: { padding: '10px 0' }, children: [isInternalProduct && (_jsx("div", { style: { padding: '10px 14px', borderRadius: 6, background: t.noticeBg, border: `1px solid ${t.noticeBorder}`, fontSize: 12, color: t.noticeText, lineHeight: 1.6 }, children: locale === 'zh-CN'
796
+ ? '仅限内部成员使用企业邮箱登录,非内部邮箱无法登录。'
797
+ : 'Only internal organization email accounts can sign in.' })), _jsxs(Flex, { vertical: true, gap: 16, children: [_jsx(Input, { size: "large", value: email, onChange: (e) => setEmail(e.target.value.slice(0, 120)), placeholder: texts.emailPlaceholder, prefix: _jsx(MailOutlined, { style: { color: t.inputIcon, marginRight: 8 } }), style: inputStyle }), _jsxs(Flex, { gap: 12, children: [_jsx(Input, { size: "large", value: emailCode, onChange: (e) => setEmailCode(e.target.value.replace(/\D/g, '').slice(0, 6)), placeholder: texts.codePlaceholder, style: { ...inputStyle, flex: 1 } }), _jsx(Button, { size: "large", onClick: () => sendCode('email'), loading: isSending.email, disabled: countdown.email > 0, style: sendButtonStyle, children: countdown.email > 0 ? `${countdown.email}s` : texts.sendCode })] })] }), _jsxs(Button, { type: "primary", size: "large", block: true, loading: isSubmitting, disabled: !canSubmit, onClick: handleVerify, style: primaryButtonStyle, children: [_jsx("span", { children: submitLabel }), _jsx(ArrowRightOutlined, { style: { marginLeft: 8 } })] })] })), activeMethod === 'whatsapp' && (_jsxs(Flex, { vertical: true, gap: 24, style: { padding: '10px 0' }, children: [_jsxs(Flex, { vertical: true, gap: 16, children: [_jsx(Input, { size: "large", value: whatsapp, onChange: (e) => setWhatsapp(phoneDigits(e.target.value).slice(0, 20)), placeholder: texts.whatsappPlaceholder, prefix: _jsx(MessageOutlined, { style: { color: t.inputIcon, marginRight: 8 } }), style: inputStyle }), _jsxs(Flex, { gap: 12, children: [_jsx(Input, { size: "large", value: whatsappCode, onChange: (e) => setWhatsappCode(e.target.value.replace(/\D/g, '').slice(0, 6)), placeholder: texts.codePlaceholder, style: { ...inputStyle, flex: 1 } }), _jsx(Button, { size: "large", onClick: () => sendCode('whatsapp'), loading: isSending.whatsapp, disabled: countdown.whatsapp > 0, style: sendButtonStyle, children: countdown.whatsapp > 0 ? `${countdown.whatsapp}s` : texts.sendCode })] })] }), _jsxs(Button, { type: "primary", size: "large", block: true, loading: isSubmitting, disabled: !canSubmit, onClick: handleVerify, style: primaryButtonStyle, children: [_jsx("span", { children: submitLabel }), _jsx(ArrowRightOutlined, { style: { marginLeft: 8 } })] })] })), activeMethod === 'wechat' && (_jsxs(Flex, { vertical: true, align: "center", justify: "center", gap: 20, style: { padding: '44px 0' }, children: [_jsx(Text, { style: { color: t.text, fontWeight: 500, textAlign: 'center' }, children: locale === 'zh-CN' ? '点击下方按钮完成微信授权' : 'Continue with WeChat authorization' }), _jsx(Button, { type: "primary", size: "large", onClick: () => openOAuth('wechat'), icon: _jsx(WechatOutlined, {}), style: { ...oauthButtonStyle, background: '#07c160', borderColor: '#07c160' }, children: texts.wechat })] })), activeMethod === 'wecom' && (_jsx(Flex, { className: "login-wecom-panel", vertical: true, align: "center", justify: "center", gap: 14, style: { padding: '28px 0 4px' }, children: wecomAutoRedirecting ? (_jsxs(Flex, { vertical: true, align: "center", justify: "center", gap: 14, style: { minHeight: 220 }, children: [_jsx(Spin, {}), _jsx(Text, { style: { color: t.textSecondary, fontSize: 13, textAlign: 'center' }, children: locale === 'zh-CN' ? '正在进入企业微信登录...' : 'Opening WeCom sign-in...' })] })) : (_jsxs(_Fragment, { children: [showWecomContinueButton && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "primary", size: "large", block: true, onClick: openWecomLogin, icon: _jsx(UserOutlined, {}), style: { ...oauthButtonStyle, background: '#07c160', borderColor: '#07c160' }, children: locale === 'zh-CN' ? '继续企业微信登录' : 'Continue with WeCom' }), _jsx(Text, { style: { color: t.textSecondary, fontSize: 12, textAlign: 'center' }, children: locale === 'zh-CN' ? '将进入企业微信授权页;已登录客户端时可直接确认' : 'Continue on the WeCom authorization page.' })] })), !showWecomContinueButton && (_jsx(Text, { style: { color: t.textSecondary, fontSize: 13, textAlign: 'center' }, children: locale === 'zh-CN' ? '使用企业微信扫码完成登录' : 'Scan with WeCom to sign in' })), showWecomContinueButton && !showWecomQR && (_jsx(Button, { type: "link", onClick: () => {
798
+ setShowWecomQR(true);
799
+ void loadWecomQR();
800
+ }, children: locale === 'zh-CN' ? '无法打开时使用扫码登录' : 'Use QR code if the app does not open' })), showWecomQRCode && (_jsxs(_Fragment, { children: [_jsxs("div", { className: "login-wecom-qr-frame", style: {
801
+ width: '100%',
802
+ maxWidth: 340,
803
+ height: 360,
804
+ overflow: 'hidden',
805
+ }, children: [(!showWecomQR || loadingWecomQR) && (_jsx(Flex, { align: "center", justify: "center", style: { height: 360 }, children: _jsx(Spin, {}) })), showWecomQR && !loadingWecomQR && wecomURLs?.auth_url && (_jsx("iframe", { className: "login-wecom-qr-iframe", title: locale === 'zh-CN' ? '企业微信扫码登录' : 'WeCom sign-in QR code', src: wecomURLs.auth_url, style: {
806
+ display: 'block',
807
+ width: 380,
808
+ height: 400,
809
+ border: 0,
810
+ background: t.qrBg,
811
+ transform: 'translateX(-20px) scale(0.9)',
812
+ transformOrigin: 'top center',
813
+ } })), showWecomQR && !loadingWecomQR && !wecomURLs?.auth_url && (_jsxs(Flex, { vertical: true, align: "center", justify: "center", gap: 12, style: { height: 360, padding: 24, textAlign: 'center' }, children: [_jsx(Text, { style: { color: t.textSecondary }, children: locale === 'zh-CN' ? '企业微信二维码加载失败' : 'Failed to load WeCom QR code' }), _jsx(Button, { onClick: loadWecomQR, children: locale === 'zh-CN' ? '重新加载' : 'Reload' })] }))] }), showWecomContinueButton && (_jsx(Text, { style: { color: t.textSecondary, fontSize: 13, textAlign: 'center' }, children: locale === 'zh-CN' ? '使用企业微信扫码完成登录' : 'Scan with WeCom to sign in' }))] }))] })) }))] }), showSocialMethods && (_jsxs(Flex, { justify: "center", gap: 16, style: { marginTop: 24 }, children: [wechatConfigured && (_jsx(Tooltip, { title: undefined, children: _jsx("span", { style: { display: 'inline-flex' }, children: _jsx(Button, { type: "text", size: "large", onClick: () => openOAuth('wechat'), className: "login-social-button login-social-button-wechat", icon: _jsx(WechatOutlined, { style: { fontSize: 18, color: '#94a0bf' } }), style: socialButtonStyle, children: texts.wechat }) }) })), whatsappConfigured && (_jsx(Tooltip, { title: undefined, children: _jsx("span", { style: { display: 'inline-flex' }, children: _jsx(Button, { type: activeMethod === 'whatsapp' ? 'primary' : 'text', size: "large", onClick: () => setActiveMethod('whatsapp'), className: "login-social-button login-social-button-whatsapp", icon: _jsx(MessageOutlined, { style: { fontSize: 18, color: activeMethod === 'whatsapp' ? '#fff' : '#94a0bf' } }), style: activeMethod === 'whatsapp' ? { ...socialButtonStyle, color: '#fff' } : socialButtonStyle, children: texts.whatsapp }) }) }))] })), _jsx(Modal, { open: Boolean(consentData), title: _jsx(Title, { level: 4, children: texts.consentTitle }), footer: null, onCancel: () => {
462
814
  setConsentData(null);
463
815
  setAgreed(false);
464
- }, destroyOnClose: true, children: consentData && (_jsxs(Space, { direction: "vertical", size: 12, style: { width: '100%' }, children: [_jsx(Paragraph, { children: consentData.is_new_user
816
+ }, destroyOnHidden: true, children: consentData && (_jsxs(Space, { direction: "vertical", size: 12, style: { width: '100%' }, children: [_jsx(Paragraph, { children: consentData.is_new_user
465
817
  ? texts.consentHintNewUser
466
818
  : texts.consentHintExistingUser }), _jsx(Space, { direction: "vertical", style: { width: '100%' }, children: consentData.pending_agreements.map((agreement) => (_jsx(Space, { align: "center", children: _jsx("a", { href: agreement.url, onClick: (event) => {
467
819
  event.preventDefault();