Skip to content

Commit 3cdeb8b

Browse files
fix(mobile): recover from a failed share discard and discard a superseded sheet's share
A rejected discardShare left the share latched as discarded forever, so it sat in the inbox unreachable. Clear the latch and re-run presentation. A newer share arriving while an earlier share's sheet was still open reset the tracked share, so closing that sheet never discarded it and it came back later. Keep tracking the open sheet's share until it closes. Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 937812d commit 3cdeb8b

3 files changed

Lines changed: 59 additions & 36 deletions

File tree

apps/mobile/src/Stack.tsx

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
createNativeStackScreen,
1111
type NativeStackNavigationOptions,
1212
} from "@react-navigation/native-stack";
13-
import { useEffect, useRef } from "react";
13+
import { useEffect, useRef, useState } from "react";
1414
import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native";
1515
import { useResolveClassNames } from "uniwind";
1616

@@ -404,6 +404,7 @@ function RootStackLayout(props: {
404404
const navigation = useNavigation();
405405
const { pendingShare, discardShare } = useIncomingShare();
406406
const sharePresentationRef = useRef(EMPTY_INCOMING_SHARE_PRESENTATION_STATE);
407+
const [sharePresentationRetry, setSharePresentationRetry] = useState(0);
407408
useAgentNotificationNavigation();
408409
// Presents the T3 Connect onboarding sheet after an in-session sign-in.
409410
useConnectOnboardingNavigation();
@@ -418,7 +419,16 @@ function RootStackLayout(props: {
418419
});
419420
sharePresentationRef.current = transition.state;
420421
if (transition.shareIdToDiscard) {
421-
void discardShare(transition.shareIdToDiscard);
422+
const shareId = transition.shareIdToDiscard;
423+
discardShare(shareId).catch((error: unknown) => {
424+
console.warn("[incoming-share] could not discard dismissed share", error);
425+
// Drop the latch so the share is presented again instead of sitting
426+
// in the inbox unreachable until the next foreground refresh.
427+
if (sharePresentationRef.current.discardedShareId === shareId) {
428+
sharePresentationRef.current = EMPTY_INCOMING_SHARE_PRESENTATION_STATE;
429+
setSharePresentationRetry((attempt) => attempt + 1);
430+
}
431+
});
422432
}
423433
if (!transition.shareIdToPresent) {
424434
return;
@@ -427,7 +437,7 @@ function RootStackLayout(props: {
427437
screen: "NewTask",
428438
params: { incomingShareId: transition.shareIdToPresent },
429439
});
430-
}, [discardShare, navigation, pendingShare, props.state]);
440+
}, [discardShare, navigation, pendingShare, props.state, sharePresentationRetry]);
431441
// Full pathname (sheets included) for keyboard-command scoping; the
432442
// workspace layout only reacts to the underlying non-overlay route.
433443
const path = getPathFromState(props.state, navigationPathConfig);

apps/mobile/src/features/sharing/incoming-share-presentation.test.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,35 @@ describe("transitionIncomingSharePresentation", () => {
8484
expect(present("share-1", empty.state).presentedShareId).toBe("share-1");
8585
});
8686

87-
it("presents the next queued share after the previous one is consumed", () => {
87+
it("keeps tracking the open sheet's share when a newer share arrives, then discards it on close", () => {
8888
const state = seen("share-1", present("share-1"));
89-
const consumedWhileOpen = transitionIncomingSharePresentation(state, {
89+
const newerWhileOpen = transitionIncomingSharePresentation(state, {
9090
...sheetWith("share-1"),
9191
pendingShareId: "share-2",
9292
});
93-
expect(consumedWhileOpen.shareIdToPresent).toBeNull();
94-
expect(consumedWhileOpen.shareIdToDiscard).toBeNull();
93+
expect(newerWhileOpen).toEqual({ state, shareIdToPresent: null, shareIdToDiscard: null });
9594

96-
const next = transitionIncomingSharePresentation(consumedWhileOpen.state, {
95+
const next = transitionIncomingSharePresentation(newerWhileOpen.state, {
9796
...closed,
9897
pendingShareId: "share-2",
9998
});
99+
expect(next.shareIdToDiscard).toBe("share-1");
100100
expect(next.shareIdToPresent).toBe("share-2");
101+
expect(next.state).toEqual({
102+
presentedShareId: "share-2",
103+
sheetSeen: false,
104+
discardedShareId: "share-1",
105+
});
106+
});
107+
108+
it("presents a newer share directly when the earlier request never landed", () => {
109+
const requested = present("share-1");
110+
const next = transitionIncomingSharePresentation(requested, {
111+
...closed,
112+
pendingShareId: "share-2",
113+
});
114+
expect(next.shareIdToPresent).toBe("share-2");
115+
expect(next.shareIdToDiscard).toBeNull();
101116
});
102117

103118
it("holds while another route is pushed above the seen sheet", () => {

apps/mobile/src/features/sharing/incoming-share-presentation.ts

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,11 @@ export function incomingShareIdOfSheetRoute(
6363
* user has walked away from it. A share counts as dismissed only after the
6464
* sheet was seen carrying it and then left the stack; until then a missing
6565
* sheet means the navigation never landed (container not ready, another route
66-
* won the race) and the request is simply repeated. Dismissal discards the share so that sharing
67-
* the same content again yields a fresh handoff instead of a silent no-op.
66+
* won the race) and the request is simply repeated. Dismissal discards the
67+
* share so that sharing the same content again yields a fresh handoff instead
68+
* of a silent no-op. The tracked share survives a newer pending one arriving
69+
* while its sheet is still up, so closing that sheet discards it rather than
70+
* letting it resurface after the newer share is handled.
6871
*/
6972
export function transitionIncomingSharePresentation(
7073
state: IncomingSharePresentationState,
@@ -90,36 +93,31 @@ export function transitionIncomingSharePresentation(
9093
return hold;
9194
}
9295

93-
if (state.presentedShareId === pendingShareId) {
94-
if (input.isSheetPresented) {
95-
if (input.sheetShareId === pendingShareId && !state.sheetSeen) {
96-
return { ...hold, state: { ...state, sheetSeen: true } };
97-
}
98-
return hold;
96+
const tracked = state.presentedShareId;
97+
if (input.isSheetPresented) {
98+
if (tracked === null) {
99+
// A sheet nobody here asked for (a manual new task). Wait for it to close.
100+
return { ...hold, state: EMPTY_INCOMING_SHARE_PRESENTATION_STATE };
99101
}
100-
if (state.sheetSeen) {
101-
return {
102-
state: { presentedShareId: null, sheetSeen: false, discardedShareId: pendingShareId },
103-
shareIdToPresent: null,
104-
shareIdToDiscard: pendingShareId,
105-
};
102+
if (input.sheetShareId === tracked && !state.sheetSeen) {
103+
return { ...hold, state: { ...state, sheetSeen: true } };
106104
}
107-
return { ...hold, shareIdToPresent: pendingShareId };
108-
}
109-
110-
if (input.isSheetPresented) {
111-
// Someone else owns the sheet (a manual new task, or the sheet of a share
112-
// that was just consumed). Wait for it to close.
113-
return {
114-
state: { presentedShareId: null, sheetSeen: false, discardedShareId: null },
115-
shareIdToPresent: null,
116-
shareIdToDiscard: null,
117-
};
105+
return hold;
118106
}
119107

108+
// The sheet is gone. A tracked share that was seen is dismissed; anything
109+
// else is a request that never landed and is repeated. A consumed share can
110+
// also land here (its sheet closed onto the thread); discarding it again is
111+
// a no-op since the inbox no longer has it.
112+
const shareIdToDiscard = tracked !== null && state.sheetSeen ? tracked : null;
113+
const shareIdToPresent = shareIdToDiscard === pendingShareId ? null : pendingShareId;
120114
return {
121-
state: { presentedShareId: pendingShareId, sheetSeen: false, discardedShareId: null },
122-
shareIdToPresent: pendingShareId,
123-
shareIdToDiscard: null,
115+
state: {
116+
presentedShareId: shareIdToPresent,
117+
sheetSeen: false,
118+
discardedShareId: shareIdToDiscard,
119+
},
120+
shareIdToPresent,
121+
shareIdToDiscard,
124122
};
125123
}

0 commit comments

Comments
 (0)