fix(mobile): sign in race condition

Closes #217
This commit is contained in:
Arjun Patel
2026-05-26 10:00:52 -07:00
parent d262f734f0
commit ec01ca77e4
7 changed files with 119 additions and 108 deletions
+16 -24
View File
@@ -1,5 +1,4 @@
import { appConfig } from "@/config/env";
import { useSessionStore } from "@/stores/session-store";
import { ApiError } from "@/lib/errors";
import type { z } from "zod";
import {
@@ -28,17 +27,20 @@ import type {
SignInRequest,
} from "./types";
interface ApiClientConfig {
baseUrl: string;
getToken: () => string | null;
onUnauthorized: () => void;
}
/**
* HTTP transport for Orion. Holds the bearer token as private state — the auth
* store pushes it in via {@link setToken} on sign-in / restore and clears it
* on sign-out. The client itself has no opinion about what a 401 means; it
* just throws, and the query-client onError handler is the single place that
* turns a 401 into a session invalidation.
*/
class ApiClient {
private config: ApiClientConfig;
private token: string | null = null;
constructor(config: ApiClientConfig) {
this.config = config;
constructor(private readonly baseUrl: string) {}
setToken(token: string | null): void {
this.token = token;
}
private async fetch(
@@ -52,19 +54,17 @@ class ApiClient {
headers["Content-Type"] = "application/json";
}
const token = this.config.getToken();
if (token) {
headers["Authorization"] = `Bearer ${token}`;
if (this.token) {
headers["Authorization"] = `Bearer ${this.token}`;
}
const response = await fetch(`${this.config.baseUrl}${path}`, {
const response = await fetch(`${this.baseUrl}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (response.status === 401) {
this.config.onUnauthorized();
throw new ApiError(401, "Unauthorized");
}
@@ -273,12 +273,4 @@ class ApiClient {
}
}
export const apiClient = new ApiClient({
baseUrl: appConfig.orionUrl,
getToken: () => useSessionStore.getState().token,
// SecureStore writes are async; we fire-and-forget so the throwing
// request doesn't have to wait for persistence to finish.
onUnauthorized: () => {
void useSessionStore.getState().clearToken();
},
});
export const apiClient = new ApiClient(appConfig.orionUrl);