mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
merge: correct mobile invite disclosure
This commit is contained in:
commit
a1b62c3b47
7 files changed
+27
-10
No files matched your search
@@ -14,7 +14,7 @@ const bucket = (n: number) => n === 0 ? "0" : n <= 2 ? "1-2" : n <= 5 ? "3-5" :
|
|||||||
|
|
||||||
export default function PairedScreen() {
|
export default function PairedScreen() {
|
||||||
const { user, couple, partner } = useAuthStore();
|
const { user, couple, partner } = useAuthStore();
|
||||||
const { newMatchesCount, fetchMatches } = useMatchStore();
|
const { newMatchesCount, fetchMatches, error } = useMatchStore();
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const { role: rawRole } = useLocalSearchParams<{ role?: "inviter" | "joiner" }>();
|
const { role: rawRole } = useLocalSearchParams<{ role?: "inviter" | "joiner" }>();
|
||||||
const heading = useRef<Text>(null);
|
const heading = useRef<Text>(null);
|
||||||
@@ -24,7 +24,7 @@ export default function PairedScreen() {
|
|||||||
void fetchMatches(true).finally(() => setReady(true));
|
void fetchMatches(true).finally(() => setReady(true));
|
||||||
}, [fetchMatches]);
|
}, [fetchMatches]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready || !user || !couple) return;
|
if (!ready || error || !user || !couple) return;
|
||||||
void markPairedUnlockSeen(user.id, couple.id);
|
void markPairedUnlockSeen(user.id, couple.id);
|
||||||
Events.pairingUnlockViewed(role, bucket(newMatchesCount));
|
Events.pairingUnlockViewed(role, bucket(newMatchesCount));
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -40,10 +40,13 @@ export default function PairedScreen() {
|
|||||||
if (!ready) {
|
if (!ready) {
|
||||||
return <GradientBackground><View style={styles.container}><ActivityIndicator size="large" color={colors.primary} /></View></GradientBackground>;
|
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}>
|
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>
|
<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>
|
<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>
|
<TouchableOpacity onPress={dismiss} testID="paired-not-now" style={styles.dismiss}><Text style={styles.dismissText}>Not now</Text></TouchableOpacity>
|
||||||
</View></GradientBackground>;
|
</View></GradientBackground>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export default function PairingScreen() {
|
|||||||
<Ionicons name="heart" size={40} color={colors.text} />
|
<Ionicons name="heart" size={40} color={colors.text} />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
They unlock your answers when they join
|
Your answers are ready to compare when you both answer
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
@@ -375,7 +375,7 @@ export default function PairingScreen() {
|
|||||||
>
|
>
|
||||||
<GlassCard variant="elevated">
|
<GlassCard variant="elevated">
|
||||||
<Text style={styles.sealedCountText} testID="pairing-sealed-count">
|
<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>
|
</Text>
|
||||||
</GlassCard>
|
</GlassCard>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -511,7 +511,7 @@ export default function PairingScreen() {
|
|||||||
<Ionicons name="link" size={40} color={colors.text} />
|
<Ionicons name="link" size={40} color={colors.text} />
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
<Text style={styles.subtitle}>
|
<Text style={styles.subtitle}>
|
||||||
Answer privately. See what you agree on together.
|
Answer privately. Compare your shared discoveries together.
|
||||||
</Text>
|
</Text>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ describe("buildInviteShareMessage", () => {
|
|||||||
const message = buildInviteShareMessage("AB12CD34", 3);
|
const message = buildInviteShareMessage("AB12CD34", 3);
|
||||||
expect(message).toContain("https://sauci.app/join/AB12CD34");
|
expect(message).toContain("https://sauci.app/join/AB12CD34");
|
||||||
expect(message).toContain("enter code AB12CD34 in Sauci");
|
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", () => {
|
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 { usePacksStore } from "@/store/packsStore";
|
||||||
import { useSubscriptionStore } from "@/store/subscriptionStore";
|
import { useSubscriptionStore } from "@/store/subscriptionStore";
|
||||||
import type { Profile } from "@/types";
|
import type { Profile } from "@/types";
|
||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
|
||||||
const profile = {
|
const profile = {
|
||||||
id: "me",
|
id: "me",
|
||||||
@@ -259,6 +260,7 @@ describe("authStore", () => {
|
|||||||
|
|
||||||
it("clears sealed answers during an explicit sign-out", async () => {
|
it("clears sealed answers during an explicit sign-out", async () => {
|
||||||
useAuthStore.setState({ user: profile, sealedCount: 3, isAuthenticated: true } as any);
|
useAuthStore.setState({ user: profile, sealedCount: 3, isAuthenticated: true } as any);
|
||||||
|
await AsyncStorage.setItem("pending_invite_code", "ABCD1234");
|
||||||
|
|
||||||
await useAuthStore.getState().signOut();
|
await useAuthStore.getState().signOut();
|
||||||
|
|
||||||
@@ -267,6 +269,7 @@ describe("authStore", () => {
|
|||||||
sealedCount: 0,
|
sealedCount: 0,
|
||||||
isAuthenticated: false,
|
isAuthenticated: false,
|
||||||
});
|
});
|
||||||
|
await expect(AsyncStorage.getItem("pending_invite_code")).resolves.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clears sealed answers when the auth listener clears the user", () => {
|
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', () => {
|
it('keeps a pending Apple name scoped to its original subject', () => {
|
||||||
useAuthStore.getState().setPendingAppleDisplayName({ userId: 'apple-user', name: 'Ada Lovelace' });
|
useAuthStore.getState().setPendingAppleDisplayName({ userId: 'apple-user', name: 'Ada Lovelace' });
|
||||||
useAuthStore.getState().clearPendingAppleDisplayName('other-user');
|
useAuthStore.getState().clearPendingAppleDisplayName('other-user');
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ export const SwipeSealedAnswersBanner = ({ sealedCount, onInvitePress }: SwipeSe
|
|||||||
onPress={onInvitePress}
|
onPress={onInvitePress}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
testID="swipe-sealed-answers-banner"
|
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} />
|
<Ionicons name="lock-closed" size={16} color={colors.premium.rose} />
|
||||||
<View style={styles.copy}>
|
<View style={styles.copy}>
|
||||||
<Text style={styles.title}>{sealedCount} answer{sealedCount === 1 ? "" : "s"} sealed for your partner</Text>
|
<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>
|
</View>
|
||||||
<Text style={styles.cta}>Invite</Text>
|
<Text style={styles.cta}>Invite</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export function buildInviteShareMessage(inviteCode: string, sealedCount: number)
|
|||||||
const link = `https://sauci.app/join/${inviteCode}`;
|
const link = `https://sauci.app/join/${inviteCode}`;
|
||||||
if (sealedCount > 0) {
|
if (sealedCount > 0) {
|
||||||
const noun = sealedCount === 1 ? "question" : "questions";
|
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.`;
|
return `Join me on Sauci! ${link} If the link does not open the app, enter code ${inviteCode} in Sauci.`;
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import { profileSettingsApi } from "../lib/profileSettingsApi";
|
|||||||
import { syncTimezone } from "../lib/reportedTimezone";
|
import { syncTimezone } from "../lib/reportedTimezone";
|
||||||
import { Events } from "../lib/analytics";
|
import { Events } from "../lib/analytics";
|
||||||
import { clearPairedUnlockSeen } from "../lib/pairedUnlockSeen";
|
import { clearPairedUnlockSeen } from "../lib/pairedUnlockSeen";
|
||||||
|
import { clearPendingInviteCode } from "../lib/pendingInviteCode";
|
||||||
import type { Profile, Couple } from "@/types";
|
import type { Profile, Couple } from "@/types";
|
||||||
|
|
||||||
export interface AuthSessionSnapshot {
|
export interface AuthSessionSnapshot {
|
||||||
@@ -209,6 +210,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
if (unlockUserId) {
|
if (unlockUserId) {
|
||||||
await clearPairedUnlockSeen(unlockUserId, unlockCoupleId);
|
await clearPairedUnlockSeen(unlockUserId, unlockCoupleId);
|
||||||
}
|
}
|
||||||
|
await clearPendingInviteCode();
|
||||||
|
|
||||||
// Clear local state FIRST to ensure UI updates even if Supabase call fails
|
// Clear local state FIRST to ensure UI updates even if Supabase call fails
|
||||||
set({
|
set({
|
||||||
@@ -257,6 +259,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
// Clear other stores when user signs out
|
// Clear other stores when user signs out
|
||||||
if (user === null) {
|
if (user === null) {
|
||||||
|
void clearPendingInviteCode();
|
||||||
if (priorUserId) {
|
if (priorUserId) {
|
||||||
void clearPairedUnlockSeen(priorUserId, priorCoupleId);
|
void clearPairedUnlockSeen(priorUserId, priorCoupleId);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user