@poolse/react-native 2.17.1 → 2.19.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/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to `@poolse/react-native` are documented here.
4
4
  Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
5
5
  versions follow [semver](https://semver.org).
6
6
 
7
+ ## [2.19.0] — 2026-08-25
8
+
9
+ ### Added
10
+
11
+ - **The end of a call now travels by push, like its start.** A device
12
+ woken by a VoIP push is ringing on a lock screen with a socket that
13
+ may not be connected, so the broadcast that ends a call reaches
14
+ nothing there. `useVoipCalls` now understands a teardown push
15
+ (`event: "ended"`) and tears the ring down instead of starting one.
16
+ Needs a server that sends them, and an AppDelegate that reports the
17
+ call before ending it — see the sample's `plugins/withVoipPush.js`.
18
+
19
+ - `readPush()` — exported and pure, so what a push MEANS can be tested
20
+ apart from what CallKit does about it. A missing `event` reads as a
21
+ ring, which is what servers predating the field send.
22
+
23
+ ### Fixed
24
+
25
+ - **A ring that arrived by push is now registered with `useCalls`**, so
26
+ every signal that ends a call can match it. Previously only CallKit
27
+ knew about it. See `@poolse/react` 2.19.0.
28
+
29
+ ### Changed
30
+
31
+ - A finished call — declined, busy, no answer — closes itself after a
32
+ short pause instead of waiting behind a Close button.
33
+
34
+ ## [2.18.0] — 2026-08-24
35
+
36
+ ### Fixed
37
+
38
+ - **The device was often never registered for VoIP push, so no call
39
+ ever rang a backgrounded or closed app.** `useVoipCalls` registered
40
+ the PushKit token the instant iOS issued it — which is moments after
41
+ launch, and routinely before the host app has a session. Registering
42
+ is an authenticated call, so it 401'd, and the error was swallowed as
43
+ "offline at launch". No device row meant poolse had nothing to push
44
+ to.
45
+
46
+ The token is now held until a signed-in user exists, and registered
47
+ when both are present — in whichever order they arrive. A failure no
48
+ longer marks the pair as done, so it retries rather than giving up
49
+ until the next launch.
50
+
7
51
  ## [2.17.1] — 2026-08-24
8
52
 
9
53
  ### Fixed
package/dist/index.cjs CHANGED
@@ -5836,6 +5836,10 @@ function format(total) {
5836
5836
  return `${hours > 0 ? `${hours}:` : ""}${mm}:${String(seconds).padStart(2, "0")}`;
5837
5837
  }
5838
5838
  var CHROME_IDLE_MS = 4500;
5839
+ var ENDED_LINGER_MS = 2500;
5840
+ function isOver(phase) {
5841
+ return phase === "declined" || phase === "busy" || phase === "timed-out";
5842
+ }
5839
5843
  function CallScreen({
5840
5844
  userId,
5841
5845
  labelFor,
@@ -5951,6 +5955,11 @@ function CallScreen({
5951
5955
  endedOnEmpty.current = true;
5952
5956
  hangUp();
5953
5957
  }, [alone, calls.phase, hangUp]);
5958
+ react.useEffect(() => {
5959
+ if (!isOver(calls.phase)) return;
5960
+ const timer = setTimeout(calls.reset, ENDED_LINGER_MS);
5961
+ return () => clearTimeout(timer);
5962
+ }, [calls.phase, calls.reset]);
5954
5963
  const micOn = usingSfu ? sfu.micEnabled : !mesh.muted;
5955
5964
  const toggleMic = react.useCallback(() => {
5956
5965
  if (usingSfu) void sfu.toggleMic();
@@ -5964,7 +5973,7 @@ function CallScreen({
5964
5973
  const label = react.useMemo(() => labelFor ?? ((id) => `User ${id.slice(0, 6)}`), [labelFor]);
5965
5974
  const ringingIn = calls.phase === "ringing-in";
5966
5975
  const ringingOut = calls.phase === "ringing-out";
5967
- const ended = calls.phase === "declined" || calls.phase === "busy" || calls.phase === "timed-out";
5976
+ const ended = isOver(calls.phase);
5968
5977
  const visible = calls.phase !== "idle" && !(nativeIncomingUi && ringingIn);
5969
5978
  const duration = useCallDuration(connected);
5970
5979
  const participants = usingSfu ? sfu.participants : [];
@@ -6206,6 +6215,26 @@ function isStalePush(sentAt, now, staleAfterMs) {
6206
6215
  if (age < 0) return false;
6207
6216
  return age > staleAfterMs;
6208
6217
  }
6218
+ function readPush(payload) {
6219
+ const callId = payload.call_id;
6220
+ if (!callId) return { kind: "ignore" };
6221
+ if (payload.event === "ended") return { kind: "teardown", callId };
6222
+ const conversationId = payload.conversation_id;
6223
+ if (!conversationId) return { kind: "ignore" };
6224
+ return {
6225
+ kind: "ring",
6226
+ sentAt: payload.sent_at,
6227
+ call: {
6228
+ callId,
6229
+ conversationId,
6230
+ callerUserId: payload.caller_user_id ?? "",
6231
+ callerName: payload.caller_name ?? "Incoming call",
6232
+ // `has_video` is what older servers sent; `media` supersedes it.
6233
+ // Either saying video means the system UI offers video.
6234
+ media: payload.media === "video" || payload.has_video ? "video" : "audio"
6235
+ }
6236
+ };
6237
+ }
6209
6238
  function useVoipCalls({
6210
6239
  voipPush,
6211
6240
  callKeep,
@@ -6218,6 +6247,9 @@ function useVoipCalls({
6218
6247
  }) {
6219
6248
  const poolse = react$1.usePoolse();
6220
6249
  const shown = react.useRef(/* @__PURE__ */ new Map());
6250
+ const [voipToken, setVoipToken] = react.useState(null);
6251
+ const { me } = react$1.useMe();
6252
+ const registered = react.useRef(null);
6221
6253
  const handlers = react.useRef({ onAnswer, onDecline, calls });
6222
6254
  handlers.current = { onAnswer, onDecline, calls };
6223
6255
  const staleRef = react.useRef(staleAfterMs);
@@ -6257,24 +6289,37 @@ function useVoipCalls({
6257
6289
  void ready.current.then(done, done);
6258
6290
  if (isStalePush(sentAt, Date.now(), staleRef.current)) dismiss.current(call.callId);
6259
6291
  });
6292
+ react.useEffect(() => {
6293
+ const userId = me?.id;
6294
+ if (!voipToken || !userId) return;
6295
+ const pair = `${userId}:${voipToken}`;
6296
+ if (registered.current === pair) return;
6297
+ void poolse.devices.register({
6298
+ token: voipToken,
6299
+ platform: "ios",
6300
+ environment: apnsEnvironment ?? guessApnsEnvironment()
6301
+ }).then(() => {
6302
+ registered.current = pair;
6303
+ }).catch(() => {
6304
+ });
6305
+ }, [poolse, voipToken, me?.id, apnsEnvironment]);
6260
6306
  react.useEffect(() => {
6261
6307
  let cancelled = false;
6262
6308
  const fromPush = (payload) => {
6263
- const callId = payload.call_id;
6264
- const conversationId = payload.conversation_id;
6265
- if (!callId || !conversationId) return;
6266
- present.current(
6267
- {
6268
- callId,
6269
- conversationId,
6270
- callerUserId: payload.caller_user_id ?? "",
6271
- callerName: payload.caller_name ?? "Incoming call",
6272
- // `has_video` is what older servers sent; `media` supersedes
6273
- // it. Either says video, and the system UI offers video.
6274
- media: payload.media === "video" || payload.has_video ? "video" : "audio"
6275
- },
6276
- payload.sent_at
6277
- );
6309
+ const intent = readPush(payload);
6310
+ if (intent.kind === "ignore") return;
6311
+ if (intent.kind === "teardown") {
6312
+ dismiss.current(intent.callId);
6313
+ handlers.current.calls?.dropped(intent.callId);
6314
+ return;
6315
+ }
6316
+ present.current(intent.call, intent.sentAt);
6317
+ handlers.current.calls?.ring({
6318
+ callId: intent.call.callId,
6319
+ conversationId: intent.call.conversationId,
6320
+ callerUserId: intent.call.callerUserId,
6321
+ media: intent.call.media
6322
+ });
6278
6323
  };
6279
6324
  ready.current = callKeep.setup({
6280
6325
  ios: { appName },
@@ -6289,12 +6334,7 @@ function useVoipCalls({
6289
6334
  });
6290
6335
  voipPush.addEventListener("register", ((token) => {
6291
6336
  if (cancelled || !token) return;
6292
- void poolse.devices.register({
6293
- token,
6294
- platform: "ios",
6295
- environment: apnsEnvironment ?? guessApnsEnvironment()
6296
- }).catch(() => {
6297
- });
6337
+ setVoipToken(token);
6298
6338
  }));
6299
6339
  voipPush.addEventListener("notification", ((payload) => {
6300
6340
  fromPush(payload);