PersonalSetting.jsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useContext, useEffect, useState } from 'react';
  16. import { useNavigate } from 'react-router-dom';
  17. import {
  18. API,
  19. copy,
  20. showError,
  21. showInfo,
  22. showSuccess,
  23. setStatusData,
  24. prepareCredentialCreationOptions,
  25. buildRegistrationResult,
  26. isPasskeySupported,
  27. setUserData,
  28. } from '../../helpers';
  29. import { UserContext } from '../../context/User';
  30. import { Modal } from '@douyinfe/semi-ui';
  31. import { useTranslation } from 'react-i18next';
  32. // 导入子组件
  33. import UserInfoHeader from './personal/components/UserInfoHeader';
  34. import AccountManagement from './personal/cards/AccountManagement';
  35. import NotificationSettings from './personal/cards/NotificationSettings';
  36. import PreferencesSettings from './personal/cards/PreferencesSettings';
  37. import CheckinCalendar from './personal/cards/CheckinCalendar';
  38. import EmailBindModal from './personal/modals/EmailBindModal';
  39. import WeChatBindModal from './personal/modals/WeChatBindModal';
  40. import AccountDeleteModal from './personal/modals/AccountDeleteModal';
  41. import ChangePasswordModal from './personal/modals/ChangePasswordModal';
  42. import SecureVerificationModal from '../common/modals/SecureVerificationModal';
  43. import { useSecureVerification } from '../../hooks/common/useSecureVerification';
  44. const PersonalSetting = () => {
  45. const [userState, userDispatch] = useContext(UserContext);
  46. let navigate = useNavigate();
  47. const { t } = useTranslation();
  48. const [inputs, setInputs] = useState({
  49. wechat_verification_code: '',
  50. email_verification_code: '',
  51. email: '',
  52. self_account_deletion_confirmation: '',
  53. original_password: '',
  54. set_new_password: '',
  55. set_new_password_confirmation: '',
  56. });
  57. const [status, setStatus] = useState({});
  58. const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
  59. const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
  60. const [showEmailBindModal, setShowEmailBindModal] = useState(false);
  61. const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
  62. const [turnstileEnabled, setTurnstileEnabled] = useState(false);
  63. const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
  64. const [turnstileToken, setTurnstileToken] = useState('');
  65. const [loading, setLoading] = useState(false);
  66. const [disableButton, setDisableButton] = useState(false);
  67. const [countdown, setCountdown] = useState(30);
  68. const [systemToken, setSystemToken] = useState('');
  69. const [passkeyStatus, setPasskeyStatus] = useState({ enabled: false });
  70. const [passkeyRegisterLoading, setPasskeyRegisterLoading] = useState(false);
  71. const [passkeyDeleteLoading, setPasskeyDeleteLoading] = useState(false);
  72. const [passkeySupported, setPasskeySupported] = useState(false);
  73. const [
  74. passkeyRequiredVerificationMethod,
  75. setPasskeyRequiredVerificationMethod,
  76. ] = useState(null);
  77. const [notificationSettings, setNotificationSettings] = useState({
  78. warningType: 'email',
  79. warningThreshold: 100000,
  80. webhookUrl: '',
  81. webhookSecret: '',
  82. notificationEmail: '',
  83. barkUrl: '',
  84. gotifyUrl: '',
  85. gotifyToken: '',
  86. gotifyPriority: 5,
  87. upstreamModelUpdateNotifyEnabled: false,
  88. acceptUnsetModelRatioModel: false,
  89. recordIpLog: false,
  90. });
  91. const {
  92. isModalVisible: isPasskeyVerificationModalVisible,
  93. verificationMethods: passkeyVerificationMethods,
  94. verificationState: passkeyVerificationState,
  95. startVerification: startPasskeyVerification,
  96. executeVerification: executePasskeyVerification,
  97. cancelVerification: cancelPasskeyVerification,
  98. setVerificationCode: setPasskeyVerificationCode,
  99. switchVerificationMethod: switchPasskeyVerificationMethod,
  100. checkVerificationMethods: checkPasskeyVerificationMethods,
  101. } = useSecureVerification({
  102. onSuccess: () => {
  103. setPasskeyRequiredVerificationMethod(null);
  104. },
  105. });
  106. const visiblePasskeyVerificationMethods = passkeyRequiredVerificationMethod
  107. ? {
  108. ...passkeyVerificationMethods,
  109. has2FA:
  110. passkeyRequiredVerificationMethod === '2fa' &&
  111. passkeyVerificationMethods.has2FA,
  112. hasPasskey:
  113. passkeyRequiredVerificationMethod === 'passkey' &&
  114. passkeyVerificationMethods.hasPasskey,
  115. }
  116. : passkeyVerificationMethods;
  117. useEffect(() => {
  118. let saved = localStorage.getItem('status');
  119. if (saved) {
  120. const parsed = JSON.parse(saved);
  121. setStatus(parsed);
  122. if (parsed.turnstile_check) {
  123. setTurnstileEnabled(true);
  124. setTurnstileSiteKey(parsed.turnstile_site_key);
  125. } else {
  126. setTurnstileEnabled(false);
  127. setTurnstileSiteKey('');
  128. }
  129. }
  130. // Always refresh status from server to avoid stale flags (e.g., admin just enabled OAuth)
  131. (async () => {
  132. try {
  133. const res = await API.get('/api/status');
  134. const { success, data } = res.data;
  135. if (success && data) {
  136. setStatus(data);
  137. setStatusData(data);
  138. if (data.turnstile_check) {
  139. setTurnstileEnabled(true);
  140. setTurnstileSiteKey(data.turnstile_site_key);
  141. } else {
  142. setTurnstileEnabled(false);
  143. setTurnstileSiteKey('');
  144. }
  145. }
  146. } catch (e) {
  147. // ignore and keep local status
  148. }
  149. })();
  150. getUserData();
  151. isPasskeySupported()
  152. .then(setPasskeySupported)
  153. .catch(() => setPasskeySupported(false));
  154. }, []);
  155. useEffect(() => {
  156. let countdownInterval = null;
  157. if (disableButton && countdown > 0) {
  158. countdownInterval = setInterval(() => {
  159. setCountdown(countdown - 1);
  160. }, 1000);
  161. } else if (countdown === 0) {
  162. setDisableButton(false);
  163. setCountdown(30);
  164. }
  165. return () => clearInterval(countdownInterval); // Clean up on unmount
  166. }, [disableButton, countdown]);
  167. useEffect(() => {
  168. if (userState?.user?.setting) {
  169. const settings = JSON.parse(userState.user.setting);
  170. setNotificationSettings({
  171. warningType: settings.notify_type || 'email',
  172. warningThreshold: settings.quota_warning_threshold || 500000,
  173. webhookUrl: settings.webhook_url || '',
  174. webhookSecret: settings.webhook_secret || '',
  175. notificationEmail: settings.notification_email || '',
  176. barkUrl: settings.bark_url || '',
  177. gotifyUrl: settings.gotify_url || '',
  178. gotifyToken: settings.gotify_token || '',
  179. gotifyPriority:
  180. settings.gotify_priority !== undefined ? settings.gotify_priority : 5,
  181. upstreamModelUpdateNotifyEnabled:
  182. settings.upstream_model_update_notify_enabled === true,
  183. acceptUnsetModelRatioModel:
  184. settings.accept_unset_model_ratio_model || false,
  185. recordIpLog: settings.record_ip_log || false,
  186. });
  187. }
  188. }, [userState?.user?.setting]);
  189. const handleInputChange = (name, value) => {
  190. setInputs((inputs) => ({ ...inputs, [name]: value }));
  191. };
  192. const generateAccessToken = async () => {
  193. const res = await API.get('/api/user/token');
  194. const { success, message, data } = res.data;
  195. if (success) {
  196. setSystemToken(data);
  197. await copy(data);
  198. showSuccess(t('令牌已重置并已复制到剪贴板'));
  199. } else {
  200. showError(message);
  201. }
  202. };
  203. const loadPasskeyStatus = async () => {
  204. try {
  205. const res = await API.get('/api/user/passkey');
  206. const { success, data, message } = res.data;
  207. if (success) {
  208. setPasskeyStatus({
  209. enabled: data?.enabled || false,
  210. last_used_at: data?.last_used_at || null,
  211. backup_eligible: data?.backup_eligible || false,
  212. backup_state: data?.backup_state || false,
  213. });
  214. } else {
  215. showError(message);
  216. }
  217. } catch (error) {
  218. // 忽略错误,保留默认状态
  219. }
  220. };
  221. const startPasskeyManagementVerification = async (apiCall, options = {}) => {
  222. const methods = await checkPasskeyVerificationMethods();
  223. const requiredMethod = methods.has2FA
  224. ? '2fa'
  225. : methods.hasPasskey
  226. ? 'passkey'
  227. : null;
  228. if (!requiredMethod) {
  229. showError(t('您需要先启用两步验证或 Passkey 才能执行此操作'));
  230. return;
  231. }
  232. if (requiredMethod === 'passkey' && !methods.passkeySupported) {
  233. showInfo(t('当前设备不支持 Passkey'));
  234. return;
  235. }
  236. setPasskeyRequiredVerificationMethod(requiredMethod);
  237. await startPasskeyVerification(apiCall, {
  238. preferredMethod: requiredMethod,
  239. title: t('安全验证'),
  240. ...options,
  241. });
  242. };
  243. const startPasskeyRegistration = async () => {
  244. const methods = await checkPasskeyVerificationMethods();
  245. if (!methods.has2FA) {
  246. try {
  247. await registerPasskey();
  248. } catch (error) {
  249. showError(error.message || t('Passkey 注册失败,请重试'));
  250. }
  251. return;
  252. }
  253. setPasskeyRequiredVerificationMethod('2fa');
  254. await startPasskeyVerification(registerPasskey, {
  255. preferredMethod: '2fa',
  256. title: t('安全验证'),
  257. });
  258. };
  259. const registerPasskey = async () => {
  260. setPasskeyRegisterLoading(true);
  261. try {
  262. const beginRes = await API.post('/api/user/passkey/register/begin');
  263. const { success, message, data } = beginRes.data;
  264. if (!success) {
  265. throw new Error(message || t('无法发起 Passkey 注册'));
  266. }
  267. const publicKey = prepareCredentialCreationOptions(
  268. data?.options || data?.publicKey || data,
  269. );
  270. const credential = await navigator.credentials.create({ publicKey });
  271. const payload = buildRegistrationResult(credential);
  272. if (!payload) {
  273. throw new Error(t('Passkey 注册失败,请重试'));
  274. }
  275. const finishRes = await API.post(
  276. '/api/user/passkey/register/finish',
  277. payload,
  278. );
  279. if (!finishRes.data.success) {
  280. throw new Error(
  281. finishRes.data.message || t('Passkey 注册失败,请重试'),
  282. );
  283. }
  284. showSuccess(t('Passkey 注册成功'));
  285. await loadPasskeyStatus();
  286. return finishRes.data;
  287. } catch (error) {
  288. if (error?.name === 'AbortError') {
  289. showInfo(t('已取消 Passkey 注册'));
  290. return { cancelled: true };
  291. }
  292. throw new Error(error?.message || t('Passkey 注册失败,请重试'));
  293. } finally {
  294. setPasskeyRegisterLoading(false);
  295. }
  296. };
  297. const handleRegisterPasskey = async () => {
  298. if (!passkeySupported || !window.PublicKeyCredential) {
  299. showInfo(t('当前设备不支持 Passkey'));
  300. return;
  301. }
  302. await startPasskeyRegistration();
  303. };
  304. const removePasskey = async () => {
  305. setPasskeyDeleteLoading(true);
  306. try {
  307. const res = await API.delete('/api/user/passkey');
  308. const { success, message } = res.data;
  309. if (!success) {
  310. throw new Error(message || t('操作失败,请重试'));
  311. }
  312. showSuccess(t('Passkey 已解绑'));
  313. await loadPasskeyStatus();
  314. return res.data;
  315. } catch (error) {
  316. throw new Error(error?.message || t('操作失败,请重试'));
  317. } finally {
  318. setPasskeyDeleteLoading(false);
  319. }
  320. };
  321. const handleRemovePasskey = async () => {
  322. await startPasskeyManagementVerification(removePasskey);
  323. };
  324. const handlePasskeyVerificationCancel = () => {
  325. setPasskeyRequiredVerificationMethod(null);
  326. cancelPasskeyVerification();
  327. };
  328. const getUserData = async () => {
  329. let res = await API.get(`/api/user/self`);
  330. const { success, message, data } = res.data;
  331. if (success) {
  332. userDispatch({ type: 'login', payload: data });
  333. setUserData(data);
  334. await loadPasskeyStatus();
  335. } else {
  336. showError(message);
  337. }
  338. };
  339. const handleSystemTokenClick = async (e) => {
  340. e.target.select();
  341. await copy(e.target.value);
  342. showSuccess(t('系统令牌已复制到剪切板'));
  343. };
  344. const deleteAccount = async () => {
  345. if (inputs.self_account_deletion_confirmation !== userState.user.username) {
  346. showError(t('请输入你的账户名以确认删除!'));
  347. return;
  348. }
  349. const res = await API.delete('/api/user/self');
  350. const { success, message } = res.data;
  351. if (success) {
  352. showSuccess(t('账户已删除!'));
  353. await API.get('/api/user/logout');
  354. userDispatch({ type: 'logout' });
  355. localStorage.removeItem('user');
  356. navigate('/login');
  357. } else {
  358. showError(message);
  359. }
  360. };
  361. const bindWeChat = async () => {
  362. if (inputs.wechat_verification_code === '') return;
  363. const res = await API.post('/api/oauth/wechat/bind', {
  364. code: inputs.wechat_verification_code,
  365. });
  366. const { success, message } = res.data;
  367. if (success) {
  368. showSuccess(t('微信账户绑定成功!'));
  369. setShowWeChatBindModal(false);
  370. } else {
  371. showError(message);
  372. }
  373. };
  374. const changePassword = async () => {
  375. // if (inputs.original_password === '') {
  376. // showError(t('请输入原密码!'));
  377. // return;
  378. // }
  379. if (inputs.set_new_password === '') {
  380. showError(t('请输入新密码!'));
  381. return;
  382. }
  383. if (inputs.original_password === inputs.set_new_password) {
  384. showError(t('新密码需要和原密码不一致!'));
  385. return;
  386. }
  387. if (inputs.set_new_password !== inputs.set_new_password_confirmation) {
  388. showError(t('两次输入的密码不一致!'));
  389. return;
  390. }
  391. const res = await API.put(`/api/user/self`, {
  392. original_password: inputs.original_password,
  393. password: inputs.set_new_password,
  394. });
  395. const { success, message } = res.data;
  396. if (success) {
  397. showSuccess(t('密码修改成功!'));
  398. setShowWeChatBindModal(false);
  399. } else {
  400. showError(message);
  401. }
  402. setShowChangePasswordModal(false);
  403. };
  404. const sendVerificationCode = async () => {
  405. if (inputs.email === '') {
  406. showError(t('请输入邮箱!'));
  407. return;
  408. }
  409. setDisableButton(true);
  410. if (turnstileEnabled && turnstileToken === '') {
  411. showInfo(t('请稍后几秒重试,Turnstile 正在检查用户环境!'));
  412. return;
  413. }
  414. setLoading(true);
  415. const res = await API.get(
  416. `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
  417. );
  418. const { success, message } = res.data;
  419. if (success) {
  420. showSuccess(t('验证码发送成功,请检查邮箱!'));
  421. } else {
  422. showError(message);
  423. }
  424. setLoading(false);
  425. };
  426. const bindEmail = async () => {
  427. if (inputs.email_verification_code === '') {
  428. showError(t('请输入邮箱验证码!'));
  429. return;
  430. }
  431. setLoading(true);
  432. const res = await API.post('/api/oauth/email/bind', {
  433. email: inputs.email,
  434. code: inputs.email_verification_code,
  435. });
  436. const { success, message } = res.data;
  437. if (success) {
  438. showSuccess(t('邮箱账户绑定成功!'));
  439. setShowEmailBindModal(false);
  440. userState.user.email = inputs.email;
  441. } else {
  442. showError(message);
  443. }
  444. setLoading(false);
  445. };
  446. const copyText = async (text) => {
  447. if (await copy(text)) {
  448. showSuccess(t('已复制:') + text);
  449. } else {
  450. // setSearchKeyword(text);
  451. Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
  452. }
  453. };
  454. const handleNotificationSettingChange = (type, value) => {
  455. setNotificationSettings((prev) => ({
  456. ...prev,
  457. [type]: value.target
  458. ? value.target.value !== undefined
  459. ? value.target.value
  460. : value.target.checked
  461. : value, // handle checkbox properly
  462. }));
  463. };
  464. const saveNotificationSettings = async () => {
  465. try {
  466. const res = await API.put('/api/user/setting', {
  467. notify_type: notificationSettings.warningType,
  468. quota_warning_threshold: parseFloat(
  469. notificationSettings.warningThreshold,
  470. ),
  471. webhook_url: notificationSettings.webhookUrl,
  472. webhook_secret: notificationSettings.webhookSecret,
  473. notification_email: notificationSettings.notificationEmail,
  474. bark_url: notificationSettings.barkUrl,
  475. gotify_url: notificationSettings.gotifyUrl,
  476. gotify_token: notificationSettings.gotifyToken,
  477. gotify_priority: (() => {
  478. const parsed = parseInt(notificationSettings.gotifyPriority);
  479. return isNaN(parsed) ? 5 : parsed;
  480. })(),
  481. upstream_model_update_notify_enabled:
  482. notificationSettings.upstreamModelUpdateNotifyEnabled === true,
  483. accept_unset_model_ratio_model:
  484. notificationSettings.acceptUnsetModelRatioModel,
  485. record_ip_log: notificationSettings.recordIpLog,
  486. });
  487. if (res.data.success) {
  488. showSuccess(t('设置保存成功'));
  489. await getUserData();
  490. } else {
  491. showError(res.data.message);
  492. }
  493. } catch (error) {
  494. showError(t('设置保存失败'));
  495. }
  496. };
  497. return (
  498. <div className='mt-[60px]'>
  499. <div className='flex justify-center'>
  500. <div className='w-full max-w-7xl mx-auto px-2'>
  501. {/* 顶部用户信息区域 */}
  502. <UserInfoHeader t={t} userState={userState} />
  503. {/* 签到日历 - 仅在启用时显示 */}
  504. {status?.checkin_enabled && (
  505. <div className='mt-4 md:mt-6'>
  506. <CheckinCalendar
  507. t={t}
  508. status={status}
  509. turnstileEnabled={turnstileEnabled}
  510. turnstileSiteKey={turnstileSiteKey}
  511. />
  512. </div>
  513. )}
  514. {/* 账户管理和其他设置 */}
  515. <div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
  516. {/* 左侧:账户管理设置 */}
  517. <div className='flex flex-col gap-4 md:gap-6'>
  518. <AccountManagement
  519. t={t}
  520. userState={userState}
  521. status={status}
  522. systemToken={systemToken}
  523. setShowEmailBindModal={setShowEmailBindModal}
  524. setShowWeChatBindModal={setShowWeChatBindModal}
  525. generateAccessToken={generateAccessToken}
  526. handleSystemTokenClick={handleSystemTokenClick}
  527. setShowChangePasswordModal={setShowChangePasswordModal}
  528. setShowAccountDeleteModal={setShowAccountDeleteModal}
  529. passkeyStatus={passkeyStatus}
  530. passkeySupported={passkeySupported}
  531. passkeyRegisterLoading={passkeyRegisterLoading}
  532. passkeyDeleteLoading={passkeyDeleteLoading}
  533. onPasskeyRegister={handleRegisterPasskey}
  534. onPasskeyDelete={handleRemovePasskey}
  535. />
  536. {/* 偏好设置(语言等) */}
  537. <PreferencesSettings t={t} />
  538. </div>
  539. {/* 右侧:其他设置 */}
  540. <NotificationSettings
  541. t={t}
  542. notificationSettings={notificationSettings}
  543. handleNotificationSettingChange={handleNotificationSettingChange}
  544. saveNotificationSettings={saveNotificationSettings}
  545. />
  546. </div>
  547. </div>
  548. </div>
  549. {/* 模态框组件 */}
  550. <EmailBindModal
  551. t={t}
  552. showEmailBindModal={showEmailBindModal}
  553. setShowEmailBindModal={setShowEmailBindModal}
  554. inputs={inputs}
  555. handleInputChange={handleInputChange}
  556. sendVerificationCode={sendVerificationCode}
  557. bindEmail={bindEmail}
  558. disableButton={disableButton}
  559. loading={loading}
  560. countdown={countdown}
  561. turnstileEnabled={turnstileEnabled}
  562. turnstileSiteKey={turnstileSiteKey}
  563. setTurnstileToken={setTurnstileToken}
  564. />
  565. <WeChatBindModal
  566. t={t}
  567. showWeChatBindModal={showWeChatBindModal}
  568. setShowWeChatBindModal={setShowWeChatBindModal}
  569. inputs={inputs}
  570. handleInputChange={handleInputChange}
  571. bindWeChat={bindWeChat}
  572. status={status}
  573. />
  574. <AccountDeleteModal
  575. t={t}
  576. showAccountDeleteModal={showAccountDeleteModal}
  577. setShowAccountDeleteModal={setShowAccountDeleteModal}
  578. inputs={inputs}
  579. handleInputChange={handleInputChange}
  580. deleteAccount={deleteAccount}
  581. userState={userState}
  582. turnstileEnabled={turnstileEnabled}
  583. turnstileSiteKey={turnstileSiteKey}
  584. setTurnstileToken={setTurnstileToken}
  585. />
  586. <ChangePasswordModal
  587. t={t}
  588. showChangePasswordModal={showChangePasswordModal}
  589. setShowChangePasswordModal={setShowChangePasswordModal}
  590. inputs={inputs}
  591. handleInputChange={handleInputChange}
  592. changePassword={changePassword}
  593. turnstileEnabled={turnstileEnabled}
  594. turnstileSiteKey={turnstileSiteKey}
  595. setTurnstileToken={setTurnstileToken}
  596. />
  597. <SecureVerificationModal
  598. visible={isPasskeyVerificationModalVisible}
  599. verificationMethods={visiblePasskeyVerificationMethods}
  600. verificationState={passkeyVerificationState}
  601. onVerify={executePasskeyVerification}
  602. onCancel={handlePasskeyVerificationCancel}
  603. onCodeChange={setPasskeyVerificationCode}
  604. onMethodSwitch={switchPasskeyVerificationMethod}
  605. title={passkeyVerificationState.title}
  606. description={passkeyVerificationState.description}
  607. />
  608. </div>
  609. );
  610. };
  611. export default PersonalSetting;