-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtokenStore.ts
More file actions
94 lines (83 loc) · 2.58 KB
/
tokenStore.ts
File metadata and controls
94 lines (83 loc) · 2.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
import {makeVar} from '@apollo/client';
import {useReactiveVar} from '@apollo/client/react';
import EncryptedStorage from 'react-native-encrypted-storage';
// Persistent auth store backed by EncryptedSharedPreferences (Android) /
// Keychain (iOS) via react-native-encrypted-storage. The reactive var
// drives UI; the storage layer is the source of truth across app launches.
const STORAGE_KEY = 'culpeos.auth';
// Refresh the access token this many ms before its expiry to avoid the
// round-trip on the next request. Must be < access-token lifetime (15min).
export const REFRESH_LEAD_MS = 60_000;
export type AuthUser = {
id: string;
email: string;
locale?: string | null;
};
export type AuthState = {
accessToken: string;
refreshToken: string;
/** Absolute access-token expiration, ms since epoch. */
expiresAtMs: number;
user: AuthUser;
};
const authVar = makeVar<AuthState | null>(null);
const hydratedVar = makeVar<boolean>(false);
function persist(state: AuthState): void {
EncryptedStorage.setItem(STORAGE_KEY, JSON.stringify(state)).catch(err => {
console.warn('[auth] failed to persist token', err);
});
}
function remove(): void {
EncryptedStorage.removeItem(STORAGE_KEY).catch(err => {
if (__DEV__) console.debug('[auth] removeItem:', err);
});
}
export const tokenStore = {
get(): AuthState | null {
return authVar();
},
set(state: AuthState): void {
authVar(state);
persist(state);
},
clear(): void {
authVar(null);
remove();
},
/** True if the access token is missing or within the proactive-refresh window. */
isAccessTokenExpiringSoon(now: number = Date.now()): boolean {
const state = authVar();
if (!state) return false;
return state.expiresAtMs - now <= REFRESH_LEAD_MS;
},
/**
* Load any persisted tokens into memory. Call once at app startup before
* deciding whether to render the login screen.
*/
async hydrate(): Promise<void> {
try {
const raw = await EncryptedStorage.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as Partial<AuthState>;
if (
parsed?.accessToken &&
parsed?.refreshToken &&
typeof parsed.expiresAtMs === 'number' &&
parsed.user?.id
) {
authVar(parsed as AuthState);
}
}
} catch (err) {
console.warn('[auth] failed to hydrate token', err);
} finally {
hydratedVar(true);
}
},
};
export function useAuth(): AuthState | null {
return useReactiveVar(authVar);
}
export function useAuthHydrated(): boolean {
return useReactiveVar(hydratedVar);
}