mobile v0.1 with deployment for ios (#191)
* stage 1: project init * stage 2: skeleton with navigation * step 2.5: streams list * step 4: stream playback experience * step 5-6: compose experience * fix: broken record * transcode media particles to mp4 * build: reproducible go generate * build: rename skaffold module for particle processor worker * infra: increase particle processor worker resources Was dealing with OOM errors * tweaks to mobile * log transcode work * view on desktop placeholder * tweak padding * cap video resolution to save on memory * infra: bump memory limits as insurance * ux improvements * update bundle id for mobile * config for mobile
This commit was merged in pull request #191.
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import { create } from "zustand";
|
||||
import {
|
||||
signInWithCustomToken,
|
||||
signOut as firebaseSignOut,
|
||||
} from "firebase/auth";
|
||||
import { apiClient } from "@/api/client";
|
||||
import type { Human } from "@/api/types";
|
||||
import { firebaseAuth } from "@/firebase";
|
||||
import { logError, ApiError } from "@/lib/errors";
|
||||
import { hydrateSession, useSessionStore } from "./session-store";
|
||||
|
||||
async function signInToFirebase(): Promise<void> {
|
||||
try {
|
||||
const { token } = await apiClient.getFirebaseToken();
|
||||
await signInWithCustomToken(firebaseAuth, token);
|
||||
} catch (err) {
|
||||
// Firestore subscriptions will fail until the next successful sign-in; the
|
||||
// rest of the app keeps working against Orion. Sentry catches the failure.
|
||||
logError(err, { scope: "auth.firebase" });
|
||||
}
|
||||
}
|
||||
|
||||
type AuthStatus = "idle" | "restoring" | "unauthenticated" | "authenticated";
|
||||
|
||||
interface AuthState {
|
||||
status: AuthStatus;
|
||||
user: Human | null;
|
||||
isRequestingCode: boolean;
|
||||
isSigningIn: boolean;
|
||||
isSigningOut: boolean;
|
||||
error: string | null;
|
||||
restoreSession: () => Promise<void>;
|
||||
requestCode: (email: string) => Promise<void>;
|
||||
signIn: (email: string, code: string) => Promise<void>;
|
||||
signOut: () => Promise<void>;
|
||||
clearError: () => void;
|
||||
}
|
||||
|
||||
export const useAuthStore = create<AuthState>((set) => ({
|
||||
status: "idle",
|
||||
user: null,
|
||||
isRequestingCode: false,
|
||||
isSigningIn: false,
|
||||
isSigningOut: false,
|
||||
error: null,
|
||||
|
||||
restoreSession: async () => {
|
||||
set({ status: "restoring" });
|
||||
if (!useSessionStore.getState().hydrated) {
|
||||
await hydrateSession();
|
||||
}
|
||||
|
||||
const token = useSessionStore.getState().token;
|
||||
if (!token) {
|
||||
set({ status: "unauthenticated" });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const user = await apiClient.me();
|
||||
await signInToFirebase();
|
||||
set({ status: "authenticated", user });
|
||||
} catch (err) {
|
||||
// Expected on expired/invalid tokens — fall back to the login screen.
|
||||
logError(err, { scope: "auth.restore" });
|
||||
await useSessionStore.getState().clearToken();
|
||||
set({ status: "unauthenticated", user: null });
|
||||
}
|
||||
},
|
||||
|
||||
requestCode: async (email: string) => {
|
||||
set({ isRequestingCode: true, error: null });
|
||||
try {
|
||||
await apiClient.requestCode({ email });
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof ApiError ? e.message : "Failed to send code";
|
||||
set({ error: message });
|
||||
throw e;
|
||||
} finally {
|
||||
set({ isRequestingCode: false });
|
||||
}
|
||||
},
|
||||
|
||||
signIn: async (email: string, code: string) => {
|
||||
set({ isSigningIn: true, error: null });
|
||||
try {
|
||||
const { human, token } = await apiClient.signIn({ email, code });
|
||||
await useSessionStore.getState().setToken(token);
|
||||
await signInToFirebase();
|
||||
set({ status: "authenticated", user: human });
|
||||
} catch (e) {
|
||||
const message = e instanceof ApiError ? e.message : "Failed to sign in";
|
||||
set({ error: message });
|
||||
throw e;
|
||||
} finally {
|
||||
set({ isSigningIn: false });
|
||||
}
|
||||
},
|
||||
|
||||
signOut: async () => {
|
||||
set({ isSigningOut: true });
|
||||
try {
|
||||
await apiClient.signOut();
|
||||
} catch (err) {
|
||||
// Best-effort — sign out locally regardless.
|
||||
logError(err, { scope: "auth.signOut" });
|
||||
} finally {
|
||||
await firebaseSignOut(firebaseAuth).catch((err) =>
|
||||
logError(err, { scope: "auth.firebaseSignOut" }),
|
||||
);
|
||||
await useSessionStore.getState().clearToken();
|
||||
set({
|
||||
status: "unauthenticated",
|
||||
user: null,
|
||||
isSigningOut: false,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearError: () => set({ error: null }),
|
||||
}));
|
||||
|
||||
// React to token being cleared externally (e.g. 401 from API client).
|
||||
useSessionStore.subscribe((state, prevState) => {
|
||||
if (prevState.token && !state.token) {
|
||||
const authState = useAuthStore.getState();
|
||||
if (authState.status === "authenticated") {
|
||||
useAuthStore.setState({
|
||||
status: "unauthenticated",
|
||||
user: null,
|
||||
error: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user