From 4415dcf29f0ee21116b9fdc9473d4bfc8d27fb3b Mon Sep 17 00:00:00 2001 From: jjbrunton Date: Sun, 6 Sep 2026 12:49:57 +0100 Subject: [PATCH] fix(mobile): keep pairing discoveries truthful --- apps/mobile/app/(app)/paired.tsx | 9 ++++++--- apps/mobile/app/(app)/pairing.tsx | 6 +++--- apps/mobile/src/__tests__/lib/inviteShareCopy.test.ts | 2 +- apps/mobile/src/__tests__/stores/authStore.test.ts | 11 +++++++++++ .../swipe/components/SwipeSealedAnswersBanner.tsx | 4 ++-- apps/mobile/src/lib/inviteShareCopy.ts | 2 +- apps/mobile/src/store/authStore.ts | 3 +++ 7 files changed, 27 insertions(+), 10 deletions(-) diff --git a/apps/mobile/app/(app)/paired.tsx b/apps/mobile/app/(app)/paired.tsx index 494dde7..09e8d4f 100644 --- a/apps/mobile/app/(app)/paired.tsx +++ b/apps/mobile/app/(app)/paired.tsx @@ -14,7 +14,7 @@ const bucket = (n: number) => n === 0 ? "0" : n <= 2 ? "1-2" : n <= 5 ? "3-5" : export default function PairedScreen() { const { user, couple, partner } = useAuthStore(); - const { newMatchesCount, fetchMatches } = useMatchStore(); + const { newMatchesCount, fetchMatches, error } = useMatchStore(); const [ready, setReady] = useState(false); const { role: rawRole } = useLocalSearchParams<{ role?: "inviter" | "joiner" }>(); const heading = useRef(null); @@ -24,7 +24,7 @@ export default function PairedScreen() { void fetchMatches(true).finally(() => setReady(true)); }, [fetchMatches]); useEffect(() => { - if (!ready || !user || !couple) return; + if (!ready || error || !user || !couple) return; void markPairedUnlockSeen(user.id, couple.id); Events.pairingUnlockViewed(role, bucket(newMatchesCount)); const timer = setTimeout(() => { @@ -40,10 +40,13 @@ export default function PairedScreen() { if (!ready) { return ; } + if (error) { + return We couldn't load your shared discoveriesCheck your connection and try again. { setReady(false); void fetchMatches(true).finally(() => setReady(true)); }} testID="paired-retry">Try again; + } return You're paired with {partnerName} - {newMatchesCount > 0 ? <>{newMatchesCount} thing{newMatchesCount === 1 ? "" : "s"} you already agree onFrom answers you both gave before now.See what you agree onStart a session : <>Nothing to compare yet. Answer a few questions and anything you agree on shows up here.Answer your first questions} + {newMatchesCount > 0 ? <>{newMatchesCount} shared discover{newMatchesCount === 1 ? "y" : "ies"} ready to exploreFrom answers you both gave before now.See shared discoveriesStart a session : <>Nothing is ready to compare yet. Answer a few questions and shared discoveries will appear here.Answer your first questions} Not now ; } diff --git a/apps/mobile/app/(app)/pairing.tsx b/apps/mobile/app/(app)/pairing.tsx index de58712..3e8fb8e 100755 --- a/apps/mobile/app/(app)/pairing.tsx +++ b/apps/mobile/app/(app)/pairing.tsx @@ -363,7 +363,7 @@ export default function PairingScreen() { - They unlock your answers when they join + Your answers are ready to compare when you both answer @@ -375,7 +375,7 @@ export default function PairingScreen() { > - You've answered at least {sealedCount} question{sealedCount === 1 ? "" : "s"} about you two. As soon as they join, everything you both agree on appears for both of you. + You've answered at least {sealedCount} question{sealedCount === 1 ? "" : "s"} about you two. When you both answer, shared discoveries are visible to you both. @@ -511,7 +511,7 @@ export default function PairingScreen() { - Answer privately. See what you agree on together. + Answer privately. Compare your shared discoveries together. diff --git a/apps/mobile/src/__tests__/lib/inviteShareCopy.test.ts b/apps/mobile/src/__tests__/lib/inviteShareCopy.test.ts index 3d088ff..aef1fd3 100644 --- a/apps/mobile/src/__tests__/lib/inviteShareCopy.test.ts +++ b/apps/mobile/src/__tests__/lib/inviteShareCopy.test.ts @@ -5,7 +5,7 @@ describe("buildInviteShareMessage", () => { const message = buildInviteShareMessage("AB12CD34", 3); expect(message).toContain("https://sauci.app/join/AB12CD34"); expect(message).toContain("enter code AB12CD34 in Sauci"); - expect(message).toContain("we'll both see what we agree on"); + expect(message).toContain("compare our answers together"); }); it("does not claim a sealed outcome before answers exist", () => { diff --git a/apps/mobile/src/__tests__/stores/authStore.test.ts b/apps/mobile/src/__tests__/stores/authStore.test.ts index a50caa8..4f7f3d3 100644 --- a/apps/mobile/src/__tests__/stores/authStore.test.ts +++ b/apps/mobile/src/__tests__/stores/authStore.test.ts @@ -8,6 +8,7 @@ import { useMessageStore } from "@/store/messageStore"; import { usePacksStore } from "@/store/packsStore"; import { useSubscriptionStore } from "@/store/subscriptionStore"; import type { Profile } from "@/types"; +import AsyncStorage from "@react-native-async-storage/async-storage"; const profile = { id: "me", @@ -259,6 +260,7 @@ describe("authStore", () => { it("clears sealed answers during an explicit sign-out", async () => { useAuthStore.setState({ user: profile, sealedCount: 3, isAuthenticated: true } as any); + await AsyncStorage.setItem("pending_invite_code", "ABCD1234"); await useAuthStore.getState().signOut(); @@ -267,6 +269,7 @@ describe("authStore", () => { sealedCount: 0, isAuthenticated: false, }); + await expect(AsyncStorage.getItem("pending_invite_code")).resolves.toBeNull(); }); it("clears sealed answers when the auth listener clears the user", () => { @@ -281,6 +284,14 @@ describe("authStore", () => { }); }); + it("clears a pending invite when the auth listener clears the user", async () => { + await AsyncStorage.setItem("pending_invite_code", "ABCD1234"); + useAuthStore.setState({ user: profile, isAuthenticated: true } as any); + useAuthStore.getState().setUser(null); + await new Promise((resolve) => setTimeout(resolve, 0)); + await expect(AsyncStorage.getItem("pending_invite_code")).resolves.toBeNull(); + }); + it('keeps a pending Apple name scoped to its original subject', () => { useAuthStore.getState().setPendingAppleDisplayName({ userId: 'apple-user', name: 'Ada Lovelace' }); useAuthStore.getState().clearPendingAppleDisplayName('other-user'); diff --git a/apps/mobile/src/features/swipe/components/SwipeSealedAnswersBanner.tsx b/apps/mobile/src/features/swipe/components/SwipeSealedAnswersBanner.tsx index 9327c46..16fd9ac 100644 --- a/apps/mobile/src/features/swipe/components/SwipeSealedAnswersBanner.tsx +++ b/apps/mobile/src/features/swipe/components/SwipeSealedAnswersBanner.tsx @@ -21,12 +21,12 @@ export const SwipeSealedAnswersBanner = ({ sealedCount, onInvitePress }: SwipeSe onPress={onInvitePress} activeOpacity={0.8} testID="swipe-sealed-answers-banner" - accessibilityLabel={`Invite your partner to unlock ${sealedCount} sealed answers`} + accessibilityLabel={`Invite your partner to compare ${sealedCount} sealed answers`} > {sealedCount} answer{sealedCount === 1 ? "" : "s"} sealed for your partner - You'll both see what you agree on the moment they join. + When you both answer, your shared discoveries are ready to compare. Invite diff --git a/apps/mobile/src/lib/inviteShareCopy.ts b/apps/mobile/src/lib/inviteShareCopy.ts index 0dea9a8..03c3008 100644 --- a/apps/mobile/src/lib/inviteShareCopy.ts +++ b/apps/mobile/src/lib/inviteShareCopy.ts @@ -8,7 +8,7 @@ export function buildInviteShareMessage(inviteCode: string, sealedCount: number) const link = `https://sauci.app/join/${inviteCode}`; if (sealedCount > 0) { const noun = sealedCount === 1 ? "question" : "questions"; - return `Join me on Sauci! I've answered ${sealedCount} ${noun} about us. Join and we'll both see what we agree on. ${link} If the link does not open the app, enter code ${inviteCode} in Sauci.`; + return `Join me on Sauci! I've answered ${sealedCount} ${noun} about us. Join to compare our answers together. ${link} If the link does not open the app, enter code ${inviteCode} in Sauci.`; } return `Join me on Sauci! ${link} If the link does not open the app, enter code ${inviteCode} in Sauci.`; } diff --git a/apps/mobile/src/store/authStore.ts b/apps/mobile/src/store/authStore.ts index c79d820..cfff194 100644 --- a/apps/mobile/src/store/authStore.ts +++ b/apps/mobile/src/store/authStore.ts @@ -6,6 +6,7 @@ import { profileSettingsApi } from "../lib/profileSettingsApi"; import { syncTimezone } from "../lib/reportedTimezone"; import { Events } from "../lib/analytics"; import { clearPairedUnlockSeen } from "../lib/pairedUnlockSeen"; +import { clearPendingInviteCode } from "../lib/pendingInviteCode"; import type { Profile, Couple } from "@/types"; export interface AuthSessionSnapshot { @@ -209,6 +210,7 @@ export const useAuthStore = create((set, get) => ({ if (unlockUserId) { await clearPairedUnlockSeen(unlockUserId, unlockCoupleId); } + await clearPendingInviteCode(); // Clear local state FIRST to ensure UI updates even if Supabase call fails set({ @@ -257,6 +259,7 @@ export const useAuthStore = create((set, get) => ({ }); // Clear other stores when user signs out if (user === null) { + void clearPendingInviteCode(); if (priorUserId) { void clearPairedUnlockSeen(priorUserId, priorCoupleId); }