接入个人中心原生扫码优先路径

让个人中心扫码弹层优先调用 scanner.scanQrCode HostBridge 能力
保留宿主不支持或异常时的浏览器摄像头扫码回退
取消原生扫码时关闭弹层且不连带打开 H5 摄像头
增加扫码弹层 HostBridge 优先测试并纳入原生壳总门禁
This commit is contained in:
2026-06-19 16:39:48 +08:00
parent d7af659aa5
commit 94f12f0c95
3 changed files with 82 additions and 2 deletions
+2
View File
@@ -77,6 +77,7 @@ const h5HostBridgeRequiredCallChainFiles = [
'src/components/platform-entry/PlatformEntryFlowShellImpl.tsx',
'src/components/platform-entry/PlatformFeedbackView.tsx',
'src/components/platform-entry/PlatformProfilePrimitives.tsx',
'src/components/platform-entry/PlatformProfileQrScannerModal.tsx',
'src/components/platform-entry/PlatformProfileReferralModal.tsx',
'src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.tsx',
'src/components/platform-entry/usePlatformProfileCenterController.ts',
@@ -430,6 +431,7 @@ const h5HostBridgeTests = [
'src/components/bark-battle-creation/BarkBattleResultView.test.tsx',
'src/components/common/CreativeAudioInputPanel.test.tsx',
'src/components/common/PublishShareModal.test.tsx',
'src/components/platform-entry/PlatformProfileQrScannerModal.test.tsx',
'src/components/creation-agent/CreationAgentWorkspace.test.tsx',
'src/components/visual-novel-result/VisualNovelResultView.test.tsx',
'src/components/platform-entry/platformDraftGenerationShelfModel.test.ts',
@@ -3,6 +3,7 @@
import { act, render, screen } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import * as hostBridgeServices from '../../services/host-bridge/hostBridge';
import { PlatformProfileQrScannerModal } from './PlatformProfileQrScannerModal';
type MockTrack = {
@@ -49,6 +50,7 @@ describe('PlatformProfileQrScannerModal', () => {
});
test('detects qr result and stops camera tracks', async () => {
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false);
const stop = vi.fn();
const stream = buildStream([{ stop }]);
const getUserMedia = vi.fn().mockResolvedValue(stream);
@@ -84,6 +86,7 @@ describe('PlatformProfileQrScannerModal', () => {
});
test('releases camera resource when modal unmounts before recognition', async () => {
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false);
const stop = vi.fn();
const stream = buildStream([{ stop }]);
const getUserMedia = vi.fn().mockResolvedValue(stream);
@@ -112,6 +115,59 @@ describe('PlatformProfileQrScannerModal', () => {
expect(stop).toHaveBeenCalledTimes(1);
expect(screen.queryByRole('dialog', { name: '扫码' })).toBeNull();
});
test('uses native QR scanner before browser camera', async () => {
const getUserMedia = vi.fn();
const onResult = vi.fn();
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue({
value: 'https://app.genarrative.world/works/detail?work=PZ-1',
format: 'qr_code',
});
installMediaDevices(getUserMedia);
render(
<PlatformProfileQrScannerModal
error={null}
result={null}
onClose={vi.fn()}
onError={vi.fn()}
onResult={onResult}
/>,
);
await act(async () => {
await flushPromises();
});
expect(onResult).toHaveBeenCalledWith(
'https://app.genarrative.world/works/detail?work=PZ-1',
);
expect(getUserMedia).not.toHaveBeenCalled();
});
test('closes scanner without browser fallback when native scan is cancelled', async () => {
const getUserMedia = vi.fn();
const onClose = vi.fn();
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(null);
installMediaDevices(getUserMedia);
render(
<PlatformProfileQrScannerModal
error={null}
result={null}
onClose={onClose}
onError={vi.fn()}
onResult={vi.fn()}
/>,
);
await act(async () => {
await flushPromises();
});
expect(onClose).toHaveBeenCalledTimes(1);
expect(getUserMedia).not.toHaveBeenCalled();
});
});
function buildStream(tracks: MockTrack[]): MockStream {
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { scanHostQrCode } from '../../services/host-bridge/hostBridge';
import { PlatformProfileModalShell } from './PlatformProfileModalShell';
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
@@ -102,7 +103,7 @@ export function PlatformProfileQrScannerModal({
}
};
const startCamera = async () => {
const startBrowserCamera = async () => {
if (
typeof navigator === 'undefined' ||
!navigator.mediaDevices?.getUserMedia
@@ -136,7 +137,28 @@ export function PlatformProfileQrScannerModal({
}
};
void startCamera();
const startScanner = async () => {
try {
const nativeResult = await scanHostQrCode();
if (!isMounted) {
return;
}
if (nativeResult) {
onResult(nativeResult.value);
return;
}
if (nativeResult === null) {
onClose();
return;
}
} catch {
// 浏览器摄像头路径继续承接不支持或异常宿主。
}
await startBrowserCamera();
};
void startScanner();
return () => {
isMounted = false;