fix: 代码质量清理 — XWebView Pressable/Clipboard、configCrypto 消除 any、UpdateSDK AsyncStorage 优化

- XWebViewView/XWebViewScreen: TouchableOpacity → Pressable
- XWebViewView/XWebViewScreen: Clipboard 改用 @react-native-clipboard/clipboard
- configCrypto.ts: 6 处 any 替换为 SubtleCryptoLike 接口
- UpdateSDK: CachedRnBundle 移除 source 字段,AsyncStorage 仅存版本元数据
- xwebview 添加 @react-native-clipboard/clipboard peerDependency
这个提交包含在:
XuqmGroup 2026-06-16 14:48:47 +08:00
父节点 611f2ba95d
当前提交 b6581adc51
共有 6 个文件被更改,包括 34 次插入34 次删除

查看文件

@ -28,6 +28,7 @@
"@react-native-async-storage/async-storage": "^2.1.2", "@react-native-async-storage/async-storage": "^2.1.2",
"axios": "^1.7.0", "axios": "^1.7.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-native": "^0.85.0",
"zod": "^3.23.0" "zod": "^3.23.0"
} }
} }

查看文件

@ -28,10 +28,15 @@ export interface DecryptedConfig {
expiresAt?: string expiresAt?: string
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any interface SubtleCryptoLike {
function getSubtle(): any { importKey(format: string, keyData: BufferSource, algorithm: string | Record<string, unknown>, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>
deriveKey(algorithm: string | Record<string, unknown>, baseKey: CryptoKey, derivedKeyAlgorithm: string | Record<string, unknown>, extractable: boolean, keyUsages: string[]): Promise<CryptoKey>
decrypt(algorithm: string | Record<string, unknown>, key: CryptoKey, data: BufferSource): Promise<ArrayBuffer>
}
function getSubtle(): SubtleCryptoLike {
// eslint-disable-next-line @typescript-eslint/no-require-imports // eslint-disable-next-line @typescript-eslint/no-require-imports
const qc = require('react-native-quick-crypto') as any const qc = require('react-native-quick-crypto') as { subtle?: SubtleCryptoLike; default?: { subtle?: SubtleCryptoLike } }
const subtle = qc.subtle ?? qc.default?.subtle const subtle = qc.subtle ?? qc.default?.subtle
if (!subtle) throw new Error('[XuqmSDK] react-native-quick-crypto not available') if (!subtle) throw new Error('[XuqmSDK] react-native-quick-crypto not available')
return subtle return subtle
@ -43,8 +48,7 @@ function base64UrlDecode(s: string): Uint8Array {
return Uint8Array.from({ length: binary.length }, (_, i) => binary.charCodeAt(i)) return Uint8Array.from({ length: binary.length }, (_, i) => binary.charCodeAt(i))
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any async function deriveKey(salt: Uint8Array, passphrase: string): Promise<CryptoKey> {
async function deriveKey(salt: Uint8Array, passphrase: string): Promise<any> {
const subtle = getSubtle() const subtle = getSubtle()
const passphraseKey = await subtle.importKey( const passphraseKey = await subtle.importKey(
'raw', 'raw',
@ -54,8 +58,7 @@ async function deriveKey(salt: Uint8Array, passphrase: string): Promise<any> {
['deriveKey'], ['deriveKey'],
) )
return subtle.deriveKey( return subtle.deriveKey(
// eslint-disable-next-line @typescript-eslint/no-explicit-any { name: 'PBKDF2', salt: salt as unknown as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
{ name: 'PBKDF2', salt: salt as any, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' },
passphraseKey, passphraseKey,
{ name: 'AES-GCM', length: 256 }, { name: 'AES-GCM', length: 256 },
false, false,
@ -76,8 +79,7 @@ export async function decryptConfigFile(content: string): Promise<DecryptedConfi
const passphrase = getPassphrase(magic) const passphrase = getPassphrase(magic)
const key = await deriveKey(salt, passphrase) const key = await deriveKey(salt, passphrase)
const subtle = getSubtle() const subtle = getSubtle()
// eslint-disable-next-line @typescript-eslint/no-explicit-any const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv: iv as unknown as BufferSource }, key, ciphertext as unknown as BufferSource)
const plainBuffer = await subtle.decrypt({ name: 'AES-GCM', iv } as any, key, ciphertext as any)
const json = new TextDecoder().decode(plainBuffer) const json = new TextDecoder().decode(plainBuffer)
return JSON.parse(json) as DecryptedConfig return JSON.parse(json) as DecryptedConfig
} }

查看文件

@ -55,7 +55,6 @@ interface CachedRnBundle {
version: string version: string
md5: string md5: string
downloadedAt: string downloadedAt: string
source: string
} }
// ─── Internal state ──────────────────────────────────────────────────────────── // ─── Internal state ────────────────────────────────────────────────────────────
@ -435,12 +434,11 @@ export const UpdateSDK = {
} }
} }
// 写入 AsyncStorage 缓存(版本记录 // 写入 AsyncStorage 缓存(仅版本元数据,不含 bundle 源码
const cachePayload: CachedRnBundle = { const cachePayload: CachedRnBundle = {
moduleId, moduleId,
version: info.latestVersion, version: info.latestVersion,
md5: info.md5, md5: info.md5,
source,
downloadedAt: new Date().toISOString(), downloadedAt: new Date().toISOString(),
} }
await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(cachePayload)) await AsyncStorage.setItem(bundleCacheKey(moduleId), JSON.stringify(cachePayload))

查看文件

@ -32,6 +32,8 @@
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
"@react-navigation/native": "^7.0.0", "@react-navigation/native": "^7.0.0",
"@react-native-async-storage/async-storage": "^1.21.0", "@react-native-async-storage/async-storage": "^1.21.0",
"@react-native-clipboard/clipboard": "^1.16.3",
"react-native-safe-area-context": "^5.4.0",
"typescript": "^5.9.3" "typescript": "^5.9.3"
} }
} }

查看文件

@ -9,16 +9,16 @@ import {
ActionSheetIOS, ActionSheetIOS,
Alert, Alert,
BackHandler, BackHandler,
Clipboard,
Linking, Linking,
Platform, Platform,
Pressable,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Text, Text,
ToastAndroid, ToastAndroid,
TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import WebView from 'react-native-webview' import WebView from 'react-native-webview'
import type { import type {
@ -82,13 +82,13 @@ function HeaderBackClose({
return ( return (
<View style={styles.headerLeft}> <View style={styles.headerLeft}>
{canGoBack ? ( {canGoBack ? (
<TouchableOpacity style={styles.headerBtn} onPress={onBack} hitSlop={HIT_SLOP}> <Pressable style={styles.headerBtn} onPress={onBack} hitSlop={HIT_SLOP}>
<IconBack size={22} color="#222222" /> <IconBack size={22} color="#222222" />
</TouchableOpacity> </Pressable>
) : null} ) : null}
<TouchableOpacity style={styles.headerBtn} onPress={onClose} hitSlop={HIT_SLOP}> <Pressable style={styles.headerBtn} onPress={onClose} hitSlop={HIT_SLOP}>
<IconClose size={24} color="#222222" /> <IconClose size={24} color="#222222" />
</TouchableOpacity> </Pressable>
</View> </View>
) )
} }
@ -144,20 +144,20 @@ function MenuButton({
if (config.clickMenu) { if (config.clickMenu) {
const { clickMenu } = config const { clickMenu } = config
return ( return (
<TouchableOpacity <Pressable
style={styles.headerBtn} style={styles.headerBtn}
onPress={clickMenu.onClick} onPress={clickMenu.onClick}
hitSlop={HIT_SLOP} hitSlop={HIT_SLOP}
> >
{clickMenu.view ?? <IconMenu size={22} />} {clickMenu.view ?? <IconMenu size={22} />}
</TouchableOpacity> </Pressable>
) )
} }
return ( return (
<TouchableOpacity style={styles.headerBtn} onPress={handleDefaultMenu} hitSlop={HIT_SLOP}> <Pressable style={styles.headerBtn} onPress={handleDefaultMenu} hitSlop={HIT_SLOP}>
<IconMenu size={22} /> <IconMenu size={22} />
</TouchableOpacity> </Pressable>
) )
} }
@ -526,7 +526,7 @@ export function XWebViewScreen() {
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<Text style={styles.errorTitle}></Text> <Text style={styles.errorTitle}></Text>
<Text style={styles.errorMessage}>{loadError}</Text> <Text style={styles.errorMessage}>{loadError}</Text>
<TouchableOpacity <Pressable
style={styles.retryBtn} style={styles.retryBtn}
onPress={() => { onPress={() => {
setLoadError(null) setLoadError(null)
@ -534,7 +534,7 @@ export function XWebViewScreen() {
}} }}
> >
<Text style={styles.retryText}></Text> <Text style={styles.retryText}></Text>
</TouchableOpacity> </Pressable>
</View> </View>
) : ( ) : (
<WebViewAny <WebViewAny

查看文件

@ -1,14 +1,14 @@
import React, { useCallback, useEffect, useRef, useState } from 'react' import React, { useCallback, useEffect, useRef, useState } from 'react'
import { import {
Alert, Alert,
Clipboard,
Platform, Platform,
Pressable,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Text, Text,
TouchableOpacity,
View, View,
} from 'react-native' } from 'react-native'
import Clipboard from '@react-native-clipboard/clipboard'
import { SafeAreaView } from 'react-native-safe-area-context' import { SafeAreaView } from 'react-native-safe-area-context'
import WebView from 'react-native-webview' import WebView from 'react-native-webview'
import type { import type {
@ -336,15 +336,12 @@ export function XWebViewView() {
<View style={styles.errorContainer}> <View style={styles.errorContainer}>
<Text style={styles.errorTitle}></Text> <Text style={styles.errorTitle}></Text>
<Text style={styles.errorMessage}>{loadError}</Text> <Text style={styles.errorMessage}>{loadError}</Text>
<TouchableOpacity <Pressable
style={styles.retryBtn} onPress={() => webViewRef.current?.reload()}
onPress={() => { style={({ pressed }) => [styles.retryBtn, pressed && { opacity: 0.7 }]}
setLoadError(null)
webViewRef.current?.reload()
}}
> >
<Text style={styles.retryText}></Text> <Text style={styles.retryText}></Text>
</TouchableOpacity> </Pressable>
</View> </View>
) : ( ) : (
<WebViewAny <WebViewAny