mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-04 19:41:02 +08:00
feat(ui): Add an automated Web Crypto environment check (#908)
* feat: add web crypto environment check * fix: auto check env * refactor: WebCryptoEnvironmentCheck swtich to map
This commit is contained in:
@@ -18,6 +18,10 @@ import {
|
||||
TwoStageKeyModal,
|
||||
type TwoStageKeyModalResult,
|
||||
} from './TwoStageKeyModal'
|
||||
import {
|
||||
WebCryptoEnvironmentCheck,
|
||||
type WebCryptoCheckStatus,
|
||||
} from './WebCryptoEnvironmentCheck'
|
||||
import {
|
||||
Bot,
|
||||
Brain,
|
||||
@@ -1772,6 +1776,8 @@ function ExchangeConfigModal({
|
||||
} | null>(null)
|
||||
const [loadingIP, setLoadingIP] = useState(false)
|
||||
const [copiedIP, setCopiedIP] = useState(false)
|
||||
const [webCryptoStatus, setWebCryptoStatus] =
|
||||
useState<WebCryptoCheckStatus>('idle')
|
||||
|
||||
// 币安配置指南展开状态
|
||||
const [showBinanceGuide, setShowBinanceGuide] = useState(false)
|
||||
@@ -2008,34 +2014,51 @@ function ExchangeConfigModal({
|
||||
style={{ maxHeight: 'calc(100vh - 16rem)' }}
|
||||
>
|
||||
{!editingExchangeId && (
|
||||
<div>
|
||||
<label
|
||||
className="block text-sm font-semibold mb-2"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{t('selectExchange', language)}
|
||||
</label>
|
||||
<select
|
||||
value={selectedExchangeId}
|
||||
onChange={(e) => setSelectedExchangeId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
required
|
||||
>
|
||||
<option value="">
|
||||
{t('pleaseSelectExchange', language)}
|
||||
</option>
|
||||
{availableExchanges.map((exchange) => (
|
||||
<option key={exchange.id} value={exchange.id}>
|
||||
{getShortName(exchange.name)} (
|
||||
{exchange.type.toUpperCase()})
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className="text-xs font-semibold uppercase tracking-wide"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{t('environmentSteps.checkTitle', language)}
|
||||
</div>
|
||||
<WebCryptoEnvironmentCheck
|
||||
language={language}
|
||||
variant="card"
|
||||
onStatusChange={setWebCryptoStatus}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div
|
||||
className="text-xs font-semibold uppercase tracking-wide"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{t('environmentSteps.selectTitle', language)}
|
||||
</div>
|
||||
<select
|
||||
value={selectedExchangeId}
|
||||
onChange={(e) => setSelectedExchangeId(e.target.value)}
|
||||
className="w-full px-3 py-2 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
aria-label={t('selectExchange', language)}
|
||||
disabled={webCryptoStatus !== 'secure'}
|
||||
required
|
||||
>
|
||||
<option value="">
|
||||
{t('pleaseSelectExchange', language)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{availableExchanges.map((exchange) => (
|
||||
<option key={exchange.id} value={exchange.id}>
|
||||
{getShortName(exchange.name)} (
|
||||
{exchange.type.toUpperCase()})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
import { toast } from 'sonner'
|
||||
import { WebCryptoEnvironmentCheck } from './WebCryptoEnvironmentCheck'
|
||||
|
||||
const DEFAULT_LENGTH = 64
|
||||
|
||||
@@ -197,6 +198,10 @@ export function TwoStageKeyModal({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<WebCryptoEnvironmentCheck language={language} variant="compact" />
|
||||
</div>
|
||||
|
||||
{/* Stage 1 */}
|
||||
{stage === 1 && (
|
||||
<div className="space-y-4">
|
||||
|
||||
138
web/src/components/WebCryptoEnvironmentCheck.tsx
Normal file
138
web/src/components/WebCryptoEnvironmentCheck.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { useCallback, useEffect, useState, type ReactNode } from 'react'
|
||||
import { Loader2, ShieldAlert, ShieldCheck } from 'lucide-react'
|
||||
import { diagnoseWebCryptoEnvironment } from '../lib/crypto'
|
||||
import { t, type Language } from '../i18n/translations'
|
||||
|
||||
export type WebCryptoCheckStatus =
|
||||
| 'idle'
|
||||
| 'checking'
|
||||
| 'secure'
|
||||
| 'insecure'
|
||||
| 'unsupported'
|
||||
|
||||
interface WebCryptoEnvironmentCheckProps {
|
||||
language: Language
|
||||
variant?: 'card' | 'compact'
|
||||
onStatusChange?: (status: WebCryptoCheckStatus) => void
|
||||
}
|
||||
|
||||
export function WebCryptoEnvironmentCheck({
|
||||
language,
|
||||
variant = 'card',
|
||||
onStatusChange,
|
||||
}: WebCryptoEnvironmentCheckProps) {
|
||||
const [status, setStatus] = useState<WebCryptoCheckStatus>('idle')
|
||||
const [summary, setSummary] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
onStatusChange?.(status)
|
||||
}, [onStatusChange, status])
|
||||
|
||||
const runCheck = useCallback(() => {
|
||||
setStatus('checking')
|
||||
setSummary(null)
|
||||
|
||||
setTimeout(() => {
|
||||
const result = diagnoseWebCryptoEnvironment()
|
||||
setSummary(
|
||||
t('environmentCheck.summary', language, {
|
||||
origin: result.origin || 'N/A',
|
||||
protocol: result.protocol || 'unknown',
|
||||
})
|
||||
)
|
||||
|
||||
if (!result.isBrowser || !result.hasSubtleCrypto) {
|
||||
setStatus('unsupported')
|
||||
return
|
||||
}
|
||||
|
||||
if (!result.isSecureContext) {
|
||||
setStatus('insecure')
|
||||
return
|
||||
}
|
||||
|
||||
setStatus('secure')
|
||||
}, 0)
|
||||
}, [language, t])
|
||||
|
||||
useEffect(() => {
|
||||
runCheck()
|
||||
}, [runCheck])
|
||||
|
||||
const isCompact = variant === 'compact'
|
||||
const containerClass = isCompact
|
||||
? 'p-3 rounded border border-gray-700 bg-gray-900 space-y-3'
|
||||
: 'p-4 rounded border border-[#2B3139] bg-[#0B0E11] space-y-4'
|
||||
|
||||
const descriptionColor = isCompact ? '#CBD5F5' : '#A1AEC8'
|
||||
const showInfo = status !== 'idle'
|
||||
|
||||
const statusRendererMap: Record<WebCryptoCheckStatus, () => ReactNode> = {
|
||||
secure: () => (
|
||||
<div className="flex items-start gap-2 text-green-400 text-xs">
|
||||
<ShieldCheck className="w-4 h-4 flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-semibold">
|
||||
{t('environmentCheck.secureTitle', language)}
|
||||
</div>
|
||||
<div>{t('environmentCheck.secureDesc', language)}</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
insecure: () => (
|
||||
<div className="text-xs" style={{ color: '#F59E0B' }}>
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<ShieldAlert className="w-4 h-4 flex-shrink-0" />
|
||||
<div className="font-semibold">
|
||||
{t('environmentCheck.insecureTitle', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{t('environmentCheck.insecureDesc', language)}</div>
|
||||
<div className="mt-2 font-semibold">
|
||||
{t('environmentCheck.tipsTitle', language)}
|
||||
</div>
|
||||
<ul className="list-disc pl-5 space-y-1 mt-1">
|
||||
<li>{t('environmentCheck.tipHTTPS', language)}</li>
|
||||
<li>{t('environmentCheck.tipLocalhost', language)}</li>
|
||||
<li>{t('environmentCheck.tipIframe', language)}</li>
|
||||
</ul>
|
||||
</div>
|
||||
),
|
||||
unsupported: () => (
|
||||
<div className="text-xs" style={{ color: '#F87171' }}>
|
||||
<div className="flex items-start gap-2 mb-1">
|
||||
<ShieldAlert className="w-4 h-4 flex-shrink-0" />
|
||||
<div className="font-semibold">
|
||||
{t('environmentCheck.unsupportedTitle', language)}
|
||||
</div>
|
||||
</div>
|
||||
<div>{t('environmentCheck.unsupportedDesc', language)}</div>
|
||||
</div>
|
||||
),
|
||||
checking: () => (
|
||||
<div
|
||||
className="flex items-center gap-2 text-xs"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>{t('environmentCheck.checking', language)}</span>
|
||||
</div>
|
||||
),
|
||||
idle: () => null,
|
||||
}
|
||||
|
||||
const renderStatus = () => statusRendererMap[status]()
|
||||
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
{showInfo && (
|
||||
<div className="text-xs" style={{ color: descriptionColor }}>
|
||||
{summary ?? t('environmentCheck.description', language)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showInfo && <div className="min-h-[1.5rem]">{renderStatus()}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user