fix(platform): improve web compatibility and optimize module loading
Some checks failed
Frontend CI / ota-ios (push) Successful in 2m0s
Frontend CI / ota-android (push) Successful in 2m0s
Frontend CI / build-android-apk (push) Successful in 20m41s
Frontend CI / build-and-push-web (push) Failing after 22m49s

Refactor core services and components to prevent runtime errors on web platforms and optimize build environment.

- Update `Dockerfile.web` to use Node 25 for the build stage.
- Implement lazy loading for `expo-media-library` and `expo-file-system` in `ImageGallery` to prevent crashes on web where these native modules are unavailable.
- Refactor `CallKeepServiceImpl` to use dynamic imports for `expo-callkit-telecom`, ensuring the service remains compatible with web environments.
This commit is contained in:
2026-06-04 11:32:05 +08:00
parent 2e2f6e3467
commit 765fd8cce9
3 changed files with 47 additions and 54 deletions

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine AS builder FROM node:25-alpine AS builder
WORKDIR /app WORKDIR /app

View File

@@ -30,8 +30,6 @@ import Animated, {
} from 'react-native-reanimated'; } from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { MaterialCommunityIcons } from '@expo/vector-icons'; import { MaterialCommunityIcons } from '@expo/vector-icons';
import * as MediaLibrary from 'expo-media-library';
import { File, Paths } from 'expo-file-system';
import { spacing, borderRadius, fontSizes } from '../../theme'; import { spacing, borderRadius, fontSizes } from '../../theme';
import { blurActiveElement } from '../../infrastructure/platform'; import { blurActiveElement } from '../../infrastructure/platform';
@@ -203,7 +201,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 保存图片到本地相册 // 保存图片到本地相册
const handleSaveImage = useCallback(async () => { const handleSaveImage = useCallback(async () => {
if (!currentImage || saving) return; if (!currentImage || saving || Platform.OS === 'web') return;
const MediaLibrary = require('expo-media-library') as typeof import('expo-media-library');
const { File, Paths } = require('expo-file-system') as typeof import('expo-file-system');
const { status } = await MediaLibrary.requestPermissionsAsync(); const { status } = await MediaLibrary.requestPermissionsAsync();
if (status !== 'granted') { if (status !== 'granted') {

View File

@@ -1,37 +1,27 @@
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { import type {
startOutgoingCall, CallParticipant,
reportIncomingCall, IncomingCallEvent,
answerCall, CallEndedReason,
endCall, CallSession,
reportCallEnded, CallAnsweredEvent,
reportOutgoingCallConnected, CallEndedEvent,
fulfillIncomingCallConnected, SetMutedActionEvent,
failIncomingCallConnected,
setMuted,
reportVideo,
setAudioSessionPortOverride,
setRTCAudioSessionConfiguration,
addCallSessionAddedListener,
addCallAnsweredListener,
addCallEndedListener,
addOutgoingCallStartedListener,
addSetMutedActionListener,
addVideoChangedListener,
addAudioSessionActivatedListener,
addAudioSessionDeactivatedListener,
addAudioRouteChangedListener,
type CallParticipant,
type IncomingCallEvent,
type CallEndedReason,
type CallSession,
type CallAnsweredEvent,
type CallEndedEvent,
type SetMutedActionEvent,
} from 'expo-callkit-telecom'; } from 'expo-callkit-telecom';
const isWeb = Platform.OS === 'web'; const isWeb = Platform.OS === 'web';
type NativeModule = typeof import('expo-callkit-telecom');
let _native: NativeModule | null = null;
function getNative(): NativeModule {
if (_native) return _native;
// eslint-disable-next-line @typescript-eslint/no-require-imports
_native = require('expo-callkit-telecom') as NativeModule;
return _native;
}
export interface CallKeepCallbacks { export interface CallKeepCallbacks {
onIncomingCallFromPush: (session: CallSession) => void; onIncomingCallFromPush: (session: CallSession) => void;
onSystemAnswered: () => void; onSystemAnswered: () => void;
@@ -74,16 +64,18 @@ class CallKeepServiceImpl {
} }
private _setupListeners(): void { private _setupListeners(): void {
const native = getNative();
// Outgoing call accepted by the OS — media should start connecting // Outgoing call accepted by the OS — media should start connecting
this._subscriptions.push( this._subscriptions.push(
addOutgoingCallStartedListener(() => { native.addOutgoingCallStartedListener(() => {
this._callbacks?.onOutgoingStarted(); this._callbacks?.onOutgoingStarted();
}), }),
); );
// Incoming call — a new session was created (from push or reportIncomingCall) // Incoming call — a new session was created (from push or reportIncomingCall)
this._subscriptions.push( this._subscriptions.push(
addCallSessionAddedListener((event) => { native.addCallSessionAddedListener((event) => {
if (event.session.origin === 'incoming') { if (event.session.origin === 'incoming') {
this._activeSystemCallId = event.session.id; this._activeSystemCallId = event.session.id;
if (!this._pendingRequestId) { if (!this._pendingRequestId) {
@@ -95,7 +87,7 @@ class CallKeepServiceImpl {
// User answered incoming call (from system UI or via answerCall) // User answered incoming call (from system UI or via answerCall)
this._subscriptions.push( this._subscriptions.push(
addCallAnsweredListener((event: CallAnsweredEvent) => { native.addCallAnsweredListener((event: CallAnsweredEvent) => {
this._activeSystemCallId = event.id; this._activeSystemCallId = event.id;
this._pendingRequestId = event.requestId; this._pendingRequestId = event.requestId;
this._callbacks?.onSystemAnswered(); this._callbacks?.onSystemAnswered();
@@ -104,7 +96,7 @@ class CallKeepServiceImpl {
// Call ended from system UI (user pressed end in CallKit UI) // Call ended from system UI (user pressed end in CallKit UI)
this._subscriptions.push( this._subscriptions.push(
addCallEndedListener((_event: CallEndedEvent) => { native.addCallEndedListener((_event: CallEndedEvent) => {
this._activeSystemCallId = null; this._activeSystemCallId = null;
this._pendingRequestId = null; this._pendingRequestId = null;
this._callbacks?.onSystemEnded(); this._callbacks?.onSystemEnded();
@@ -113,14 +105,14 @@ class CallKeepServiceImpl {
// System mute toggle (CallKit mute button) // System mute toggle (CallKit mute button)
this._subscriptions.push( this._subscriptions.push(
addSetMutedActionListener((event: SetMutedActionEvent) => { native.addSetMutedActionListener((event: SetMutedActionEvent) => {
this._callbacks?.onSystemMuted(event.isMuted); this._callbacks?.onSystemMuted(event.isMuted);
}), }),
); );
// Video changed from system // Video changed from system
this._subscriptions.push( this._subscriptions.push(
addVideoChangedListener((event) => { native.addVideoChangedListener((event) => {
if (this._callbacks && event.hasVideo !== undefined) { if (this._callbacks && event.hasVideo !== undefined) {
// could add onSystemVideoChanged callback if needed // could add onSystemVideoChanged callback if needed
} }
@@ -129,19 +121,19 @@ class CallKeepServiceImpl {
// Audio session lifecycle — keep for debugging // Audio session lifecycle — keep for debugging
this._subscriptions.push( this._subscriptions.push(
addAudioSessionActivatedListener(() => { native.addAudioSessionActivatedListener(() => {
console.log('[CallKeep] Audio session activated'); console.log('[CallKeep] Audio session activated');
}), }),
); );
this._subscriptions.push( this._subscriptions.push(
addAudioSessionDeactivatedListener(() => { native.addAudioSessionDeactivatedListener(() => {
console.log('[CallKeep] Audio session deactivated'); console.log('[CallKeep] Audio session deactivated');
}), }),
); );
this._subscriptions.push( this._subscriptions.push(
addAudioRouteChangedListener((event) => { native.addAudioRouteChangedListener((event) => {
console.log('[CallKeep] Audio route changed:', event.currentRoute); console.log('[CallKeep] Audio route changed:', event.currentRoute);
}), }),
); );
@@ -167,7 +159,7 @@ class CallKeepServiceImpl {
id: calleeId, id: calleeId,
displayName: calleeName, displayName: calleeName,
}; };
const id = await startOutgoingCall(recipient, { hasVideo }); const id = await getNative().startOutgoingCall(recipient, { hasVideo });
this._activeSystemCallId = id; this._activeSystemCallId = id;
console.log('[CallKeep] Outgoing call started:', id); console.log('[CallKeep] Outgoing call started:', id);
return id; return id;
@@ -179,7 +171,7 @@ class CallKeepServiceImpl {
reportOutgoingCallConnected(): void { reportOutgoingCallConnected(): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
reportOutgoingCallConnected(this._activeSystemCallId); getNative().reportOutgoingCallConnected(this._activeSystemCallId);
} }
// ---- Incoming Call ---- // ---- Incoming Call ----
@@ -187,7 +179,7 @@ class CallKeepServiceImpl {
reportIncomingCallEvent(event: IncomingCallEvent): void { reportIncomingCallEvent(event: IncomingCallEvent): void {
if (isWeb) return; if (isWeb) return;
try { try {
reportIncomingCall(event); getNative().reportIncomingCall(event);
} catch (err) { } catch (err) {
console.error('[CallKeep] Failed to report incoming call:', err); console.error('[CallKeep] Failed to report incoming call:', err);
} }
@@ -195,13 +187,13 @@ class CallKeepServiceImpl {
answerCallFromApp(): void { answerCallFromApp(): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
answerCall(this._activeSystemCallId); getNative().answerCall(this._activeSystemCallId);
} }
fulfillIncomingCallConnected(): void { fulfillIncomingCallConnected(): void {
if (isWeb || !this._pendingRequestId) return; if (isWeb || !this._pendingRequestId) return;
try { try {
fulfillIncomingCallConnected(this._pendingRequestId); getNative().fulfillIncomingCallConnected(this._pendingRequestId);
this._pendingRequestId = null; this._pendingRequestId = null;
} catch (err) { } catch (err) {
console.error('[CallKeep] Failed to fulfill incoming call:', err); console.error('[CallKeep] Failed to fulfill incoming call:', err);
@@ -211,7 +203,7 @@ class CallKeepServiceImpl {
failIncomingCallConnected(): void { failIncomingCallConnected(): void {
if (isWeb || !this._activeSystemCallId || !this._pendingRequestId) return; if (isWeb || !this._activeSystemCallId || !this._pendingRequestId) return;
try { try {
failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId); getNative().failIncomingCallConnected(this._activeSystemCallId, this._pendingRequestId);
this._pendingRequestId = null; this._pendingRequestId = null;
} catch (err) { } catch (err) {
console.error('[CallKeep] Failed to fail incoming call:', err); console.error('[CallKeep] Failed to fail incoming call:', err);
@@ -223,7 +215,7 @@ class CallKeepServiceImpl {
endActiveCall(): void { endActiveCall(): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
try { try {
endCall(this._activeSystemCallId); getNative().endCall(this._activeSystemCallId);
} catch (err) { } catch (err) {
console.error('[CallKeep] Failed to end call:', err); console.error('[CallKeep] Failed to end call:', err);
} }
@@ -234,7 +226,7 @@ class CallKeepServiceImpl {
reportCallEndedExternal(reason: CallEndedReason): void { reportCallEndedExternal(reason: CallEndedReason): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
try { try {
reportCallEnded(this._activeSystemCallId, reason); getNative().reportCallEnded(this._activeSystemCallId, reason);
} catch (err) { } catch (err) {
console.error('[CallKeep] Failed to report call ended:', err); console.error('[CallKeep] Failed to report call ended:', err);
} }
@@ -246,24 +238,24 @@ class CallKeepServiceImpl {
configureAudioForCall(hasVideo: boolean): void { configureAudioForCall(hasVideo: boolean): void {
if (isWeb) return; if (isWeb) return;
setRTCAudioSessionConfiguration(hasVideo); getNative().setRTCAudioSessionConfiguration(hasVideo);
} }
// ---- Controls ---- // ---- Controls ----
setMuted(muted: boolean): void { setMuted(muted: boolean): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
setMuted(this._activeSystemCallId, muted); getNative().setMuted(this._activeSystemCallId, muted);
} }
reportVideoEnabled(enabled: boolean): void { reportVideoEnabled(enabled: boolean): void {
if (isWeb || !this._activeSystemCallId) return; if (isWeb || !this._activeSystemCallId) return;
reportVideo(this._activeSystemCallId, enabled); getNative().reportVideo(this._activeSystemCallId, enabled);
} }
setAudioOutputToSpeaker(enabled: boolean): void { setAudioOutputToSpeaker(enabled: boolean): void {
if (isWeb) return; if (isWeb) return;
setAudioSessionPortOverride(enabled); getNative().setAudioSessionPortOverride(enabled);
} }
// ---- Getters ---- // ---- Getters ----