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

View File

@@ -30,8 +30,6 @@ import Animated, {
} from 'react-native-reanimated';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
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 { blurActiveElement } from '../../infrastructure/platform';
@@ -203,7 +201,10 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
// 保存图片到本地相册
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();
if (status !== 'granted') {

View File

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