Skip to content

Commit 1f74748

Browse files
authored
fix: unify miniapp auth flow context and chain resolution (#411)
- share miniapp context builder across handlers to keep icon/app metadata consistent - pass wallet/chain/miniapp context into wallet lock sheet for consistent header UX - normalize chain id across miniapp + provider pipeline to avoid BFMetaV2 mismatch - add miniapp wallet normalization tests and transfer handler coverage - defer monochrome mask work to idle and avoid runtime new Function for built-in hooks
1 parent 11c9996 commit 1f74748

19 files changed

Lines changed: 775 additions & 217 deletions

src/components/security/pattern-lock.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export interface PatternLockProps {
1717
error?: boolean;
1818
/** 成功状态 */
1919
success?: boolean;
20+
/** 错误提示文案(可选,覆盖默认错误文案) */
21+
errorText?: string;
2022
/** 额外的 className */
2123
className?: string;
2224
/** 网格大小 (默认 3x3) */
@@ -48,6 +50,7 @@ export function PatternLock({
4850
disabled = false,
4951
error = false,
5052
success = false,
53+
errorText,
5154
className,
5255
size = 3,
5356
'data-testid': testId,
@@ -496,7 +499,7 @@ export function PatternLock({
496499
<div className="text-center h-5">
497500
{error || isErrorAnimating ? (
498501
<p className="text-destructive text-sm">
499-
{t('patternLock.error')}
502+
{errorText ?? t('patternLock.error')}
500503
</p>
501504
) : selectedNodes.length === 0 ? (
502505
<p className="text-muted-foreground text-sm">

src/hooks/useMonochromeMask.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,43 @@ import { createMonochromeMask, type MonochromeMaskOptions, type PipelineHook } f
33

44
export type { MonochromeMaskOptions, PipelineHook };
55

6+
interface IdleDeadlineLike {
7+
didTimeout: boolean;
8+
timeRemaining: () => number;
9+
}
10+
11+
type IdleCallbackLike = (deadline: IdleDeadlineLike) => void;
12+
13+
type IdleRequestFn = (callback: IdleCallbackLike, options?: { timeout: number }) => number;
14+
type IdleCancelFn = (handle: number) => void;
15+
16+
type IdleGlobal = typeof globalThis & {
17+
requestIdleCallback?: IdleRequestFn;
18+
cancelIdleCallback?: IdleCancelFn;
19+
};
20+
21+
const idleGlobal: IdleGlobal = globalThis;
22+
23+
const requestIdleWork: IdleRequestFn =
24+
typeof idleGlobal.requestIdleCallback === 'function'
25+
? idleGlobal.requestIdleCallback.bind(idleGlobal)
26+
: ((callback: IdleCallbackLike) => {
27+
const timeoutHandle = idleGlobal.setTimeout(() => {
28+
callback({
29+
didTimeout: false,
30+
timeRemaining: () => 0,
31+
});
32+
}, 16);
33+
return timeoutHandle as unknown as number;
34+
});
35+
36+
const cancelIdleWork: IdleCancelFn =
37+
typeof idleGlobal.cancelIdleCallback === 'function'
38+
? idleGlobal.cancelIdleCallback.bind(idleGlobal)
39+
: ((handle: number) => {
40+
idleGlobal.clearTimeout(handle);
41+
});
42+
643
/**
744
* 将图标转换为单色遮罩(用于防伪水印)
845
*
@@ -33,6 +70,7 @@ export function useMonochromeMask(iconUrl: string | undefined, options: Monochro
3370
}
3471

3572
let cancelled = false;
73+
let idleHandle = 0;
3674

3775
// 解析 pipeline(从稳定化的 key 还原)
3876
const pipelineData: PipelineHook[] | undefined = pipelineKey ? JSON.parse(pipelineKey) : undefined;
@@ -41,21 +79,26 @@ export function useMonochromeMask(iconUrl: string | undefined, options: Monochro
4179
if (pipelineData) {
4280
opts.pipeline = pipelineData;
4381
}
44-
if (clip) {
82+
if (clip !== undefined) {
4583
opts.clip = clip;
4684
}
4785
if (targetBrightness !== undefined) {
4886
opts.targetBrightness = targetBrightness;
4987
}
5088

51-
createMonochromeMask(iconUrl, opts).then((url) => {
52-
if (!cancelled) {
53-
setMaskUrl(url);
54-
}
55-
});
89+
idleHandle = requestIdleWork(() => {
90+
createMonochromeMask(iconUrl, opts).then((url) => {
91+
if (!cancelled) {
92+
setMaskUrl(url);
93+
}
94+
});
95+
}, { timeout: 300 });
5696

5797
return () => {
5898
cancelled = true;
99+
if (idleHandle !== 0) {
100+
cancelIdleWork(idleHandle);
101+
}
59102
};
60103
}, [iconUrl, size, invert, contrast, pipelineKey, clip, targetBrightness]);
61104

src/lib/canvas/monochrome-mask.ts

Lines changed: 130 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -107,30 +107,148 @@ function loadImage(url: string): Promise<HTMLImageElement> {
107107
}
108108

109109
type Ctx2D = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D
110+
type HookExecutor = (ctx: Ctx2D, imageData: ImageData, size: number, args: unknown) => void
110111

111-
/**
112-
* 编译 hook 函数字符串为可执行函数
113-
* 函数签名: (ctx, imageData, size, args) => void
114-
*/
115-
function compileHook(code: string): (
112+
const normalizeHookCode = (code: string): string => code.replace(/\s+/g, ' ').trim()
113+
114+
function getNumberArg(args: unknown, key: string, fallback: number): number {
115+
if (typeof args !== 'object' || args === null) {
116+
return fallback
117+
}
118+
const value = Reflect.get(args, key)
119+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
120+
}
121+
122+
function runRainbowGradientHook(
116123
ctx: Ctx2D,
117124
imageData: ImageData,
118125
size: number,
119126
args: unknown
120-
) => void {
127+
): void {
128+
const data = imageData.data
129+
const centerX = size / 2
130+
const centerY = size / 2
131+
const opacity = getNumberArg(args, 'opacity', 1)
132+
133+
for (let index = 0; index < data.length; index += 4) {
134+
const alpha = data[index + 3]!
135+
if (alpha < 10) continue
136+
137+
const pixelX = (index / 4) % size
138+
const pixelY = Math.floor(index / 4 / size)
139+
140+
const angle = (Math.atan2(pixelX - centerX, centerY - pixelY) * 180) / Math.PI + 180
141+
const hue = (angle % 360) / 60
142+
const hueBand = Math.floor(hue) % 6
143+
const fraction = hue - Math.floor(hue)
144+
145+
let red = 255
146+
let green = 0
147+
let blue = 0
148+
149+
switch (hueBand) {
150+
case 0:
151+
red = 255
152+
green = Math.round(fraction * 255)
153+
blue = 0
154+
break
155+
case 1:
156+
red = Math.round((1 - fraction) * 255)
157+
green = 255
158+
blue = 0
159+
break
160+
case 2:
161+
red = 0
162+
green = 255
163+
blue = Math.round(fraction * 255)
164+
break
165+
case 3:
166+
red = 0
167+
green = Math.round((1 - fraction) * 255)
168+
blue = 255
169+
break
170+
case 4:
171+
red = Math.round(fraction * 255)
172+
green = 0
173+
blue = 255
174+
break
175+
default:
176+
red = 255
177+
green = 0
178+
blue = Math.round((1 - fraction) * 255)
179+
break
180+
}
181+
182+
data[index] = red
183+
data[index + 1] = green
184+
data[index + 2] = blue
185+
data[index + 3] = Math.round(alpha * opacity)
186+
}
187+
188+
ctx.putImageData(imageData, 0, 0)
189+
}
190+
191+
function runSolidColorHook(
192+
ctx: Ctx2D,
193+
imageData: ImageData,
194+
_size: number,
195+
args: unknown
196+
): void {
197+
const data = imageData.data
198+
const red = getNumberArg(args, 'r', 255)
199+
const green = getNumberArg(args, 'g', 255)
200+
const blue = getNumberArg(args, 'b', 255)
201+
const opacity = getNumberArg(args, 'opacity', 1)
202+
203+
for (let index = 0; index < data.length; index += 4) {
204+
const alpha = data[index + 3]!
205+
if (alpha < 10) continue
206+
207+
data[index] = red
208+
data[index + 1] = green
209+
data[index + 2] = blue
210+
data[index + 3] = Math.round(alpha * opacity)
211+
}
212+
213+
ctx.putImageData(imageData, 0, 0)
214+
}
215+
216+
const builtinHookExecutors = new Map<string, HookExecutor>()
217+
let builtinHookExecutorsReady = false
218+
219+
function ensureBuiltinHookExecutors(): void {
220+
if (builtinHookExecutorsReady) {
221+
return
222+
}
223+
224+
builtinHookExecutors.set(normalizeHookCode(RAINBOW_GRADIENT_HOOK.code), runRainbowGradientHook)
225+
builtinHookExecutors.set(normalizeHookCode(SOLID_COLOR_HOOK_CODE), runSolidColorHook)
226+
builtinHookExecutorsReady = true
227+
}
228+
229+
function resolveBuiltinHookExecutor(code: string): HookExecutor | undefined {
230+
ensureBuiltinHookExecutors()
231+
return builtinHookExecutors.get(normalizeHookCode(code))
232+
}
233+
234+
/**
235+
* 编译 hook 函数字符串为可执行函数
236+
* 函数签名: (ctx, imageData, size, args) => void
237+
*/
238+
function compileHook(code: string): HookExecutor {
121239
// eslint-disable-next-line @typescript-eslint/no-implied-eval
122-
return new Function('ctx', 'imageData', 'size', 'args', code) as (
123-
ctx: Ctx2D,
124-
imageData: ImageData,
125-
size: number,
126-
args: unknown
127-
) => void
240+
return new Function('ctx', 'imageData', 'size', 'args', code) as HookExecutor
128241
}
129242

130243
// Hook 函数缓存(避免重复编译)
131244
const hookCache = new Map<string, ReturnType<typeof compileHook>>()
132245

133246
function getCompiledHook(code: string): ReturnType<typeof compileHook> {
247+
const builtinHook = resolveBuiltinHookExecutor(code)
248+
if (builtinHook) {
249+
return builtinHook
250+
}
251+
134252
let fn = hookCache.get(code)
135253
if (!fn) {
136254
fn = compileHook(code)

src/services/chain-adapter/providers/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ const PROVIDER_FACTORIES: ApiProviderFactory[] = [
5959
createBtcwalletProviderEffect,
6060
];
6161

62+
function resolveChainId(chainId: string): string {
63+
const normalized = chainId.trim();
64+
if (normalized.length === 0) return normalized;
65+
const config = chainConfigService.getConfig(normalized);
66+
if (config) return config.id;
67+
return normalized.toLowerCase();
68+
}
69+
6270
/**
6371
* 从配置创建 ApiProvider
6472
*/
@@ -74,17 +82,18 @@ function createApiProvider(entry: ParsedApiEntry, chainId: string): ApiProvider
7482
* 为指定链创建 ChainProvider
7583
*/
7684
export function createChainProvider(chainId: string): ChainProvider {
77-
const entries = chainConfigService.getApi(chainId);
85+
const resolvedChainId = resolveChainId(chainId);
86+
const entries = chainConfigService.getApi(resolvedChainId);
7887
const providers: ApiProvider[] = [];
7988

8089
for (const entry of entries) {
81-
const provider = createApiProvider(entry, chainId);
90+
const provider = createApiProvider(entry, resolvedChainId);
8291
if (provider) {
8392
providers.push(provider);
8493
}
8594
}
8695

87-
return new ChainProvider(chainId, providers);
96+
return new ChainProvider(resolvedChainId, providers);
8897
}
8998

9099
/** ChainProvider 缓存 */
@@ -94,10 +103,12 @@ const providerCache = new Map<string, ChainProvider>();
94103
* 获取或创建 ChainProvider(带缓存)
95104
*/
96105
export function getChainProvider(chainId: string): ChainProvider {
97-
let provider = providerCache.get(chainId);
106+
const resolvedChainId = resolveChainId(chainId);
107+
108+
let provider = providerCache.get(resolvedChainId);
98109
if (!provider) {
99-
provider = createChainProvider(chainId);
100-
providerCache.set(chainId, provider);
110+
provider = createChainProvider(resolvedChainId);
111+
providerCache.set(resolvedChainId, provider);
101112
}
102113
return provider;
103114
}

src/services/chain-config/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -397,5 +397,8 @@ export function getEnabledChains(snapshot: ChainConfigSnapshot): ChainConfig[] {
397397
}
398398

399399
export function getChainById(snapshot: ChainConfigSnapshot, id: string): ChainConfig | null {
400-
return snapshot.configs.find((c) => c.id === id) ?? null
400+
const normalizedId = id.trim().toLowerCase()
401+
return snapshot.configs.find((c) => c.id === id)
402+
?? snapshot.configs.find((c) => c.id.toLowerCase() === normalizedId)
403+
?? null
401404
}

0 commit comments

Comments
 (0)