fix: 修复登录报错key问题

This commit is contained in:
电摇小子
2026-07-14 11:07:35 +08:00
parent aebf56b3f7
commit eaf8e9b07d
11 changed files with 215 additions and 77 deletions
+40 -38
View File
@@ -5,6 +5,7 @@ import path from 'path'
import { promisify } from 'util'
import { isValidDatabaseKey } from './database-key-store'
import crypto from 'crypto'
import { findResource, getResourceCandidates } from './resource-paths'
const execFileAsync = promisify(execFile)
@@ -25,33 +26,10 @@ export interface ImageKeyResult {
export class KeyServiceMac {
private getHelperPath(): string {
// 多 candidate fallback:覆盖 extraResources、asarUnpack、dev 三种场景
// (extraResources → Contents/Resources/resources/;asarUnpack 同路径;dev → cwd 或 app.getAppPath)
const candidates = [
// 1) extraResources 标准位置(electron-builder.yml 配的就是这个)
path.join(process.resourcesPath, 'resources', 'xkey_helper'),
// 2) process.resourcesPath 直接(防止 extraResources 没复制成功)
path.join(process.resourcesPath, 'xkey_helper'),
// 3) asarUnpack 路径(如果在 asar 内的 resources/ 被解包到 app.asar.unpacked)
path.join(app.getAppPath(), 'app.asar.unpacked', 'resources', 'xkey_helper'),
// 4) dev 模式 + 打包后某些版本 app.getAppPath() 也指向 .app 根目录
path.join(app.getAppPath(), 'resources', 'xkey_helper'),
// 5) dev 模式:cwd
path.join(process.cwd(), 'resources', 'xkey_helper')
].filter((p, idx, arr) => arr.indexOf(p) === idx) // 去重
// 诊断:即使命中也打 log,方便排查"装了但找不到"的问题(translocation / quarantine)
const statusList = candidates.map((candidate) => ({
path: candidate,
exists: fs.existsSync(candidate)
}))
console.log('[KeyServiceMac] xkey_helper candidates:', JSON.stringify(statusList))
const helperPath = candidates.find((candidate) => fs.existsSync(candidate))
const helperPath = findResource('xkey_helper')
if (!helperPath) {
throw new Error(
`找不到 xkey_helper(尝试 ${candidates.length} 个路径;` +
` app.isPackaged=${app.isPackaged} resourcesPath=${process.resourcesPath} ` +
` appPath=${app.getAppPath()} cwd=${process.cwd()})`
`找不到 xkey_helper(已检查:${getResourceCandidates('xkey_helper').join('')}`
)
}
return helperPath
@@ -163,7 +141,7 @@ export class KeyServiceMac {
'return "ERR::" & errNum & "::" & errMsg',
'end try'
]
onStatus?.('授权后请保持微信登录并活动...')
onStatus?.('授权后请保持微信登录界面 并点击登录微信...')
const { stdout } = await execFileAsync(
'/usr/bin/osascript',
scriptLines.flatMap((line) => ['-e', line]),
@@ -207,7 +185,8 @@ export class KeyServiceMac {
const orderedWxids: string[] = []
this.pushAccountIdCandidates(orderedWxids, path.basename(candidateAccountPath))
for (const candidate of wxidCandidates) this.pushAccountIdCandidates(orderedWxids, candidate)
for (const candidate of wxidCandidates)
this.pushAccountIdCandidates(orderedWxids, candidate)
onStatus?.(`正在校验候选 wxid${orderedWxids.length} 个)...`)
for (const candidateWxid of orderedWxids) {
@@ -255,20 +234,33 @@ export class KeyServiceMac {
private getKvcommCandidates(accountPath?: string): string[] {
const home = app.getPath('home')
const candidates = new Set<string>([
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm'),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm'),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm'),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Documents/app_data/net/kvcomm'
),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/xwechat/net/kvcomm'
),
path.join(
home,
'Library/Containers/com.tencent.xinWeChat/Data/Library/Application Support/com.tencent.xinWeChat/net/kvcomm'
),
path.join(home, 'Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat/net/kvcomm')
])
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
const marker = '/xwechat_files'
const markerIndex = normalized.indexOf(marker)
if (markerIndex >= 0) {
candidates.add(`${normalized.slice(0, markerIndex)}/app_data/net/kvcomm`)
}
const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/)
const newPathMatch = normalized.match(
/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))/
)
if (newPathMatch) {
candidates.add(`${newPathMatch[1]}/net/kvcomm`)
candidates.add(`${newPathMatch[1]}/xwechat/net/kvcomm`)
@@ -281,7 +273,9 @@ export class KeyServiceMac {
const candidates: string[] = []
this.pushAccountIdCandidates(candidates, wxidParam)
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
if (normalized) {
this.pushAccountIdCandidates(candidates, path.basename(normalized))
const root = this.resolveXwechatRootFromPath(normalized)
@@ -290,7 +284,8 @@ export class KeyServiceMac {
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const entryPath = path.join(root, entry.name)
if (this.isAccountDirPath(entryPath)) this.pushAccountIdCandidates(candidates, entry.name)
if (this.isAccountDirPath(entryPath))
this.pushAccountIdCandidates(candidates, entry.name)
}
} catch {
// Ignore unreadable directories.
@@ -316,7 +311,8 @@ export class KeyServiceMac {
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory()) continue
const entryPath = path.join(root, entry.name)
if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name)) push(entryPath)
if (this.isAccountDirPath(entryPath) && this.isReasonableAccountId(entry.name))
push(entryPath)
}
} catch {
// Ignore unreadable directories.
@@ -326,12 +322,16 @@ export class KeyServiceMac {
}
private resolveXwechatRootFromPath(accountPath?: string): string | null {
const normalized = String(accountPath || '').replace(/\\/g, '/').replace(/\/+$/, '')
const normalized = String(accountPath || '')
.replace(/\\/g, '/')
.replace(/\/+$/, '')
if (!normalized) return null
const marker = '/xwechat_files'
const markerIndex = normalized.indexOf(marker)
if (markerIndex >= 0) return normalized.slice(0, markerIndex + marker.length)
const newPathMatch = normalized.match(/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/)
const newPathMatch = normalized.match(
/^(.*\/com\.tencent\.xinWeChat\/(?:\d+\.\d+b\d+\.\d+|\d+\.\d+\.\d+))(\/|$)/
)
return newPathMatch ? newPathMatch[1] : null
}
@@ -366,7 +366,9 @@ export class KeyServiceMac {
}
private isReasonableAccountId(value: string): boolean {
const lowered = String(value || '').trim().toLowerCase()
const lowered = String(value || '')
.trim()
.toLowerCase()
if (!lowered || lowered.includes('/') || lowered.includes('\\')) return false
return !['xwechat_files', 'all_users', 'backup', 'wmpf', 'app_data'].includes(lowered)
}
+2 -9
View File
@@ -1,13 +1,13 @@
// @ts-nocheck
// Windows native bridge adapted from WeFlow. Koffi Win32 callbacks and optional
// helpers are intentionally dynamic; keep this file isolated from strict TS.
import { app } from 'electron'
import { join, dirname, delimiter } from 'path'
import { existsSync, copyFileSync, mkdirSync } from 'fs'
import { execFile, spawn } from 'child_process'
import { promisify } from 'util'
import os from 'os'
import crypto from 'crypto'
import { getResourceRoots as getSharedResourceRoots } from './resource-paths'
const execFileAsync = promisify(execFile)
@@ -69,14 +69,7 @@ export class KeyService {
private readonly DB_KEY_PROCESS_CHECK_INTERVAL_MS = 1000
private getResourceRoots(): string[] {
const roots = [
join(process.cwd(), 'resources'),
join(process.cwd(), 'resources', 'resources'),
join(process.resourcesPath || '', 'resources'),
process.resourcesPath || '',
join(app.getAppPath(), 'resources')
]
return Array.from(new Set(roots.filter((root) => root && existsSync(root))))
return getSharedResourceRoots()
}
private getDllPath(): string {
+35
View File
@@ -0,0 +1,35 @@
import { app } from 'electron'
import { existsSync } from 'fs'
import { dirname, join } from 'path'
function unique(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean)))
}
export function getResourceRoots(): string[] {
const appPath = app.getAppPath()
const appPathDir = dirname(appPath)
const execDir = dirname(process.execPath)
return unique([
process.env.WECHATEXPLORER_RESOURCES_PATH || '',
join(process.cwd(), 'resources'),
join(process.cwd(), 'resources', 'resources'),
join(process.resourcesPath || '', 'resources'),
process.resourcesPath || '',
join(appPath, 'resources'),
join(appPathDir, 'resources'),
appPathDir,
join(execDir, 'resources'),
join(dirname(execDir), 'Resources', 'resources'),
join(dirname(execDir), 'Resources')
]).filter((root) => existsSync(root))
}
export function getResourceCandidates(relativePath: string): string[] {
return unique(getResourceRoots().map((root) => join(root, relativePath)))
}
export function findResource(relativePath: string): string | null {
return getResourceCandidates(relativePath).find((candidate) => existsSync(candidate)) || null
}
+35 -28
View File
@@ -4,6 +4,7 @@ import os from 'os'
import crypto from 'crypto'
import { createRequire } from 'module'
import { createConnection, Socket } from 'net'
import { getResourceRoots } from './resource-paths'
export interface Wcdb4Session {
username: string
@@ -81,14 +82,14 @@ export function bootstrapWcdbNative(libPath?: string, libDirOverride?: string):
resourcePath: string
) => number
const resourceRoots = Array.from(
new Set([
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
path.join(process.resourcesPath || process.cwd(), 'resources'),
process.resourcesPath || process.cwd(),
path.join(process.cwd(), 'resources')
])
new Set(
[
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
...getResourceRoots()
].filter(Boolean)
)
)
let lastCode = -1
let initOk = false
@@ -1330,14 +1331,14 @@ export class Wcdb4Client {
) => number
const resourceRoots = Array.from(
new Set([
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
path.join(process.resourcesPath || process.cwd(), 'resources'),
process.resourcesPath || process.cwd(),
path.join(process.cwd(), 'resources')
])
new Set(
[
libDir,
path.dirname(libDir),
process.env.WCDB_RESOURCES_PATH || '',
...getResourceRoots()
].filter(Boolean)
)
)
let lastCode = -1
@@ -1373,19 +1374,21 @@ export class Wcdb4Client {
? 'libwcdb_api.so'
: 'wcdb_api.dll'
const platformDir =
process.platform === 'darwin' ? 'macos' : process.platform === 'win32' ? 'win32' : process.platform
process.platform === 'darwin'
? 'macos'
: process.platform === 'win32'
? 'win32'
: process.platform
const archDir = process.arch === 'arm64' ? 'arm64' : 'x64'
const resourcesPath = process.resourcesPath || process.cwd()
const resourceRoots = getResourceRoots()
const candidates = [
process.env.WCDB_DLL_PATH,
path.join(resourcesPath, 'resources', 'wcdb', platformDir, archDir, libName),
path.join(resourcesPath, 'resources', 'wcdb', platformDir, 'x64', libName),
path.join(process.cwd(), 'resources', 'wcdb', platformDir, archDir, libName),
path.join(process.cwd(), 'resources', 'wcdb', platformDir, 'x64', libName),
path.join(resourcesPath, 'resources', platformDir, libName),
path.join(resourcesPath, 'resources', libName),
path.join(process.cwd(), 'resources', platformDir, libName),
path.join(process.cwd(), 'resources', libName)
...resourceRoots.flatMap((root) => [
path.join(root, 'wcdb', platformDir, archDir, libName),
path.join(root, 'wcdb', platformDir, 'x64', libName),
path.join(root, platformDir, libName),
path.join(root, libName)
])
].filter(Boolean) as string[]
const found = candidates.find((candidate) => fs.existsSync(candidate))
@@ -1393,9 +1396,13 @@ export class Wcdb4Client {
throw new Error(`找不到 WCDB native 库: ${candidates.join(', ')}`)
}
if (process.platform === 'win32') {
const runtimeDir = path.join(resourcesPath, 'resources', 'runtime', 'win32')
const runtimeDir = resourceRoots
.map((root) => path.join(root, 'runtime', 'win32'))
.find((candidate) => fs.existsSync(candidate))
const dllDir = path.dirname(found)
process.env.PATH = [dllDir, runtimeDir, process.env.PATH || ''].filter(Boolean).join(path.delimiter)
process.env.PATH = [dllDir, runtimeDir || '', process.env.PATH || '']
.filter(Boolean)
.join(path.delimiter)
}
return found
}
+20
View File
@@ -624,6 +624,25 @@ function App(): React.ReactElement {
setDbKeyStatusKind('normal')
}
const handleReturnToLogin = (): void => {
setIsAuthenticated(false)
setIsDatabaseConnected(false)
setBootState('login')
setActivePage('archive')
setSettingsCategory('database-key')
setSelectedContact(null)
setMessages([])
setContacts([])
setFilteredContacts([])
setSelfInfo(null)
setIsMessagesLoading(false)
setIsNativeMonitorActive(false)
setReportNotice('')
setDbKeyStatus('已断开当前连接,可重新输入或获取数据库密钥')
setDbKeyStatusKind('normal')
setStartupProgress(null)
}
const getDateRangeParams = (
range: string
): { startTime: number | undefined; endTime: number | undefined } => {
@@ -1140,6 +1159,7 @@ function App(): React.ReactElement {
onSelfInfoChange={setSelfInfo}
onContactsChange={setContacts}
onFilteredContactsChange={setFilteredContacts}
onReturnToLogin={handleReturnToLogin}
onAIRuntimeChange={(config: AIRuntimeModelConfig) => setAiModelConfig(config)}
onNotice={setReportNotice}
onOpenSettings={openSettings}
+1 -1
View File
@@ -3825,7 +3825,7 @@ body {
.database-key-auto-error { display:grid; gap:4px; margin-top:16px; padding:12px 14px; border-left:3px solid #c68635; background:#fff8ec; color:#79501f; font-size:12px; }.database-key-auto-error span { overflow-wrap:anywhere; }.database-key-auto-error p { color:#79501f; }.database-key-auto-error button { justify-self:start; border:0; padding:4px 0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; }
.database-key-security-info { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1px; overflow:hidden; border:1px solid #e3e8e5; border-radius:9px; background:#e3e8e5; }.database-key-security-info span { display:grid; gap:6px; padding:15px; background:#f5f7f6; color:#66706b; font-size:12px; line-height:1.5; }.database-key-security-info strong { color:#35403b; font-size:13px; }
.database-key-diagnostics { margin-top:22px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-diagnostics summary { padding:14px 16px; color:#46514c; cursor:pointer; font-size:13px; font-weight:600; }.database-key-diagnostics dl { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px 28px; margin:0; padding:2px 16px 16px; }.database-key-diagnostics button { margin:0 16px 16px; border:0; padding:0; background:transparent; color:#247a63; cursor:pointer; font:600 12px inherit; }
.database-key-danger { margin-top:30px; padding-bottom:1px; }.database-key-danger>h2 { margin:0 0 14px; color:#c85a5a; font-size:15px; }.database-key-danger>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #efcdcd; background:#fff7f7; }.database-key-danger>div:first-of-type { border-radius:9px 9px 0 0; }.database-key-danger>div:last-of-type { border-top:0; border-radius:0 0 9px 9px; }.database-key-danger span { display:grid; gap:4px; }.database-key-danger strong { color:#4a3c3c; font-size:13px; }.database-key-danger small { color:#786868; font-size:12px; }.database-key-danger button { min-height:34px; flex:0 0 auto; border:1px solid #e8baba; border-radius:8px; padding:0 13px; background:#fff; color:#c85a5a; cursor:pointer; }.database-key-danger button:disabled { cursor:not-allowed; opacity:.45; }
.database-key-connection-actions { margin-top:30px; }.database-key-connection-actions>h2 { margin:0 0 14px; color:#3d4742; font-size:15px; }.database-key-connection-actions>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #dde3e0; border-radius:9px; background:#fff; }.database-key-connection-actions span { display:grid; gap:4px; }.database-key-connection-actions strong { color:#35403b; font-size:13px; }.database-key-connection-actions small { color:#66706b; font-size:12px; }.database-key-connection-actions button { min-height:34px; flex:0 0 auto; border:1px solid #247a63; border-radius:8px; padding:0 13px; background:#fff; color:#247a63; cursor:pointer; }.database-key-connection-actions button:hover:not(:disabled) { background:#f0f7f4; }.database-key-danger { margin-top:30px; padding-bottom:1px; }.database-key-danger>h2 { margin:0 0 14px; color:#c85a5a; font-size:15px; }.database-key-danger>div { display:flex; align-items:center; justify-content:space-between; gap:20px; padding:16px 20px; border:1px solid #efcdcd; background:#fff7f7; }.database-key-danger>div:first-of-type { border-radius:9px 9px 0 0; }.database-key-danger>div:last-of-type { border-top:0; border-radius:0 0 9px 9px; }.database-key-danger span { display:grid; gap:4px; }.database-key-danger strong { color:#4a3c3c; font-size:13px; }.database-key-danger small { color:#786868; font-size:12px; }.database-key-danger button { min-height:34px; flex:0 0 auto; border:1px solid #e8baba; border-radius:8px; padding:0 13px; background:#fff; color:#c85a5a; cursor:pointer; }.database-key-danger button:disabled { cursor:not-allowed; opacity:.45; }
.database-key-confirm-backdrop { position:fixed; z-index:30; inset:0; display:grid; place-items:center; padding:24px; background:rgba(25,32,29,.34); }.database-key-confirm { width:min(100%,430px); padding:22px; border-radius:12px; background:#fff; box-shadow:0 18px 50px rgba(24,35,30,.2); }.database-key-confirm h2 { margin:0; color:#202724; font-size:17px; }.database-key-confirm p { margin:12px 0 22px; color:#66706b; font-size:13px; line-height:1.7; }.database-key-confirm>div { display:flex; justify-content:flex-end; gap:8px; }.database-key-confirm button { min-height:34px; border:1px solid #dde3e0; border-radius:8px; padding:0 14px; background:#fff; color:#46514c; cursor:pointer; }.database-key-confirm button.danger { border-color:#c85a5a; background:#c85a5a; color:#fff; }
/* SETTINGS-03: image decryption */
.image-decryption-content { padding-bottom:48px; }
@@ -20,6 +20,7 @@ export function SettingsWorkspace({
onSelfInfoChange,
onContactsChange,
onFilteredContactsChange,
onReturnToLogin,
onAIRuntimeChange,
onNotice,
onOpenSettings
@@ -34,6 +35,7 @@ export function SettingsWorkspace({
onSelfInfoChange: (info: SettingsSelfInfo | null) => void
onContactsChange: (contacts: Contact[]) => void
onFilteredContactsChange: (contacts: Contact[]) => void
onReturnToLogin: () => void
onAIRuntimeChange: (config: AIRuntimeModelConfig) => void
onNotice: (message: string) => void
onOpenSettings: () => void
@@ -64,6 +66,7 @@ export function SettingsWorkspace({
onSelfInfoChange={onSelfInfoChange}
onContactsChange={onContactsChange}
onFilteredContactsChange={onFilteredContactsChange}
onReturnToLogin={onReturnToLogin}
onNotice={onNotice}
/>
) : selectedCategory === 'image-key' ? (
@@ -3,15 +3,30 @@ import { useState } from 'react'
export function DatabaseKeyDangerZone({
disabled,
onClear,
onReplace
onReplace,
onReturnToLogin
}: {
disabled: boolean
onClear: () => void
onReplace: () => void
onReturnToLogin: () => void
}): React.ReactElement {
const [confirming, setConfirming] = useState(false)
const [confirmingReturn, setConfirmingReturn] = useState(false)
return (
<>
<section className="database-key-connection-actions">
<h2></h2>
<div>
<span>
<strong></strong>
<small></small>
</span>
<button type="button" onClick={() => setConfirmingReturn(true)}>
</button>
</div>
</section>
<section className="database-key-danger">
<h2></h2>
<div>
@@ -69,6 +84,41 @@ export function DatabaseKeyDangerZone({
</div>
</div>
)}
{confirmingReturn && (
<div
className="database-key-confirm-backdrop"
role="presentation"
onMouseDown={() => setConfirmingReturn(false)}
>
<div
className="database-key-confirm"
role="dialog"
aria-modal="true"
aria-labelledby="database-key-return-confirm-title"
onMouseDown={(event) => event.stopPropagation()}
>
<h2 id="database-key-return-confirm-title"></h2>
<p>
WechatExplorer
</p>
<div>
<button type="button" onClick={() => setConfirmingReturn(false)}>
</button>
<button
type="button"
onClick={() => {
setConfirmingReturn(false)
onReturnToLogin()
}}
>
</button>
</div>
</div>
</div>
)}
</>
)
}
@@ -59,6 +59,7 @@ export interface DatabaseKeyController {
saveKey: () => Promise<void>
autoDetectKey: () => Promise<void>
clearSavedKey: () => Promise<void>
returnToLogin: () => Promise<void>
copyDiagnostics: () => Promise<void>
refreshEnvironment: () => Promise<void>
}
@@ -14,6 +14,7 @@ export function useDatabaseKeyController({
onSelfInfoChange,
onContactsChange,
onFilteredContactsChange,
onReturnToLogin,
onNotice
}: {
dbKey: string
@@ -24,6 +25,7 @@ export function useDatabaseKeyController({
onSelfInfoChange: (info: SettingsSelfInfo | null) => void
onContactsChange: (contacts: Contact[]) => void
onFilteredContactsChange: (contacts: Contact[]) => void
onReturnToLogin: () => void
onNotice: (message: string) => void
}): DatabaseKeyController {
const [state, dispatch] = useReducer(databaseKeyReducer, initialDatabaseKeyState)
@@ -188,6 +190,26 @@ export function useDatabaseKeyController({
refreshEnvironment
])
const returnToLogin = useCallback(async (): Promise<void> => {
const result = await window.api.disconnectDb()
if (!result.success) {
onNotice(result.error || '断开数据库连接失败')
return
}
onDatabaseConnectionChange(false)
onSelfInfoChange(null)
onContactsChange([])
onFilteredContactsChange([])
onReturnToLogin()
}, [
onContactsChange,
onDatabaseConnectionChange,
onFilteredContactsChange,
onNotice,
onReturnToLogin,
onSelfInfoChange
])
const copyDiagnostics = useCallback(async (): Promise<void> => {
const result = await window.api.copyText(
buildDatabaseKeyDiagnostics(state, dbKey, selfInfo, dbReady)
@@ -214,6 +236,7 @@ export function useDatabaseKeyController({
saveKey,
autoDetectKey,
clearSavedKey,
returnToLogin,
copyDiagnostics,
refreshEnvironment
}
@@ -24,6 +24,7 @@ export function DatabaseKeyPage({
onSelfInfoChange,
onContactsChange,
onFilteredContactsChange,
onReturnToLogin,
onNotice
}: {
dbKey: string
@@ -34,6 +35,7 @@ export function DatabaseKeyPage({
onSelfInfoChange: (info: SettingsSelfInfo | null) => void
onContactsChange: (contacts: Contact[]) => void
onFilteredContactsChange: (contacts: Contact[]) => void
onReturnToLogin: () => void
onNotice: (message: string) => void
}): React.ReactElement {
const controller = useDatabaseKeyController({
@@ -45,6 +47,7 @@ export function DatabaseKeyPage({
onSelfInfoChange,
onContactsChange,
onFilteredContactsChange,
onReturnToLogin,
onNotice
})
@@ -132,6 +135,7 @@ export function DatabaseKeyPage({
disabled={controller.isBusy || !controller.state.saved}
onClear={() => void controller.clearSavedKey()}
onReplace={scrollToEditor}
onReturnToLogin={() => void controller.returnToLogin()}
/>
</div>
</div>