mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 18:19:43 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cff928ce5e |
No files matched your search
@@ -69,10 +69,12 @@ export default function MatchesScreen() {
|
|||||||
checkNudgeCooldown,
|
checkNudgeCooldown,
|
||||||
} = useMatchStore();
|
} = useMatchStore();
|
||||||
const { user, couple, partner } = useAuthStore();
|
const { user, couple, partner } = useAuthStore();
|
||||||
|
const userId = user?.id;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { width } = useWindowDimensions();
|
const { width } = useWindowDimensions();
|
||||||
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
const isWideScreen = width > MAX_CONTENT_WIDTH;
|
||||||
const [showTutorial, setShowTutorial] = useState(false);
|
const [showTutorial, setShowTutorial] = useState(false);
|
||||||
|
const tutorialDismissedRef = useRef(false);
|
||||||
const [nudgeFeedback, setNudgeFeedback] = useState<string | null>(null);
|
const [nudgeFeedback, setNudgeFeedback] = useState<string | null>(null);
|
||||||
const [headerHeight, setHeaderHeight] = useState(130);
|
const [headerHeight, setHeaderHeight] = useState(130);
|
||||||
|
|
||||||
@@ -104,21 +106,30 @@ export default function MatchesScreen() {
|
|||||||
// Check if tutorial should be shown when screen is focused or matches change
|
// Check if tutorial should be shown when screen is focused or matches change
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
let isActive = true;
|
||||||
|
|
||||||
const checkTutorial = async () => {
|
const checkTutorial = async () => {
|
||||||
if (matches.length > 0) {
|
if (matches.length > 0 && userId) {
|
||||||
const seen = await hasSeenMatchesTutorial();
|
const seen = await hasSeenMatchesTutorial(userId);
|
||||||
if (!seen) {
|
if (isActive && !tutorialDismissedRef.current && !seen) {
|
||||||
setShowTutorial(true);
|
setShowTutorial(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
checkTutorial();
|
void checkTutorial();
|
||||||
}, [matches.length])
|
|
||||||
|
return () => {
|
||||||
|
isActive = false;
|
||||||
|
};
|
||||||
|
}, [matches.length, userId])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleTutorialComplete = async () => {
|
const handleTutorialComplete = async () => {
|
||||||
await markMatchesTutorialSeen();
|
if (!userId) return;
|
||||||
|
|
||||||
|
tutorialDismissedRef.current = true;
|
||||||
setShowTutorial(false);
|
setShowTutorial(false);
|
||||||
|
await markMatchesTutorialSeen(userId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load whichever view is showing, once. Returning to this tab reuses what is
|
// Load whichever view is showing, once. Returning to this tab reuses what is
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
|
import * as SecureStore from "expo-secure-store";
|
||||||
|
import {
|
||||||
|
hasSeenMatchesTutorial,
|
||||||
|
markMatchesTutorialSeen,
|
||||||
|
resetMatchesTutorial,
|
||||||
|
} from "@/lib/matchesTutorialSeen";
|
||||||
|
|
||||||
|
jest.mock("expo-secure-store", () => ({
|
||||||
|
getItemAsync: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("matchesTutorialSeen", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await AsyncStorage.clear();
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockReset();
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockResolvedValue(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists a dismissal for the signed-in account", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not dismiss the tutorial for another account on the same device", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-2")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("migrates an existing legacy dismissal into durable account storage", async () => {
|
||||||
|
jest.mocked(SecureStore.getItemAsync).mockResolvedValue("true");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(true);
|
||||||
|
await expect(AsyncStorage.getItem("matches_tutorial_seen_user-1")).resolves.toBe("true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lets the development reset show the tutorial for the current account again", async () => {
|
||||||
|
await markMatchesTutorialSeen("user-1");
|
||||||
|
await resetMatchesTutorial("user-1");
|
||||||
|
|
||||||
|
await expect(hasSeenMatchesTutorial("user-1")).resolves.toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -223,7 +223,8 @@ export function ProfileScreen() {
|
|||||||
label="Reset Matches Tutorial"
|
label="Reset Matches Tutorial"
|
||||||
description="Show the tutorial again"
|
description="Show the tutorial again"
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
await resetMatchesTutorial();
|
if (!user?.id) return;
|
||||||
|
await resetMatchesTutorial(user.id);
|
||||||
Alert.alert("Success", "Matches tutorial has been reset.");
|
Alert.alert("Success", "Matches tutorial has been reset.");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,51 +1,70 @@
|
|||||||
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||||
import * as SecureStore from "expo-secure-store";
|
import * as SecureStore from "expo-secure-store";
|
||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
|
||||||
const STORAGE_KEY = "matches_tutorial_seen";
|
const STORAGE_KEY_PREFIX = "matches_tutorial_seen";
|
||||||
|
const LEGACY_STORAGE_KEY = "matches_tutorial_seen";
|
||||||
|
|
||||||
async function getStorage(): Promise<boolean> {
|
const buildKey = (userId: string): string => `${STORAGE_KEY_PREFIX}_${userId}`;
|
||||||
|
|
||||||
|
async function getStorage(userId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
let data: string | null = null;
|
const key = buildKey(userId);
|
||||||
|
|
||||||
if (Platform.OS === "web") {
|
if (Platform.OS === "web") {
|
||||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
||||||
data = window.localStorage.getItem(STORAGE_KEY);
|
return window.localStorage.getItem(key) === "true";
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
data = await SecureStore.getItemAsync(STORAGE_KEY);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return data === "true";
|
const storedValue = await AsyncStorage.getItem(key);
|
||||||
|
if (storedValue !== null) {
|
||||||
|
return storedValue === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep dismissals made before the account-scoped AsyncStorage key was
|
||||||
|
// introduced. Future reads use the durable app storage key above.
|
||||||
|
const legacyValue = await SecureStore.getItemAsync(LEGACY_STORAGE_KEY);
|
||||||
|
if (legacyValue === "true") {
|
||||||
|
await AsyncStorage.setItem(key, "true");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading matches tutorial state:", error);
|
console.error("Error reading matches tutorial state:", error);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setStorage(seen: boolean): Promise<void> {
|
async function setStorage(userId: string, seen: boolean): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const data = seen ? "true" : "false";
|
const data = seen ? "true" : "false";
|
||||||
|
const key = buildKey(userId);
|
||||||
|
|
||||||
if (Platform.OS === "web") {
|
if (Platform.OS === "web") {
|
||||||
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
if (typeof window !== "undefined" && typeof window.localStorage !== "undefined") {
|
||||||
window.localStorage.setItem(STORAGE_KEY, data);
|
window.localStorage.setItem(key, data);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await SecureStore.setItemAsync(STORAGE_KEY, data);
|
await AsyncStorage.setItem(key, data);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving matches tutorial state:", error);
|
console.error("Error saving matches tutorial state:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function hasSeenMatchesTutorial(): Promise<boolean> {
|
/** The Matches tutorial is shown at most once per signed-in account. */
|
||||||
return getStorage();
|
export async function hasSeenMatchesTutorial(userId: string): Promise<boolean> {
|
||||||
|
return getStorage(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markMatchesTutorialSeen(): Promise<void> {
|
export async function markMatchesTutorialSeen(userId: string): Promise<void> {
|
||||||
await setStorage(true);
|
await setStorage(userId, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resetMatchesTutorial(): Promise<void> {
|
export async function resetMatchesTutorial(userId: string): Promise<void> {
|
||||||
await setStorage(false);
|
await setStorage(userId, false);
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user