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
这个提交包含在:
父节点
611f2ba95d
当前提交
b6581adc51
@ -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
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户