fix(mobile): keep pairing discoveries truthful

This commit is contained in:
jjbrunton committed 2026-09-06 12:49:57 +01:00
1 parent a02e9d531b
commit 4415dcf29f
7 files changed
+27 -10

No files matched your search

+6 -3
View File
@@ -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<Text>(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 <GradientBackground><View style={styles.container}><ActivityIndicator size="large" color={colors.primary} /></View></GradientBackground>;
}
if (error) {
return <GradientBackground><View style={styles.container}><Text style={styles.title}>We couldn't load your shared discoveries</Text><Text style={styles.body}>Check your connection and try again.</Text><GlassButton fullWidth onPress={() => { setReady(false); void fetchMatches(true).finally(() => setReady(true)); }} testID="paired-retry">Try again</GlassButton></View></GradientBackground>;
}
return <GradientBackground><View style={styles.container}>
<LinearGradient colors={gradients.primary as [string, string]} style={styles.icon}><Ionicons name="heart" size={42} color={colors.text} /></LinearGradient>
<Text ref={heading} accessibilityRole="header" style={styles.title}>You're paired with {partnerName}</Text>
{newMatchesCount > 0 ? <><Text style={styles.count}>{newMatchesCount} thing{newMatchesCount === 1 ? "" : "s"} you already agree on</Text><Text style={styles.body}>From answers you both gave before now.</Text><GlassButton fullWidth onPress={viewMatches} testID="paired-view-matches">See what you agree on</GlassButton><GlassButton variant="secondary" fullWidth onPress={start} testID="paired-start-session">Start a session</GlassButton></> : <><Text style={styles.body}>Nothing to compare yet. Answer a few questions and anything you agree on shows up here.</Text><GlassButton fullWidth onPress={start} testID="paired-start-session">Answer your first questions</GlassButton></>}
{newMatchesCount > 0 ? <><Text style={styles.count}>{newMatchesCount} shared discover{newMatchesCount === 1 ? "y" : "ies"} ready to explore</Text><Text style={styles.body}>From answers you both gave before now.</Text><GlassButton fullWidth onPress={viewMatches} testID="paired-view-matches">See shared discoveries</GlassButton><GlassButton variant="secondary" fullWidth onPress={start} testID="paired-start-session">Start a session</GlassButton></> : <><Text style={styles.body}>Nothing is ready to compare yet. Answer a few questions and shared discoveries will appear here.</Text><GlassButton fullWidth onPress={start} testID="paired-start-session">Answer your first questions</GlassButton></>}
<TouchableOpacity onPress={dismiss} testID="paired-not-now" style={styles.dismiss}><Text style={styles.dismissText}>Not now</Text></TouchableOpacity>
</View></GradientBackground>;
}
+3 -3
View File
@@ -363,7 +363,7 @@ export default function PairingScreen() {
<Ionicons name="heart" size={40} color={colors.text} />
</LinearGradient>
<Text style={styles.subtitle}>
They unlock your answers when they join
Your answers are ready to compare when you both answer
</Text>
</Animated.View>
@@ -375,7 +375,7 @@ export default function PairingScreen() {
>
<GlassCard variant="elevated">
<Text style={styles.sealedCountText} testID="pairing-sealed-count">
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.
</Text>
</GlassCard>
</Animated.View>
@@ -511,7 +511,7 @@ export default function PairingScreen() {
<Ionicons name="link" size={40} color={colors.text} />
</LinearGradient>
<Text style={styles.subtitle}>
Answer privately. See what you agree on together.
Answer privately. Compare your shared discoveries together.
</Text>
</Animated.View>
@@ -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", () => {
@@ -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');
@@ -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`}
>
<Ionicons name="lock-closed" size={16} color={colors.premium.rose} />
<View style={styles.copy}>
<Text style={styles.title}>{sealedCount} answer{sealedCount === 1 ? "" : "s"} sealed for your partner</Text>
<Text style={styles.text}>You'll both see what you agree on the moment they join.</Text>
<Text style={styles.text}>When you both answer, your shared discoveries are ready to compare.</Text>
</View>
<Text style={styles.cta}>Invite</Text>
</TouchableOpacity>
+1 -1
View File
@@ -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.`;
}
+3
View File
@@ -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<AuthState>((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<AuthState>((set, get) => ({
});
// Clear other stores when user signs out
if (user === null) {
void clearPendingInviteCode();
if (priorUserId) {
void clearPairedUnlockSeen(priorUserId, priorCoupleId);
}