expo-callkit-telecom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +33 -0
- package/android/src/main/java/expo/modules/callkittelecom/ExpoCallKitTelecomModule.kt +384 -0
- package/android/src/main/java/expo/modules/callkittelecom/IncomingCallActivity.kt +275 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEventEmitter.kt +151 -0
- package/android/src/main/java/expo/modules/callkittelecom/events/CallEvents.kt +59 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallAudioManager.kt +361 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallManager.kt +891 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CallNotificationManager.kt +445 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/CaptureSessionManager.kt +27 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/DialtonePlayer.kt +171 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/FulfillRequestManager.kt +150 -0
- package/android/src/main/java/expo/modules/callkittelecom/managers/VoIPPushManager.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/models/CallModels.kt +269 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/CallNotificationReceiver.kt +54 -0
- package/android/src/main/java/expo/modules/callkittelecom/services/ExpoCallKitTelecomMessagingService.kt +161 -0
- package/android/src/main/java/expo/modules/callkittelecom/store/CallStore.kt +181 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/CallKitTelecomLog.kt +52 -0
- package/android/src/main/java/expo/modules/callkittelecom/utils/PermissionUtils.kt +28 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_avatar.xml +5 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_bg_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_answer.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_decline.xml +9 -0
- package/android/src/main/res/drawable/expo_callkit_telecom_ic_videocam.xml +9 -0
- package/android/src/main/res/layout/activity_incoming_call.xml +169 -0
- package/app.json +8 -0
- package/app.plugin.js +1 -0
- package/build/Calls.d.ts +577 -0
- package/build/Calls.d.ts.map +1 -0
- package/build/Calls.js +715 -0
- package/build/Calls.js.map +1 -0
- package/build/Calls.types.d.ts +203 -0
- package/build/Calls.types.d.ts.map +1 -0
- package/build/Calls.types.js +2 -0
- package/build/Calls.types.js.map +1 -0
- package/build/ExpoCallKitTelecomModule.d.ts +3 -0
- package/build/ExpoCallKitTelecomModule.d.ts.map +1 -0
- package/build/ExpoCallKitTelecomModule.js +4 -0
- package/build/ExpoCallKitTelecomModule.js.map +1 -0
- package/build/hooks/index.d.ts +2 -0
- package/build/hooks/index.d.ts.map +1 -0
- package/build/hooks/index.js +2 -0
- package/build/hooks/index.js.map +1 -0
- package/build/hooks/useVoIPPushToken.d.ts +14 -0
- package/build/hooks/useVoIPPushToken.d.ts.map +1 -0
- package/build/hooks/useVoIPPushToken.js +26 -0
- package/build/hooks/useVoIPPushToken.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +10 -0
- package/ios/AppDelegateSubscriber.swift +93 -0
- package/ios/ExpoCallKitTelecom.podspec +31 -0
- package/ios/ExpoCallKitTelecomLogger.swift +55 -0
- package/ios/ExpoCallKitTelecomModule.swift +503 -0
- package/ios/Managers/AudioManager.swift +363 -0
- package/ios/Managers/CallEventEmitter.swift +199 -0
- package/ios/Managers/CallManager+CXProviderDelegate.swift +195 -0
- package/ios/Managers/CallManager.swift +714 -0
- package/ios/Managers/CaptureSessionManager.swift +54 -0
- package/ios/Managers/DialtonePlayer.swift +126 -0
- package/ios/Managers/FulfillRequestManager.swift +154 -0
- package/ios/Managers/VoIPPushManager+PKPushRegistryDelegate.swift +123 -0
- package/ios/Managers/VoIPPushManager.swift +58 -0
- package/ios/Models/CallEvents.swift +263 -0
- package/ios/Models/CallOptions.swift +15 -0
- package/ios/Models/CallParticipant.swift +37 -0
- package/ios/Models/CallSession.swift +80 -0
- package/ios/Models/IncomingCallEvent.swift +196 -0
- package/ios/Stores/CallStore.swift +149 -0
- package/package.json +56 -0
- package/plugin/build/constants.d.ts +3 -0
- package/plugin/build/constants.js +7 -0
- package/plugin/build/withExpoCallKitTelecom.d.ts +67 -0
- package/plugin/build/withExpoCallKitTelecom.js +16 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomAndroid.js +177 -0
- package/plugin/build/withExpoCallKitTelecomIos.d.ts +3 -0
- package/plugin/build/withExpoCallKitTelecomIos.js +195 -0
- package/plugin/src/constants.ts +4 -0
- package/plugin/src/withExpoCallKitTelecom.ts +83 -0
- package/plugin/src/withExpoCallKitTelecomAndroid.ts +293 -0
- package/plugin/src/withExpoCallKitTelecomIos.ts +276 -0
- package/src/Calls.ts +848 -0
- package/src/Calls.types.ts +275 -0
- package/src/ExpoCallKitTelecomModule.ts +4 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/useVoIPPushToken.ts +34 -0
- package/src/index.ts +3 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type ConfigPlugin, createRunOncePlugin } from "expo/config-plugins";
|
|
2
|
+
|
|
3
|
+
import pkg from "../../package.json";
|
|
4
|
+
import { withExpoCallKitTelecomAndroid } from "./withExpoCallKitTelecomAndroid";
|
|
5
|
+
import { withExpoCallKitTelecomIos } from "./withExpoCallKitTelecomIos";
|
|
6
|
+
|
|
7
|
+
export type ExpoCallKitTelecomPluginProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Custom message for microphone permission prompt. Set to false to skip.
|
|
10
|
+
* @platform ios
|
|
11
|
+
*/
|
|
12
|
+
microphonePermission?: string | false;
|
|
13
|
+
/**
|
|
14
|
+
* Custom message for camera permission prompt. Set to false to skip.
|
|
15
|
+
* @platform ios
|
|
16
|
+
*/
|
|
17
|
+
cameraPermission?: string | false;
|
|
18
|
+
/**
|
|
19
|
+
* Timeout in seconds for incoming calls before they are marked as unanswered.
|
|
20
|
+
* @default 45
|
|
21
|
+
* @platform ios
|
|
22
|
+
* @platform android
|
|
23
|
+
*/
|
|
24
|
+
incomingCallTimeout?: number;
|
|
25
|
+
/**
|
|
26
|
+
* Timeout in seconds for outgoing calls to connect before they are marked as unanswered.
|
|
27
|
+
* @default 60
|
|
28
|
+
* @platform ios
|
|
29
|
+
* @platform android
|
|
30
|
+
*/
|
|
31
|
+
outgoingCallTimeout?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Timeout in seconds for waiting for the call to connect after answering.
|
|
34
|
+
* @default 30
|
|
35
|
+
* @platform ios
|
|
36
|
+
* @platform android
|
|
37
|
+
*/
|
|
38
|
+
fulfillAnswerCallTimeout?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Array of sound file paths (relative to project root) to include in the app.
|
|
41
|
+
* These files will be copied into the iOS bundle and Android raw resources.
|
|
42
|
+
* Supported formats: .caf, .aiff, .wav (max 30 seconds for CallKit ringtones).
|
|
43
|
+
* @platform ios
|
|
44
|
+
* @platform android
|
|
45
|
+
*/
|
|
46
|
+
sounds?: string[];
|
|
47
|
+
/**
|
|
48
|
+
* The default ringtone for incoming calls on iOS (CallKit).
|
|
49
|
+
* Can be the filename (with extension) of one of the provided sounds,
|
|
50
|
+
* or 'default' to use the system ringtone.
|
|
51
|
+
* @default 'default'
|
|
52
|
+
* @platform ios
|
|
53
|
+
*/
|
|
54
|
+
defaultRingtoneIos?: string;
|
|
55
|
+
/**
|
|
56
|
+
* The default ringtone for incoming calls on Android (notification channel).
|
|
57
|
+
* Can be the filename (with extension) of one of the provided sounds,
|
|
58
|
+
* or 'default' to use the system ringtone.
|
|
59
|
+
* @default 'default'
|
|
60
|
+
* @platform android
|
|
61
|
+
*/
|
|
62
|
+
defaultRingtoneAndroid?: string;
|
|
63
|
+
/**
|
|
64
|
+
* The default dialtone to play during outgoing calls while connecting.
|
|
65
|
+
* Must be the filename (with extension) of one of the provided sounds.
|
|
66
|
+
* @platform ios
|
|
67
|
+
* @platform android
|
|
68
|
+
*/
|
|
69
|
+
defaultDialtone?: string;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const withExpoCallKitTelecom: ConfigPlugin<ExpoCallKitTelecomPluginProps | void> = (
|
|
73
|
+
config,
|
|
74
|
+
props,
|
|
75
|
+
) => {
|
|
76
|
+
const opts: ExpoCallKitTelecomPluginProps = props ?? {};
|
|
77
|
+
config = withExpoCallKitTelecomAndroid(config, opts);
|
|
78
|
+
config = withExpoCallKitTelecomIos(config, opts);
|
|
79
|
+
|
|
80
|
+
return config;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export default createRunOncePlugin(withExpoCallKitTelecom, pkg.name, pkg.version);
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AndroidConfig,
|
|
3
|
+
type ConfigPlugin,
|
|
4
|
+
withAndroidManifest,
|
|
5
|
+
withDangerousMod,
|
|
6
|
+
} from "expo/config-plugins";
|
|
7
|
+
import { copyFileSync, existsSync, mkdirSync } from "fs";
|
|
8
|
+
import { basename, resolve } from "path";
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_FULFILL_ANSWER_CALL_TIMEOUT,
|
|
12
|
+
DEFAULT_INCOMING_CALL_TIMEOUT,
|
|
13
|
+
DEFAULT_OUTGOING_CALL_TIMEOUT,
|
|
14
|
+
} from "./constants";
|
|
15
|
+
import type { ExpoCallKitTelecomPluginProps } from "./withExpoCallKitTelecom";
|
|
16
|
+
|
|
17
|
+
const ERROR_MSG_PREFIX = "An error occurred while configuring Android calls. ";
|
|
18
|
+
|
|
19
|
+
// biome-ignore lint/suspicious/noExplicitAny: Expo config plugin manifest types are untyped
|
|
20
|
+
function setMetaDataValue(app: any, key: string, value: string): void {
|
|
21
|
+
const existing = app["meta-data"]?.find(
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: manifest meta-data items are untyped
|
|
23
|
+
(item: any) => item.$["android:name"] === key,
|
|
24
|
+
);
|
|
25
|
+
if (existing) {
|
|
26
|
+
existing.$["android:value"] = value;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!app["meta-data"]) {
|
|
31
|
+
app["meta-data"] = [];
|
|
32
|
+
}
|
|
33
|
+
app["meta-data"].push({
|
|
34
|
+
$: {
|
|
35
|
+
"android:name": key,
|
|
36
|
+
"android:value": value,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Sanitizes a filename for use as an Android raw resource name.
|
|
43
|
+
*
|
|
44
|
+
* Android raw resource names must be lowercase, alphanumeric + underscores,
|
|
45
|
+
* and cannot start with a digit.
|
|
46
|
+
*/
|
|
47
|
+
function toAndroidRawResourceName(filename: string): string {
|
|
48
|
+
const withoutExtension = filename.replace(/\.[^.]+$/, "");
|
|
49
|
+
const sanitized = withoutExtension.toLowerCase().replace(/[^a-z0-9_]/g, "_");
|
|
50
|
+
return /^\d/.test(sanitized) ? `_${sanitized}` : sanitized;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configures call timeout values in AndroidManifest metadata.
|
|
55
|
+
*/
|
|
56
|
+
const withTimeouts: ConfigPlugin<{
|
|
57
|
+
incomingCallTimeout?: number;
|
|
58
|
+
outgoingCallTimeout?: number;
|
|
59
|
+
fulfillAnswerCallTimeout?: number;
|
|
60
|
+
}> = (
|
|
61
|
+
config,
|
|
62
|
+
{ incomingCallTimeout, outgoingCallTimeout, fulfillAnswerCallTimeout },
|
|
63
|
+
) => {
|
|
64
|
+
return withAndroidManifest(config, (config) => {
|
|
65
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
|
|
66
|
+
config.modResults,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
setMetaDataValue(
|
|
70
|
+
app,
|
|
71
|
+
"ExpoCallKitTelecomIncomingCallTimeout",
|
|
72
|
+
String(incomingCallTimeout ?? DEFAULT_INCOMING_CALL_TIMEOUT),
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
setMetaDataValue(
|
|
76
|
+
app,
|
|
77
|
+
"ExpoCallKitTelecomOutgoingCallTimeout",
|
|
78
|
+
String(outgoingCallTimeout ?? DEFAULT_OUTGOING_CALL_TIMEOUT),
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
setMetaDataValue(
|
|
82
|
+
app,
|
|
83
|
+
"ExpoCallKitTelecomFulfillAnswerCallTimeout",
|
|
84
|
+
String(fulfillAnswerCallTimeout ?? DEFAULT_FULFILL_ANSWER_CALL_TIMEOUT),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
return config;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Copies sound files into the Android raw resources directory.
|
|
93
|
+
*/
|
|
94
|
+
const withSounds: ConfigPlugin<{ sounds?: string[] }> = (
|
|
95
|
+
config,
|
|
96
|
+
{ sounds },
|
|
97
|
+
) => {
|
|
98
|
+
if (!sounds || sounds.length === 0) {
|
|
99
|
+
return config;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return withDangerousMod(config, [
|
|
103
|
+
"android",
|
|
104
|
+
(config) => {
|
|
105
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
106
|
+
const rawDir = resolve(
|
|
107
|
+
projectRoot,
|
|
108
|
+
"android",
|
|
109
|
+
"app",
|
|
110
|
+
"src",
|
|
111
|
+
"main",
|
|
112
|
+
"res",
|
|
113
|
+
"raw",
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
mkdirSync(rawDir, { recursive: true });
|
|
117
|
+
|
|
118
|
+
for (const soundPath of sounds) {
|
|
119
|
+
const filename = basename(soundPath);
|
|
120
|
+
const sourcePath = resolve(projectRoot, soundPath);
|
|
121
|
+
|
|
122
|
+
if (!existsSync(sourcePath)) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`${ERROR_MSG_PREFIX}Sound file not found: ${sourcePath}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const resourceName = toAndroidRawResourceName(filename);
|
|
129
|
+
const extension = filename.includes(".")
|
|
130
|
+
? filename.substring(filename.lastIndexOf("."))
|
|
131
|
+
: "";
|
|
132
|
+
const destinationPath = resolve(rawDir, `${resourceName}${extension}`);
|
|
133
|
+
|
|
134
|
+
copyFileSync(sourcePath, destinationPath);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return config;
|
|
138
|
+
},
|
|
139
|
+
]);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Configures the default dialtone for outgoing calls in AndroidManifest metadata.
|
|
144
|
+
*/
|
|
145
|
+
const withDefaultDialtone: ConfigPlugin<{
|
|
146
|
+
sounds?: string[];
|
|
147
|
+
defaultDialtone?: string;
|
|
148
|
+
}> = (config, { sounds, defaultDialtone }) => {
|
|
149
|
+
if (!defaultDialtone) {
|
|
150
|
+
return config;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const soundFilenames = sounds?.map((s) => basename(s)) ?? [];
|
|
154
|
+
|
|
155
|
+
if (soundFilenames.length === 0) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${ERROR_MSG_PREFIX}"defaultDialtone" was specified but no ` +
|
|
158
|
+
`sounds were provided.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (!soundFilenames.includes(defaultDialtone)) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
`${ERROR_MSG_PREFIX}"defaultDialtone" must be one of the provided ` +
|
|
164
|
+
`sounds (${soundFilenames.join(", ")}).`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return withAndroidManifest(config, (config) => {
|
|
169
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
|
|
170
|
+
config.modResults,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
setMetaDataValue(
|
|
174
|
+
app,
|
|
175
|
+
"ExpoCallKitTelecomDefaultDialtone",
|
|
176
|
+
toAndroidRawResourceName(defaultDialtone),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return config;
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Configures the default ringtone for incoming calls in AndroidManifest metadata.
|
|
185
|
+
*
|
|
186
|
+
* Mirrors the iOS `withDefaultRingtone` plugin. The Kotlin side reads
|
|
187
|
+
* `ExpoCallKitTelecomDefaultRingtone` from manifest metadata and sets it as the
|
|
188
|
+
* notification channel sound.
|
|
189
|
+
*/
|
|
190
|
+
const withDefaultRingtone: ConfigPlugin<{
|
|
191
|
+
sounds?: string[];
|
|
192
|
+
defaultRingtone?: string;
|
|
193
|
+
}> = (config, { sounds, defaultRingtone }) => {
|
|
194
|
+
if (!defaultRingtone || defaultRingtone === "default") {
|
|
195
|
+
return config;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const soundFilenames = sounds?.map((s) => basename(s)) ?? [];
|
|
199
|
+
|
|
200
|
+
if (soundFilenames.length === 0) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`${ERROR_MSG_PREFIX}"defaultRingtone" was specified but no ` +
|
|
203
|
+
`sounds were provided.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
if (!soundFilenames.includes(defaultRingtone)) {
|
|
207
|
+
throw new Error(
|
|
208
|
+
`${ERROR_MSG_PREFIX}"defaultRingtone" must be one of the provided ` +
|
|
209
|
+
`sounds (${soundFilenames.join(", ")}) or "default" for ` +
|
|
210
|
+
`system ringtone.`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return withAndroidManifest(config, (config) => {
|
|
215
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
|
|
216
|
+
config.modResults,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
setMetaDataValue(
|
|
220
|
+
app,
|
|
221
|
+
"ExpoCallKitTelecomDefaultRingtone",
|
|
222
|
+
toAndroidRawResourceName(defaultRingtone),
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
return config;
|
|
226
|
+
});
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Removes expo-notifications' ExpoFirebaseMessagingService from the manifest.
|
|
231
|
+
*
|
|
232
|
+
* Our ExpoCallKitTelecomMessagingService extends it and takes over as the sole
|
|
233
|
+
* MESSAGING_EVENT handler, delegating non-call messages via super.
|
|
234
|
+
* Having both services registered would cause undefined delivery behaviour.
|
|
235
|
+
*
|
|
236
|
+
* The service is declared in expo-notifications' library AndroidManifest.xml,
|
|
237
|
+
* so we must use `tools:node="remove"` to tell the manifest merger to strip it.
|
|
238
|
+
*/
|
|
239
|
+
const withFirebaseMessagingService: ConfigPlugin<ExpoCallKitTelecomPluginProps> = (
|
|
240
|
+
config,
|
|
241
|
+
) => {
|
|
242
|
+
return withAndroidManifest(config, (config) => {
|
|
243
|
+
const manifest = config.modResults.manifest;
|
|
244
|
+
|
|
245
|
+
// Ensure the tools namespace is declared on the root <manifest> element.
|
|
246
|
+
if (!manifest.$["xmlns:tools"]) {
|
|
247
|
+
manifest.$["xmlns:tools"] = "http://schemas.android.com/tools";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const app = AndroidConfig.Manifest.getMainApplicationOrThrow(
|
|
251
|
+
config.modResults,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (!app.service) {
|
|
255
|
+
app.service = [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const notificationsService =
|
|
259
|
+
"expo.modules.notifications.service.ExpoFirebaseMessagingService";
|
|
260
|
+
|
|
261
|
+
// Remove any existing entry first (idempotent across repeated prebuilds).
|
|
262
|
+
app.service = app.service.filter(
|
|
263
|
+
(service) => service.$?.["android:name"] !== notificationsService,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
// Add a tools:node="remove" marker so the manifest merger strips the
|
|
267
|
+
// library-declared service during the Gradle build.
|
|
268
|
+
const removeEntry = {
|
|
269
|
+
$: {
|
|
270
|
+
"android:name": notificationsService,
|
|
271
|
+
"tools:node": "remove",
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
app.service.push(removeEntry);
|
|
275
|
+
|
|
276
|
+
return config;
|
|
277
|
+
});
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
export const withExpoCallKitTelecomAndroid: ConfigPlugin<ExpoCallKitTelecomPluginProps> = (
|
|
281
|
+
config,
|
|
282
|
+
props,
|
|
283
|
+
) => {
|
|
284
|
+
config = withTimeouts(config, props);
|
|
285
|
+
config = withSounds(config, props);
|
|
286
|
+
config = withDefaultRingtone(config, {
|
|
287
|
+
sounds: props.sounds,
|
|
288
|
+
defaultRingtone: props.defaultRingtoneAndroid,
|
|
289
|
+
});
|
|
290
|
+
config = withDefaultDialtone(config, props);
|
|
291
|
+
config = withFirebaseMessagingService(config, props);
|
|
292
|
+
return config;
|
|
293
|
+
};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ConfigPlugin,
|
|
3
|
+
IOSConfig,
|
|
4
|
+
withEntitlementsPlist,
|
|
5
|
+
withInfoPlist,
|
|
6
|
+
withXcodeProject,
|
|
7
|
+
} from "expo/config-plugins";
|
|
8
|
+
import { copyFileSync, existsSync } from "fs";
|
|
9
|
+
import { basename, resolve } from "path";
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_FULFILL_ANSWER_CALL_TIMEOUT,
|
|
13
|
+
DEFAULT_INCOMING_CALL_TIMEOUT,
|
|
14
|
+
DEFAULT_OUTGOING_CALL_TIMEOUT,
|
|
15
|
+
} from "./constants";
|
|
16
|
+
import type { ExpoCallKitTelecomPluginProps } from "./withExpoCallKitTelecom";
|
|
17
|
+
|
|
18
|
+
const ERROR_MSG_PREFIX = "An error occurred while configuring iOS calls. ";
|
|
19
|
+
|
|
20
|
+
// Default permission messages
|
|
21
|
+
const CAMERA_USAGE = "Allow $(PRODUCT_NAME) to access your camera";
|
|
22
|
+
const MICROPHONE_USAGE = "Allow $(PRODUCT_NAME) to access your microphone";
|
|
23
|
+
|
|
24
|
+
// Required background modes for CallKit and PushKit:
|
|
25
|
+
// - voip: Receive VoIP push notifications to wake the app for incoming calls
|
|
26
|
+
// - audio: Continue audio playback/recording during calls when app is backgrounded
|
|
27
|
+
const BACKGROUND_MODES = ["voip", "audio"];
|
|
28
|
+
|
|
29
|
+
// SiriKit intents for voice-activated calls
|
|
30
|
+
// INStartCallIntent: Unified intent for iOS 13+ (recommended)
|
|
31
|
+
// INStartAudioCallIntent/INStartVideoCallIntent: Deprecated in iOS 13, but still
|
|
32
|
+
// sent by the system in some cases (e.g., redialing from call history)
|
|
33
|
+
const SIRI_INTENTS = [
|
|
34
|
+
"INStartCallIntent",
|
|
35
|
+
"INStartAudioCallIntent",
|
|
36
|
+
"INStartVideoCallIntent",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Configures camera and microphone permissions for VoIP and video calls.
|
|
41
|
+
*/
|
|
42
|
+
const withPermissions: ConfigPlugin<{
|
|
43
|
+
cameraPermission?: string | false;
|
|
44
|
+
microphonePermission?: string | false;
|
|
45
|
+
}> = (config, { cameraPermission, microphonePermission }) => {
|
|
46
|
+
return IOSConfig.Permissions.createPermissionsPlugin({
|
|
47
|
+
NSCameraUsageDescription: CAMERA_USAGE,
|
|
48
|
+
NSMicrophoneUsageDescription: MICROPHONE_USAGE,
|
|
49
|
+
})(config, {
|
|
50
|
+
NSCameraUsageDescription: cameraPermission,
|
|
51
|
+
NSMicrophoneUsageDescription: microphonePermission,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Configures push notification entitlement for PushKit VoIP notifications.
|
|
57
|
+
*/
|
|
58
|
+
const withPushNotificationEntitlement: ConfigPlugin = (config) => {
|
|
59
|
+
return withEntitlementsPlist(config, (config) => {
|
|
60
|
+
const key = "aps-environment";
|
|
61
|
+
// Only set if not already configured; production builds use provisioning profile value
|
|
62
|
+
if (!config.modResults[key]) {
|
|
63
|
+
config.modResults[key] = "development";
|
|
64
|
+
}
|
|
65
|
+
return config;
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Configures UIBackgroundModes for VoIP call handling.
|
|
71
|
+
*/
|
|
72
|
+
const withBackgroundModes: ConfigPlugin = (config) => {
|
|
73
|
+
return withInfoPlist(config, (config) => {
|
|
74
|
+
const existingModes = config.modResults.UIBackgroundModes;
|
|
75
|
+
const modes = Array.isArray(existingModes)
|
|
76
|
+
? (existingModes as string[])
|
|
77
|
+
: [];
|
|
78
|
+
|
|
79
|
+
const newModes = new Set([...modes, ...BACKGROUND_MODES]);
|
|
80
|
+
config.modResults.UIBackgroundModes = [...newModes];
|
|
81
|
+
|
|
82
|
+
return config;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Configures SiriKit intents for voice-activated audio/video calls.
|
|
88
|
+
*/
|
|
89
|
+
const withSiriIntents: ConfigPlugin = (config) => {
|
|
90
|
+
return withInfoPlist(config, (config) => {
|
|
91
|
+
const existingIntents = config.modResults.NSUserActivityTypes;
|
|
92
|
+
const intents = Array.isArray(existingIntents)
|
|
93
|
+
? (existingIntents as string[])
|
|
94
|
+
: [];
|
|
95
|
+
|
|
96
|
+
const newIntents = new Set([...intents, ...SIRI_INTENTS]);
|
|
97
|
+
config.modResults.NSUserActivityTypes = [...newIntents];
|
|
98
|
+
|
|
99
|
+
return config;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Configures call timeout values in Info.plist.
|
|
105
|
+
*/
|
|
106
|
+
const withTimeouts: ConfigPlugin<{
|
|
107
|
+
incomingCallTimeout?: number;
|
|
108
|
+
outgoingCallTimeout?: number;
|
|
109
|
+
fulfillAnswerCallTimeout?: number;
|
|
110
|
+
}> = (
|
|
111
|
+
config,
|
|
112
|
+
{ incomingCallTimeout, outgoingCallTimeout, fulfillAnswerCallTimeout },
|
|
113
|
+
) => {
|
|
114
|
+
return withInfoPlist(config, (config) => {
|
|
115
|
+
config.modResults.ExpoCallKitTelecomIncomingCallTimeout =
|
|
116
|
+
incomingCallTimeout ?? DEFAULT_INCOMING_CALL_TIMEOUT;
|
|
117
|
+
config.modResults.ExpoCallKitTelecomOutgoingCallTimeout =
|
|
118
|
+
outgoingCallTimeout ?? DEFAULT_OUTGOING_CALL_TIMEOUT;
|
|
119
|
+
config.modResults.ExpoCallKitTelecomFulfillAnswerCallTimeout =
|
|
120
|
+
fulfillAnswerCallTimeout ?? DEFAULT_FULFILL_ANSWER_CALL_TIMEOUT;
|
|
121
|
+
return config;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Copies sound files into the iOS project bundle.
|
|
127
|
+
*/
|
|
128
|
+
function setSoundFiles(
|
|
129
|
+
config: Parameters<Parameters<typeof withXcodeProject>[1]>[0],
|
|
130
|
+
sounds: string[],
|
|
131
|
+
) {
|
|
132
|
+
const projectRoot = config.modRequest.projectRoot;
|
|
133
|
+
const projectName = config.modRequest.projectName;
|
|
134
|
+
|
|
135
|
+
if (!projectName) {
|
|
136
|
+
throw new Error(`${ERROR_MSG_PREFIX}Unable to find iOS project name.`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sourceRoot = resolve(projectRoot, "ios", projectName);
|
|
140
|
+
|
|
141
|
+
for (const soundPath of sounds) {
|
|
142
|
+
const filename = basename(soundPath);
|
|
143
|
+
const sourcePath = resolve(projectRoot, soundPath);
|
|
144
|
+
const destinationPath = resolve(sourceRoot, filename);
|
|
145
|
+
|
|
146
|
+
if (!existsSync(sourcePath)) {
|
|
147
|
+
throw new Error(`${ERROR_MSG_PREFIX}Sound file not found: ${sourcePath}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Copy the file to the iOS project directory
|
|
151
|
+
copyFileSync(sourcePath, destinationPath);
|
|
152
|
+
|
|
153
|
+
// Add the file to the Xcode project if not already present
|
|
154
|
+
if (!config.modResults.hasFile(`${projectName}/${filename}`)) {
|
|
155
|
+
config.modResults = IOSConfig.XcodeUtils.addResourceFileToGroup({
|
|
156
|
+
filepath: `${projectName}/${filename}`,
|
|
157
|
+
groupName: projectName,
|
|
158
|
+
isBuildFile: true,
|
|
159
|
+
project: config.modResults,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return config;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Copies sound files into the iOS project bundle.
|
|
169
|
+
*/
|
|
170
|
+
const withSounds: ConfigPlugin<{ sounds?: string[] }> = (
|
|
171
|
+
config,
|
|
172
|
+
{ sounds },
|
|
173
|
+
) => {
|
|
174
|
+
if (sounds && sounds.length > 0) {
|
|
175
|
+
config = withXcodeProject(config, (config) => {
|
|
176
|
+
return setSoundFiles(config, sounds);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return config;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Configures the default ringtone for incoming calls in Info.plist.
|
|
184
|
+
*/
|
|
185
|
+
const withDefaultRingtone: ConfigPlugin<{
|
|
186
|
+
sounds?: string[];
|
|
187
|
+
defaultRingtone?: string;
|
|
188
|
+
}> = (config, { sounds, defaultRingtone }) => {
|
|
189
|
+
const soundFilenames = sounds?.map((s) => basename(s)) ?? [];
|
|
190
|
+
|
|
191
|
+
// Validate defaultRingtone if specified and not 'default'
|
|
192
|
+
if (defaultRingtone && defaultRingtone !== "default") {
|
|
193
|
+
if (soundFilenames.length === 0) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`${ERROR_MSG_PREFIX}"defaultRingtone" was specified but no ` +
|
|
196
|
+
`sounds were provided.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (!soundFilenames.includes(defaultRingtone)) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`${ERROR_MSG_PREFIX}"defaultRingtone" must be one of the provided ` +
|
|
202
|
+
`sounds (${soundFilenames.join(", ")}) or "default" for ` +
|
|
203
|
+
`system ringtone.`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return withInfoPlist(config, (config) => {
|
|
209
|
+
config.modResults.ExpoCallKitTelecomDefaultRingtone = defaultRingtone || "default";
|
|
210
|
+
return config;
|
|
211
|
+
});
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Configures the default dialtone for outgoing calls in Info.plist.
|
|
216
|
+
*/
|
|
217
|
+
const withDefaultDialtone: ConfigPlugin<{
|
|
218
|
+
sounds?: string[];
|
|
219
|
+
defaultDialtone?: string;
|
|
220
|
+
}> = (config, { sounds, defaultDialtone }) => {
|
|
221
|
+
if (!defaultDialtone) {
|
|
222
|
+
return config;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const soundFilenames = sounds?.map((s) => basename(s)) ?? [];
|
|
226
|
+
|
|
227
|
+
if (soundFilenames.length === 0) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`${ERROR_MSG_PREFIX}"defaultDialtone" was specified but no ` +
|
|
230
|
+
`sounds were provided.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (!soundFilenames.includes(defaultDialtone)) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`${ERROR_MSG_PREFIX}"defaultDialtone" must be one of the provided ` +
|
|
236
|
+
`sounds (${soundFilenames.join(", ")}).`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return withInfoPlist(config, (config) => {
|
|
241
|
+
config.modResults.ExpoCallKitTelecomDefaultDialtone = defaultDialtone;
|
|
242
|
+
return config;
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export const withExpoCallKitTelecomIos: ConfigPlugin<ExpoCallKitTelecomPluginProps> = (
|
|
247
|
+
config,
|
|
248
|
+
{
|
|
249
|
+
cameraPermission,
|
|
250
|
+
microphonePermission,
|
|
251
|
+
incomingCallTimeout,
|
|
252
|
+
outgoingCallTimeout,
|
|
253
|
+
fulfillAnswerCallTimeout,
|
|
254
|
+
sounds,
|
|
255
|
+
defaultRingtoneIos,
|
|
256
|
+
defaultDialtone,
|
|
257
|
+
},
|
|
258
|
+
) => {
|
|
259
|
+
config = withPermissions(config, { cameraPermission, microphonePermission });
|
|
260
|
+
config = withPushNotificationEntitlement(config);
|
|
261
|
+
config = withBackgroundModes(config);
|
|
262
|
+
config = withSiriIntents(config);
|
|
263
|
+
config = withTimeouts(config, {
|
|
264
|
+
incomingCallTimeout,
|
|
265
|
+
outgoingCallTimeout,
|
|
266
|
+
fulfillAnswerCallTimeout,
|
|
267
|
+
});
|
|
268
|
+
config = withSounds(config, { sounds });
|
|
269
|
+
config = withDefaultRingtone(config, {
|
|
270
|
+
sounds,
|
|
271
|
+
defaultRingtone: defaultRingtoneIos,
|
|
272
|
+
});
|
|
273
|
+
config = withDefaultDialtone(config, { sounds, defaultDialtone });
|
|
274
|
+
|
|
275
|
+
return config;
|
|
276
|
+
};
|