@poolse/react-native 2.6.0 → 2.7.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/dist/index.d.cts CHANGED
@@ -1095,6 +1095,16 @@ interface CallScreenProps {
1095
1095
  * {@link VoiceRoomBarProps.onStatusChange}.
1096
1096
  */
1097
1097
  onStatusChange?: (status: VoiceStatus) => void;
1098
+ /**
1099
+ * Hide this screen while a call is merely ringing in.
1100
+ *
1101
+ * Set it when `useVoipCalls` is wired: CallKit already shows the
1102
+ * system call UI on the lock screen and in the Dynamic Island, and a
1103
+ * second in-app sheet on top of it means one tap of Call produces two
1104
+ * competing rings. The screen still appears once the call connects, to
1105
+ * carry the in-call controls.
1106
+ */
1107
+ nativeIncomingUi?: boolean;
1098
1108
  }
1099
1109
  /**
1100
1110
  * WhatsApp-style call UI: a full-screen sheet that covers the app while
@@ -1111,58 +1121,58 @@ interface CallScreenProps {
1111
1121
  * to whatever button your UI wants. Pass the same instance in via
1112
1122
  * `calls` so both halves share one state machine.
1113
1123
  */
1114
- declare function CallScreen({ userId, labelFor, calls: external, webrtc, onStatusChange, }: CallScreenProps): react_jsx_runtime.JSX.Element;
1124
+ declare function CallScreen({ userId, labelFor, calls: external, webrtc, onStatusChange, nativeIncomingUi, }: CallScreenProps): react_jsx_runtime.JSX.Element;
1115
1125
 
1116
1126
  /**
1117
- * Bridges iOS PushKit + CallKit to poolse calls, so a call rings a
1118
- * backgrounded or closed app.
1127
+ * Bridges iOS PushKit + CallKit to poolse calls, and owns the incoming
1128
+ * call UI end to end.
1129
+ *
1130
+ * ## Why this coordinates rather than just forwards
1119
1131
  *
1120
- * ## Why this is needed at all
1132
+ * A call arrives by two routes: `call:incoming` over the WebSocket when
1133
+ * the app is foregrounded, and a VoIP push when it isn't. The server
1134
+ * fires both, always — deciding server-side whether a socket is really
1135
+ * alive is a race it would lose.
1121
1136
  *
1122
- * `call:incoming` travels over the WebSocket, which only exists while
1123
- * the app is foregrounded — iOS suspends sockets on background and
1124
- * drops them on close. Reaching a sleeping phone requires the OS to wake
1125
- * the app, which means a VoIP push against a PushKit token.
1137
+ * Reconciling them is therefore the client's job, and doing it badly is
1138
+ * very visible: forward each route to its own UI and one tap of Call
1139
+ * shows the callee two rings, an in-app sheet fighting a system call
1140
+ * screen.
1141
+ *
1142
+ * So this hook is the single place a call becomes visible. Both routes
1143
+ * feed one map keyed by `callId`, CallKit is the only ring UI, and the
1144
+ * native call is torn down whenever the call reaches a terminal state —
1145
+ * declined, cancelled, timed out, answered. Pass the same `calls` object
1146
+ * to `<CallScreen>` and it stays out of the way while a ring is up.
1126
1147
  *
1127
1148
  * ## The rule Apple enforces
1128
1149
  *
1129
1150
  * Every delivered VoIP push MUST report an incoming call to CallKit,
1130
1151
  * essentially immediately. Miss it and iOS terminates the app; do it
1131
- * repeatedly and the system stops delivering VoIP pushes to you at all.
1132
- * `reportCall` below is not optional politeness — it is the contract.
1152
+ * repeatedly and the system stops delivering VoIP pushes at all.
1133
1153
  *
1134
1154
  * ## Dependencies
1135
1155
  *
1136
1156
  * `react-native-voip-push-notification` and `react-native-callkeep`,
1137
- * both native, both requiring a development build. Like
1138
- * `react-native-webrtc`, Metro only bundles what it can see statically,
1139
- * so YOUR APP imports them and passes them in:
1140
- *
1141
- * import VoipPushNotification from 'react-native-voip-push-notification';
1142
- * import RNCallKeep from 'react-native-callkeep';
1157
+ * both native and both requiring a development build. Metro only bundles
1158
+ * what it can see statically, so YOUR APP imports them and passes them
1159
+ * in. The iOS AppDelegate also needs PushKit delegate methods, which
1160
+ * Expo does not generate — see the sample's `plugins/withVoipPush.js`.
1143
1161
  *
1144
1162
  * useVoipCalls({
1145
1163
  * voipPush: VoipPushNotification,
1146
1164
  * callKeep: RNCallKeep,
1147
- * onAnswer: (call) => { ... },
1165
+ * calls, // the same useCalls() the UI uses
1166
+ * apnsEnvironment: 'sandbox',
1148
1167
  * });
1149
1168
  */
1150
- /**
1151
- * The slice of `react-native-voip-push-notification` used here.
1152
- *
1153
- * Loose on purpose. The real module types `addEventListener` as a
1154
- * generic whose handler payload varies per event, and pinning exact
1155
- * signatures here makes `voipPush={VoipPushNotification}` fail to
1156
- * compile — the prop becomes unusable, which is the whole point of it.
1157
- * Method syntax (not property arrow syntax) keeps the check bivariant.
1158
- */
1169
+ /** The slice of `react-native-voip-push-notification` used here. */
1159
1170
  interface VoipPushModule {
1160
1171
  addEventListener(event: string, handler: (payload: unknown) => void): void;
1161
1172
  removeEventListener(event: string): void;
1162
1173
  registerVoipToken(): void;
1163
1174
  }
1164
- /** The slice of `react-native-callkeep` used here. Loose for the same
1165
- * reason — `setup` resolves to a boolean in the real module. */
1175
+ /** The slice of `react-native-callkeep` used here. */
1166
1176
  interface CallKeepModule {
1167
1177
  setup(options: unknown): Promise<unknown>;
1168
1178
  displayIncomingCall(uuid: string, handle: string, localizedCallerName?: string, handleType?: string, hasVideo?: boolean): unknown;
@@ -1181,12 +1191,15 @@ interface UseVoipCallsOptions {
1181
1191
  voipPush: VoipPushModule;
1182
1192
  callKeep: CallKeepModule;
1183
1193
  /**
1184
- * Fired when the user answers from the native call UI. Accept the
1185
- * call and join the room here — the app may have been launched cold,
1186
- * so don't assume any prior state.
1194
+ * The `useCalls(...)` instance driving this screen. Supplying it lets
1195
+ * the hook answer, decline and dismiss through the same state machine
1196
+ * the UI uses, rather than two halves disagreeing about what is
1197
+ * happening.
1187
1198
  */
1199
+ calls?: UseCalls;
1200
+ /** Called when the user answers from the native call UI. */
1188
1201
  onAnswer?: (call: VoipIncomingCall) => void;
1189
- /** Fired when the user declines from the native call UI. */
1202
+ /** Called when the user declines from the native call UI. */
1190
1203
  onDecline?: (call: VoipIncomingCall) => void;
1191
1204
  /** CallKit display name for your app. Shown on the lock screen. */
1192
1205
  appName?: string;
@@ -1203,13 +1216,23 @@ interface UseVoipCallsOptions {
1203
1216
  * rings — with nothing in the app to suggest why.
1204
1217
  */
1205
1218
  apnsEnvironment?: 'production' | 'sandbox';
1219
+ /**
1220
+ * How old a pushed call may be before it is ignored, in ms. Defaults
1221
+ * to 45s, matching `useCalls`' ring timeout — past that the caller has
1222
+ * already given up, so there is nothing live to answer.
1223
+ *
1224
+ * PushKit holds a push that lands before the JS bundle is up and
1225
+ * replays it on launch, so opening the app can ring you for a call the
1226
+ * caller hung up on minutes ago — a ring with nothing live to accept.
1227
+ */
1228
+ staleAfterMs?: number;
1206
1229
  }
1207
1230
  /**
1208
- * Registers this device for VoIP pushes and wires the native call UI.
1231
+ * Registers this device for VoIP pushes and drives the native call UI.
1209
1232
  *
1210
- * Safe to mount once at the app root. Does nothing on Android, which
1211
- * needs a separate FCM + ConnectionService integration.
1233
+ * Mount once, as high in the signed-in tree as possible. Does nothing on
1234
+ * Android, which needs a separate FCM + ConnectionService integration.
1212
1235
  */
1213
- declare function useVoipCalls({ voipPush, callKeep, onAnswer, onDecline, appName, apnsEnvironment, }: UseVoipCallsOptions): void;
1236
+ declare function useVoipCalls({ voipPush, callKeep, calls, onAnswer, onDecline, appName, apnsEnvironment, staleAfterMs, }: UseVoipCallsOptions): void;
1214
1237
 
1215
1238
  export { AttachmentPicker, type AttachmentPickerProps, AttachmentPreview, type AttachmentPreviewProps, Avatar, type AvatarProps, type CallKeepModule, CallScreen, type CallScreenProps, ChatHeader, type ChatHeaderProps, ConversationList, type ConversationListProps, ConversationView, type ConversationViewProps, DaySeparator, type DaySeparatorProps, GroupDetailsSheet, type GroupDetailsSheetProps, type IconName, ImageMosaic, type ImageMosaicProps, type InboxUser, MemberList, type MemberListProps, MentionInput, type MentionInputProps, MessageActions, type MessageActionsProps, MessageBubble, type MessageBubbleProps, MessageComposer, type MessageComposerProps, MessageList, type MessageListItem, type MessageListProps, MessageRow, type MessageRowProps, type NativeWebRtcModule, PoolseIcon, type PoolseIconProps, PoolseInbox, type PoolseInboxHandle, type PoolseInboxProps, PoolseLogo, type PoolseLogoProps, type PoolseLogoVariant, type PoolseRNTheme, type PoolseRNThemeOverrides, PoolseTheme, type PoolseThemeProps, primitives as Primitives, ReactionPicker, type ReactionPickerProps, ReactionStrip, type ReactionStripProps, type ThemeColors, type ThemeRadii, type ThemeShadows, type ThemeSpacing, type ThemeType, ThreadView, type ThreadViewProps, TypingIndicator, type TypingIndicatorProps, UploadQueueStrip, type UseVoipCallsOptions, UserName, type UserNameProps, VoiceRoomBar, type VoiceRoomBarProps, type VoipIncomingCall, type VoipPushModule, createNativeWebRtcAdapter, defaultDarkTheme, defaultLightTheme, isNativeWebRtcAvailable, useAppStateLifecycle, useDisplayName, usePoolseTheme, useVoipCalls };
package/dist/index.d.ts CHANGED
@@ -1095,6 +1095,16 @@ interface CallScreenProps {
1095
1095
  * {@link VoiceRoomBarProps.onStatusChange}.
1096
1096
  */
1097
1097
  onStatusChange?: (status: VoiceStatus) => void;
1098
+ /**
1099
+ * Hide this screen while a call is merely ringing in.
1100
+ *
1101
+ * Set it when `useVoipCalls` is wired: CallKit already shows the
1102
+ * system call UI on the lock screen and in the Dynamic Island, and a
1103
+ * second in-app sheet on top of it means one tap of Call produces two
1104
+ * competing rings. The screen still appears once the call connects, to
1105
+ * carry the in-call controls.
1106
+ */
1107
+ nativeIncomingUi?: boolean;
1098
1108
  }
1099
1109
  /**
1100
1110
  * WhatsApp-style call UI: a full-screen sheet that covers the app while
@@ -1111,58 +1121,58 @@ interface CallScreenProps {
1111
1121
  * to whatever button your UI wants. Pass the same instance in via
1112
1122
  * `calls` so both halves share one state machine.
1113
1123
  */
1114
- declare function CallScreen({ userId, labelFor, calls: external, webrtc, onStatusChange, }: CallScreenProps): react_jsx_runtime.JSX.Element;
1124
+ declare function CallScreen({ userId, labelFor, calls: external, webrtc, onStatusChange, nativeIncomingUi, }: CallScreenProps): react_jsx_runtime.JSX.Element;
1115
1125
 
1116
1126
  /**
1117
- * Bridges iOS PushKit + CallKit to poolse calls, so a call rings a
1118
- * backgrounded or closed app.
1127
+ * Bridges iOS PushKit + CallKit to poolse calls, and owns the incoming
1128
+ * call UI end to end.
1129
+ *
1130
+ * ## Why this coordinates rather than just forwards
1119
1131
  *
1120
- * ## Why this is needed at all
1132
+ * A call arrives by two routes: `call:incoming` over the WebSocket when
1133
+ * the app is foregrounded, and a VoIP push when it isn't. The server
1134
+ * fires both, always — deciding server-side whether a socket is really
1135
+ * alive is a race it would lose.
1121
1136
  *
1122
- * `call:incoming` travels over the WebSocket, which only exists while
1123
- * the app is foregrounded — iOS suspends sockets on background and
1124
- * drops them on close. Reaching a sleeping phone requires the OS to wake
1125
- * the app, which means a VoIP push against a PushKit token.
1137
+ * Reconciling them is therefore the client's job, and doing it badly is
1138
+ * very visible: forward each route to its own UI and one tap of Call
1139
+ * shows the callee two rings, an in-app sheet fighting a system call
1140
+ * screen.
1141
+ *
1142
+ * So this hook is the single place a call becomes visible. Both routes
1143
+ * feed one map keyed by `callId`, CallKit is the only ring UI, and the
1144
+ * native call is torn down whenever the call reaches a terminal state —
1145
+ * declined, cancelled, timed out, answered. Pass the same `calls` object
1146
+ * to `<CallScreen>` and it stays out of the way while a ring is up.
1126
1147
  *
1127
1148
  * ## The rule Apple enforces
1128
1149
  *
1129
1150
  * Every delivered VoIP push MUST report an incoming call to CallKit,
1130
1151
  * essentially immediately. Miss it and iOS terminates the app; do it
1131
- * repeatedly and the system stops delivering VoIP pushes to you at all.
1132
- * `reportCall` below is not optional politeness — it is the contract.
1152
+ * repeatedly and the system stops delivering VoIP pushes at all.
1133
1153
  *
1134
1154
  * ## Dependencies
1135
1155
  *
1136
1156
  * `react-native-voip-push-notification` and `react-native-callkeep`,
1137
- * both native, both requiring a development build. Like
1138
- * `react-native-webrtc`, Metro only bundles what it can see statically,
1139
- * so YOUR APP imports them and passes them in:
1140
- *
1141
- * import VoipPushNotification from 'react-native-voip-push-notification';
1142
- * import RNCallKeep from 'react-native-callkeep';
1157
+ * both native and both requiring a development build. Metro only bundles
1158
+ * what it can see statically, so YOUR APP imports them and passes them
1159
+ * in. The iOS AppDelegate also needs PushKit delegate methods, which
1160
+ * Expo does not generate — see the sample's `plugins/withVoipPush.js`.
1143
1161
  *
1144
1162
  * useVoipCalls({
1145
1163
  * voipPush: VoipPushNotification,
1146
1164
  * callKeep: RNCallKeep,
1147
- * onAnswer: (call) => { ... },
1165
+ * calls, // the same useCalls() the UI uses
1166
+ * apnsEnvironment: 'sandbox',
1148
1167
  * });
1149
1168
  */
1150
- /**
1151
- * The slice of `react-native-voip-push-notification` used here.
1152
- *
1153
- * Loose on purpose. The real module types `addEventListener` as a
1154
- * generic whose handler payload varies per event, and pinning exact
1155
- * signatures here makes `voipPush={VoipPushNotification}` fail to
1156
- * compile — the prop becomes unusable, which is the whole point of it.
1157
- * Method syntax (not property arrow syntax) keeps the check bivariant.
1158
- */
1169
+ /** The slice of `react-native-voip-push-notification` used here. */
1159
1170
  interface VoipPushModule {
1160
1171
  addEventListener(event: string, handler: (payload: unknown) => void): void;
1161
1172
  removeEventListener(event: string): void;
1162
1173
  registerVoipToken(): void;
1163
1174
  }
1164
- /** The slice of `react-native-callkeep` used here. Loose for the same
1165
- * reason — `setup` resolves to a boolean in the real module. */
1175
+ /** The slice of `react-native-callkeep` used here. */
1166
1176
  interface CallKeepModule {
1167
1177
  setup(options: unknown): Promise<unknown>;
1168
1178
  displayIncomingCall(uuid: string, handle: string, localizedCallerName?: string, handleType?: string, hasVideo?: boolean): unknown;
@@ -1181,12 +1191,15 @@ interface UseVoipCallsOptions {
1181
1191
  voipPush: VoipPushModule;
1182
1192
  callKeep: CallKeepModule;
1183
1193
  /**
1184
- * Fired when the user answers from the native call UI. Accept the
1185
- * call and join the room here — the app may have been launched cold,
1186
- * so don't assume any prior state.
1194
+ * The `useCalls(...)` instance driving this screen. Supplying it lets
1195
+ * the hook answer, decline and dismiss through the same state machine
1196
+ * the UI uses, rather than two halves disagreeing about what is
1197
+ * happening.
1187
1198
  */
1199
+ calls?: UseCalls;
1200
+ /** Called when the user answers from the native call UI. */
1188
1201
  onAnswer?: (call: VoipIncomingCall) => void;
1189
- /** Fired when the user declines from the native call UI. */
1202
+ /** Called when the user declines from the native call UI. */
1190
1203
  onDecline?: (call: VoipIncomingCall) => void;
1191
1204
  /** CallKit display name for your app. Shown on the lock screen. */
1192
1205
  appName?: string;
@@ -1203,13 +1216,23 @@ interface UseVoipCallsOptions {
1203
1216
  * rings — with nothing in the app to suggest why.
1204
1217
  */
1205
1218
  apnsEnvironment?: 'production' | 'sandbox';
1219
+ /**
1220
+ * How old a pushed call may be before it is ignored, in ms. Defaults
1221
+ * to 45s, matching `useCalls`' ring timeout — past that the caller has
1222
+ * already given up, so there is nothing live to answer.
1223
+ *
1224
+ * PushKit holds a push that lands before the JS bundle is up and
1225
+ * replays it on launch, so opening the app can ring you for a call the
1226
+ * caller hung up on minutes ago — a ring with nothing live to accept.
1227
+ */
1228
+ staleAfterMs?: number;
1206
1229
  }
1207
1230
  /**
1208
- * Registers this device for VoIP pushes and wires the native call UI.
1231
+ * Registers this device for VoIP pushes and drives the native call UI.
1209
1232
  *
1210
- * Safe to mount once at the app root. Does nothing on Android, which
1211
- * needs a separate FCM + ConnectionService integration.
1233
+ * Mount once, as high in the signed-in tree as possible. Does nothing on
1234
+ * Android, which needs a separate FCM + ConnectionService integration.
1212
1235
  */
1213
- declare function useVoipCalls({ voipPush, callKeep, onAnswer, onDecline, appName, apnsEnvironment, }: UseVoipCallsOptions): void;
1236
+ declare function useVoipCalls({ voipPush, callKeep, calls, onAnswer, onDecline, appName, apnsEnvironment, staleAfterMs, }: UseVoipCallsOptions): void;
1214
1237
 
1215
1238
  export { AttachmentPicker, type AttachmentPickerProps, AttachmentPreview, type AttachmentPreviewProps, Avatar, type AvatarProps, type CallKeepModule, CallScreen, type CallScreenProps, ChatHeader, type ChatHeaderProps, ConversationList, type ConversationListProps, ConversationView, type ConversationViewProps, DaySeparator, type DaySeparatorProps, GroupDetailsSheet, type GroupDetailsSheetProps, type IconName, ImageMosaic, type ImageMosaicProps, type InboxUser, MemberList, type MemberListProps, MentionInput, type MentionInputProps, MessageActions, type MessageActionsProps, MessageBubble, type MessageBubbleProps, MessageComposer, type MessageComposerProps, MessageList, type MessageListItem, type MessageListProps, MessageRow, type MessageRowProps, type NativeWebRtcModule, PoolseIcon, type PoolseIconProps, PoolseInbox, type PoolseInboxHandle, type PoolseInboxProps, PoolseLogo, type PoolseLogoProps, type PoolseLogoVariant, type PoolseRNTheme, type PoolseRNThemeOverrides, PoolseTheme, type PoolseThemeProps, primitives as Primitives, ReactionPicker, type ReactionPickerProps, ReactionStrip, type ReactionStripProps, type ThemeColors, type ThemeRadii, type ThemeShadows, type ThemeSpacing, type ThemeType, ThreadView, type ThreadViewProps, TypingIndicator, type TypingIndicatorProps, UploadQueueStrip, type UseVoipCallsOptions, UserName, type UserNameProps, VoiceRoomBar, type VoiceRoomBarProps, type VoipIncomingCall, type VoipPushModule, createNativeWebRtcAdapter, defaultDarkTheme, defaultLightTheme, isNativeWebRtcAvailable, useAppStateLifecycle, useDisplayName, usePoolseTheme, useVoipCalls };
package/dist/index.js CHANGED
@@ -5088,7 +5088,8 @@ function CallScreen({
5088
5088
  labelFor,
5089
5089
  calls: external,
5090
5090
  webrtc,
5091
- onStatusChange
5091
+ onStatusChange,
5092
+ nativeIncomingUi = false
5092
5093
  }) {
5093
5094
  const theme = usePoolseTheme();
5094
5095
  const own = useCalls(external ? null : userId);
@@ -5109,7 +5110,8 @@ function CallScreen({
5109
5110
  onStatusChange?.(voice.status);
5110
5111
  }, [voice.status, onStatusChange]);
5111
5112
  const label = labelFor ?? ((id) => `User ${id.slice(0, 6)}`);
5112
- const visible = calls.phase !== "idle";
5113
+ const ringingIn = calls.phase === "ringing-in";
5114
+ const visible = calls.phase !== "idle" && !(nativeIncomingUi && ringingIn);
5113
5115
  const hangUp = () => {
5114
5116
  voice.leave();
5115
5117
  calls.reset();
@@ -5210,29 +5212,50 @@ var styles24 = StyleSheet.create({
5210
5212
  function useVoipCalls({
5211
5213
  voipPush,
5212
5214
  callKeep,
5215
+ calls,
5213
5216
  onAnswer,
5214
5217
  onDecline,
5215
5218
  appName = "poolse",
5216
- apnsEnvironment
5219
+ apnsEnvironment,
5220
+ staleAfterMs = 45e3
5217
5221
  }) {
5218
5222
  const poolse = usePoolse();
5219
- const pending = useRef(/* @__PURE__ */ new Map());
5220
- const handlers = useRef({ onAnswer, onDecline });
5221
- handlers.current = { onAnswer, onDecline };
5223
+ const shown = useRef(/* @__PURE__ */ new Map());
5224
+ const seen = useRef(/* @__PURE__ */ new Set());
5225
+ const handlers = useRef({ onAnswer, onDecline, calls });
5226
+ handlers.current = { onAnswer, onDecline, calls };
5227
+ const staleRef = useRef(staleAfterMs);
5228
+ staleRef.current = staleAfterMs;
5229
+ const dismiss = useRef((callId) => {
5230
+ if (!shown.current.has(callId)) return;
5231
+ shown.current.delete(callId);
5232
+ try {
5233
+ callKeep.endCall(callId);
5234
+ } catch {
5235
+ }
5236
+ });
5237
+ const present = useRef((call, sentAt) => {
5238
+ if (seen.current.has(call.callId)) return;
5239
+ if (typeof sentAt === "number" && Date.now() - sentAt > staleRef.current) return;
5240
+ seen.current.add(call.callId);
5241
+ shown.current.set(call.callId, call);
5242
+ callKeep.displayIncomingCall(call.callId, call.callerUserId, call.callerName, "generic", false);
5243
+ });
5222
5244
  useEffect(() => {
5223
5245
  let cancelled = false;
5224
- const reportCall = (payload) => {
5246
+ const fromPush = (payload) => {
5225
5247
  const callId = payload.call_id;
5226
5248
  const conversationId = payload.conversation_id;
5227
5249
  if (!callId || !conversationId) return;
5228
- const call = {
5229
- callId,
5230
- conversationId,
5231
- callerUserId: payload.caller_user_id ?? "",
5232
- callerName: payload.caller_name ?? "Incoming call"
5233
- };
5234
- pending.current.set(callId, call);
5235
- callKeep.displayIncomingCall(callId, call.callerUserId, call.callerName, "generic", false);
5250
+ present.current(
5251
+ {
5252
+ callId,
5253
+ conversationId,
5254
+ callerUserId: payload.caller_user_id ?? "",
5255
+ callerName: payload.caller_name ?? "Incoming call"
5256
+ },
5257
+ payload.sent_at
5258
+ );
5236
5259
  };
5237
5260
  void callKeep.setup({
5238
5261
  ios: { appName },
@@ -5255,23 +5278,36 @@ function useVoipCalls({
5255
5278
  });
5256
5279
  }));
5257
5280
  voipPush.addEventListener("notification", ((payload) => {
5258
- reportCall(payload);
5281
+ fromPush(payload);
5259
5282
  }));
5260
5283
  voipPush.addEventListener("didLoadWithEvents", ((events) => {
5261
5284
  for (const event of events ?? []) {
5262
5285
  if (event.name === "RNVoipPushRemoteNotificationReceivedEvent") {
5263
- reportCall(event.data);
5286
+ fromPush(event.data);
5264
5287
  }
5265
5288
  }
5266
5289
  }));
5267
5290
  callKeep.addEventListener("answerCall", (({ callUUID }) => {
5268
- const call = pending.current.get(callUUID);
5269
- if (call) handlers.current.onAnswer?.(call);
5291
+ const call = shown.current.get(callUUID);
5292
+ if (!call) return;
5293
+ shown.current.delete(callUUID);
5294
+ void handlers.current.calls?.accept({
5295
+ callId: call.callId,
5296
+ conversationId: call.conversationId,
5297
+ callerUserId: call.callerUserId
5298
+ });
5299
+ handlers.current.onAnswer?.(call);
5270
5300
  }));
5271
5301
  callKeep.addEventListener("endCall", (({ callUUID }) => {
5272
- const call = pending.current.get(callUUID);
5273
- pending.current.delete(callUUID);
5274
- if (call) handlers.current.onDecline?.(call);
5302
+ const call = shown.current.get(callUUID);
5303
+ shown.current.delete(callUUID);
5304
+ if (!call) return;
5305
+ void handlers.current.calls?.decline({
5306
+ callId: call.callId,
5307
+ conversationId: call.conversationId,
5308
+ callerUserId: call.callerUserId
5309
+ });
5310
+ handlers.current.onDecline?.(call);
5275
5311
  }));
5276
5312
  voipPush.registerVoipToken();
5277
5313
  return () => {
@@ -5283,6 +5319,22 @@ function useVoipCalls({
5283
5319
  callKeep.removeEventListener("endCall");
5284
5320
  };
5285
5321
  }, [poolse, voipPush, callKeep, appName, apnsEnvironment]);
5322
+ const incoming = calls?.incoming ?? null;
5323
+ const phase = calls?.phase;
5324
+ useEffect(() => {
5325
+ if (phase === "ringing-in" && incoming) {
5326
+ present.current({
5327
+ callId: incoming.callId,
5328
+ conversationId: incoming.conversationId,
5329
+ callerUserId: incoming.callerUserId,
5330
+ callerName: incoming.callerUserId
5331
+ });
5332
+ }
5333
+ }, [phase, incoming]);
5334
+ useEffect(() => {
5335
+ if (phase === "ringing-in" || phase === "ringing-out") return;
5336
+ for (const callId of [...shown.current.keys()]) dismiss.current(callId);
5337
+ }, [phase]);
5286
5338
  }
5287
5339
  function guessApnsEnvironment() {
5288
5340
  return __DEV__ ? "sandbox" : "production";