Review

Migrate session tokens off AsyncStorage

CourtReserve still kept refresh tokens in plain AsyncStorage after the security review. Routes session IO through SecureStore, and on first read after upgrade performs a one-shot lift: copy any legacy AsyncStorage value into SecureStore, delete the old key, then never touch AsyncStorage for that token again.

ExpoTier 3exposecure-storeauthmigration

Click a line to flag it, pick one or more labels, then submit. If the change looks correct, approve it.

src/auth/sessionStore.ts+22-11
11import AsyncStorage from "@react-native-async-storage/async-storage";
2+import * as SecureStore from "expo-secure-store";
23
34const REFRESH_KEY = "courtreserve.refresh";
45
5-export async function getRefreshToken(): Promise<string | null> {
6- return AsyncStorage.getItem(REFRESH_KEY);
7-}
8-
9-export async function setRefreshToken(token: string): Promise<void> {
10- await AsyncStorage.setItem(REFRESH_KEY, token);
11-}
12-
13-export async function clearRefreshToken(): Promise<void> {
14- await AsyncStorage.removeItem(REFRESH_KEY);
15-}
6+export async function getRefreshToken(): Promise<string | null> {
7+ const secure = await SecureStore.getItemAsync(REFRESH_KEY);
8+ if (secure != null) return secure;
9+
10+ const legacy = await AsyncStorage.getItem(REFRESH_KEY);
11+ if (legacy == null) return null;
12+
13+ await SecureStore.setItemAsync(REFRESH_KEY, legacy);
14+ await AsyncStorage.removeItem(REFRESH_KEY);
15+ return legacy;
16+}
17+
18+export async function setRefreshToken(token: string): Promise<void> {
19+ await SecureStore.setItemAsync(REFRESH_KEY, token);
20+ await AsyncStorage.removeItem(REFRESH_KEY);
21+}
22+
23+export async function clearRefreshToken(): Promise<void> {
24+ await SecureStore.deleteItemAsync(REFRESH_KEY);
25+ await AsyncStorage.removeItem(REFRESH_KEY);
26+}