完善移动壳系统分享目标解析
移动 Expo 壳解析统一分享目标并调用 React Native 系统分享面板 补充直接分享、缓存作品目标和空分享目标的移动壳测试 更新宿主壳方案和项目共享决策记录
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import * as Linking from 'expo-linking';
|
||||
import { Share } from 'react-native';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
import {
|
||||
configureMobileHostBridgeNavigation,
|
||||
handleMobileHostBridgeMessage,
|
||||
resetMobileHostBridgeForTest,
|
||||
} from './mobileHostBridge';
|
||||
|
||||
vi.mock('expo-clipboard', () => ({
|
||||
@@ -85,7 +87,8 @@ function expectFailed(response: HostBridgeResponse) {
|
||||
|
||||
afterEach(() => {
|
||||
vi.mocked(Linking.openURL).mockReset();
|
||||
configureMobileHostBridgeNavigation(null);
|
||||
vi.mocked(Share.share).mockReset();
|
||||
resetMobileHostBridgeForTest();
|
||||
});
|
||||
|
||||
describe('handleMobileHostBridgeMessage', () => {
|
||||
@@ -180,6 +183,60 @@ describe('handleMobileHostBridgeMessage', () => {
|
||||
expect(Linking.openURL).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('share.open 使用直接分享 payload 调起系统分享', async () => {
|
||||
const response = await send(
|
||||
request('share.open', {
|
||||
title: '测试作品',
|
||||
message: '来玩这个作品',
|
||||
url: 'https://app.genarrative.world/works/detail?work=PZ-1',
|
||||
}),
|
||||
);
|
||||
|
||||
expectOk(response);
|
||||
expect(Share.share).toHaveBeenCalledWith({
|
||||
title: '测试作品',
|
||||
message:
|
||||
'来玩这个作品\nhttps://app.genarrative.world/works/detail?work=PZ-1',
|
||||
url: 'https://app.genarrative.world/works/detail?work=PZ-1',
|
||||
});
|
||||
});
|
||||
|
||||
test('share.open 使用缓存作品目标生成作品详情链接', async () => {
|
||||
expectOk(
|
||||
await send(
|
||||
request('share.setTarget', {
|
||||
target: {
|
||||
type: 'genarrative:share-target',
|
||||
payload: {
|
||||
title: '暖灯猫街',
|
||||
message: '来玩这个作品',
|
||||
work: 'PZ-00000001',
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const response = await send(request('share.open'));
|
||||
|
||||
expectOk(response);
|
||||
expect(Share.share).toHaveBeenCalledWith({
|
||||
title: '暖灯猫街',
|
||||
message:
|
||||
'来玩这个作品\nhttps://app.genarrative.world/works/detail?work=PZ-00000001',
|
||||
url: 'https://app.genarrative.world/works/detail?work=PZ-00000001',
|
||||
});
|
||||
});
|
||||
|
||||
test('share.open 没有可分享内容时拒绝请求', async () => {
|
||||
const response = await send(request('share.open', {}));
|
||||
|
||||
const failedResponse = expectFailed(response);
|
||||
|
||||
expect(failedResponse.error.code).toBe('invalid_request');
|
||||
expect(Share.share).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('移动壳未接入真实导出能力时明确返回 unsupported', async () => {
|
||||
const response = await send(
|
||||
request('file.exportText', {
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
} from '../../../packages/shared/src/contracts/hostBridge';
|
||||
import { resolveMobileShellWebViewUrl } from './mobileShellNavigation';
|
||||
|
||||
const WEB_APP_ORIGIN = 'https://app.genarrative.world';
|
||||
|
||||
export const MOBILE_HOST_CAPABILITIES: HostBridgeCapability[] = [
|
||||
'host.getRuntime',
|
||||
'host.events',
|
||||
@@ -143,13 +145,73 @@ async function runHaptics(payload: unknown) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function stringField(value: unknown, field: string) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const fieldValue = (value as Record<string, unknown>)[field];
|
||||
if (typeof fieldValue !== 'string') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const text = fieldValue.trim();
|
||||
return text || undefined;
|
||||
}
|
||||
|
||||
function shareTargetPayload(value: unknown) {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return value;
|
||||
}
|
||||
|
||||
const target = value as Record<string, unknown>;
|
||||
return target.target ?? value;
|
||||
}
|
||||
|
||||
function workDetailUrl(work: string) {
|
||||
return `${WEB_APP_ORIGIN}/works/detail?work=${encodeURIComponent(work)}`;
|
||||
}
|
||||
|
||||
function webAppPathUrl(path: string) {
|
||||
return new URL(path, WEB_APP_ORIGIN).toString();
|
||||
}
|
||||
|
||||
function normalizeSharePayload(value: unknown): ShareOpenPayload | null {
|
||||
const target = shareTargetPayload(value);
|
||||
const payload =
|
||||
target && typeof target === 'object'
|
||||
? (target as Record<string, unknown>).payload ?? target
|
||||
: target;
|
||||
|
||||
if (!payload || typeof payload !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = stringField(payload, 'title');
|
||||
const message = stringField(payload, 'message');
|
||||
const directUrl = stringField(payload, 'url') ?? stringField(payload, 'href');
|
||||
const work = stringField(payload, 'work');
|
||||
const path = stringField(payload, 'path') ?? stringField(payload, 'targetPath');
|
||||
const url = directUrl ?? (work ? workDetailUrl(work) : undefined) ?? (path ? webAppPathUrl(path) : undefined);
|
||||
|
||||
if (!title && !message && !url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
...(title ? { title } : {}),
|
||||
...(message ? { message } : {}),
|
||||
...(url ? { url } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function openShare(payload: unknown) {
|
||||
const sharePayload =
|
||||
payload && typeof payload === 'object'
|
||||
? (payload as ShareOpenPayload)
|
||||
: currentShareTarget && typeof currentShareTarget === 'object'
|
||||
? (currentShareTarget as ShareOpenPayload)
|
||||
: undefined;
|
||||
normalizeSharePayload(payload) ?? normalizeSharePayload(currentShareTarget);
|
||||
if (!sharePayload) {
|
||||
throw invalidRequest('share target is required');
|
||||
}
|
||||
|
||||
const url = sharePayload?.url;
|
||||
const message = [sharePayload?.message, url].filter(Boolean).join('\n');
|
||||
|
||||
@@ -255,3 +317,8 @@ export async function handleMobileHostBridgeMessage(
|
||||
sendResponse(failure(parsed, normalizeError(error)));
|
||||
}
|
||||
}
|
||||
|
||||
export function resetMobileHostBridgeForTest() {
|
||||
currentShareTarget = null;
|
||||
navigation = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user