mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +02:00
Restructure/frontend editor (#6404)
## Move editor under `frontend/editor/`
Pure restructure: `frontend/` becomes the workspace, `frontend/editor/`
holds
the PDF editor. 1775 file renames + 40 wiring edits. No logic changes.
### Why
`frontend/` is currently the editor — its `src/`, `public/`,
`src-tauri/`,
config files all sit at the root. Promoting `frontend/` to a
workspace and putting the editor in a sibling folder leaves room for
future
apps to drop in alongside it, sharing one `package.json` /
`node_modules` /
lint config / Storybook.
### What moves
frontend/
├── editor/ ← NEW: everything editor-specific
│ ├── src/ ← was frontend/src/
│ ├── public/ ← was frontend/public/
│ ├── src-tauri/ ← was frontend/src-tauri/
│ ├── index.html, vite.config.ts, vitest.config.ts, playwright.config.ts
│ ├── tsconfig*.json, tailwind.config.js, postcss.config.js
│ ├── scripts/
│ ├── .env, .env.desktop, .env.saas
│ └── DeveloperGuide.md
├── package.json, package-lock.json, node_modules/ ← workspace install
├── eslint.config.mjs, .prettierrc, .prettierignore ← shared tooling
├── .gitignore
└── README.md
### Wiring edits (40 files)
- `.taskfiles/frontend.yml`, `desktop.yml`, `e2e.yml`
- `build.gradle`, `app/core/build.gradle`
- `eslint.config.mjs`, `frontend/package.json`, `.gitignore`,
`.prettierignore`
- `docker/frontend/Dockerfile`
- 8 `.github/workflows/*.yml`, plus `.github/dependabot.yml`,
`.github/config/.files.yaml`, `.github/labeler-config-srvaroa.yml`
- `scripts/translations/**`
- Docs: `AGENTS.md`, `CLAUDE.md`, `ADDING_TOOLS.md`,
`DeveloperGuide.md`,
`WINDOWS_SIGNING.md`, `devGuide/HowToAddNewLanguage.md`,
`frontend/README.md`,
`frontend/editor/DeveloperGuide.md`
Plus 3 renamed + edited: `editor/vite.config.ts` (env path +
node_modules
walk-up), `editor/scripts/setup-env.mts` (renamed from `.ts` for
`import.meta.url`), `editor/scripts/build-provisioner.mjs` (resolve
src-tauri
relative to script).
### Verification
| Check | Result |
|---|---|
| `task frontend:typecheck:all` (6 variants) | exit 0 |
| `task frontend:lint` (eslint + dpdm) | exit 0 |
| `task frontend:format:check` | exit 0 |
| `task frontend:test` | 657 tests pass, 50 files |
| `task frontend:build:{core,proprietary,saas,desktop,prototypes}` | all
green |
| `task desktop:build` | full Tauri pipeline →
`Stirling-PDF_2.11.0_x64_en-US.msi` |
| `playwright test --list --project=stubbed` | 172 tests discovered |
`task desktop:build` exercises the heaviest path — Rust + WiX + MSI
bundle
against the moved `editor/src-tauri/`. If anything in the restructure
was
wrong it wouldn't have built.
### Test plan
- [ ] `frontend-validation.yml` green
- [ ] `e2e-stubbed.yml` green
- [ ] `tauri-build.yml` green on at least one platform
- [ ] `check_toml.yml` runs on a translation-touching PR
---------
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
48027ee9d6
commit
0a50e765b7
@@ -0,0 +1,710 @@
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
} from "react";
|
||||
import { supabase } from "@app/auth/supabase";
|
||||
import type {
|
||||
Session,
|
||||
User as SupabaseUser,
|
||||
AuthError,
|
||||
} from "@supabase/supabase-js";
|
||||
import {
|
||||
CreditSummary,
|
||||
SubscriptionInfo,
|
||||
CreditCheckResult,
|
||||
ApiCredits,
|
||||
} from "@app/types/credits";
|
||||
import apiClient, {
|
||||
setGlobalCreditUpdateCallback,
|
||||
} from "@app/services/apiClient";
|
||||
import { synchronizeUserUpgrade } from "@app/services/userService";
|
||||
import {
|
||||
syncOAuthAvatar,
|
||||
getProfilePictureMetadata,
|
||||
type ProfilePictureMetadata,
|
||||
} from "@app/services/avatarSyncService";
|
||||
|
||||
// Extend Supabase User to include optional username for compatibility
|
||||
export type User = SupabaseUser & { username?: string };
|
||||
|
||||
export interface TrialStatus {
|
||||
isTrialing: boolean;
|
||||
trialEnd: string;
|
||||
daysRemaining: number;
|
||||
hasPaymentMethod: boolean;
|
||||
hasScheduledSub: boolean;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface AuthContextType {
|
||||
session: Session | null;
|
||||
user: User | null;
|
||||
loading: boolean;
|
||||
error: AuthError | null;
|
||||
creditBalance: number | null;
|
||||
subscription: SubscriptionInfo | null;
|
||||
creditSummary: CreditSummary | null;
|
||||
isPro: boolean | null;
|
||||
trialStatus: TrialStatus | null;
|
||||
profilePictureUrl: string | null;
|
||||
profilePictureMetadata: ProfilePictureMetadata | null;
|
||||
signOut: () => Promise<void>;
|
||||
refreshSession: () => Promise<void>;
|
||||
hasSufficientCredits: (requiredCredits: number) => CreditCheckResult;
|
||||
updateCredits: (newBalance: number) => void;
|
||||
refreshCredits: () => Promise<void>;
|
||||
refreshProStatus: () => Promise<void>;
|
||||
refreshTrialStatus: () => Promise<void>;
|
||||
refreshProfilePicture: () => Promise<void>;
|
||||
refreshProfilePictureMetadata: () => Promise<void>;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
session: null,
|
||||
user: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
creditBalance: null,
|
||||
subscription: null,
|
||||
creditSummary: null,
|
||||
isPro: null,
|
||||
trialStatus: null,
|
||||
profilePictureUrl: null,
|
||||
profilePictureMetadata: null,
|
||||
signOut: async () => {},
|
||||
refreshSession: async () => {},
|
||||
hasSufficientCredits: () => ({
|
||||
hasSufficientCredits: false,
|
||||
currentBalance: 0,
|
||||
requiredCredits: 0,
|
||||
}),
|
||||
updateCredits: () => {},
|
||||
refreshCredits: async () => {},
|
||||
refreshProStatus: async () => {},
|
||||
refreshTrialStatus: async () => {},
|
||||
refreshProfilePicture: async () => {},
|
||||
refreshProfilePictureMetadata: async () => {},
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [session, setSession] = useState<Session | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<AuthError | null>(null);
|
||||
const [creditBalance, setCreditBalance] = useState<number | null>(null);
|
||||
const [subscription, setSubscription] = useState<SubscriptionInfo | null>(
|
||||
null,
|
||||
);
|
||||
const [creditSummary, setCreditSummary] = useState<CreditSummary | null>(
|
||||
null,
|
||||
);
|
||||
const [isPro, setIsPro] = useState<boolean | null>(null);
|
||||
const [trialStatus, setTrialStatus] = useState<TrialStatus | null>(null);
|
||||
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [profilePictureMetadata, setProfilePictureMetadata] =
|
||||
useState<ProfilePictureMetadata | null>(null);
|
||||
|
||||
const fetchCredits = useCallback(
|
||||
async (sessionToUse?: Session | null) => {
|
||||
const currentSession = sessionToUse ?? session;
|
||||
|
||||
if (!currentSession?.user) {
|
||||
console.debug("[Auth Debug] No user session, skipping credit fetch");
|
||||
setCreditBalance(null);
|
||||
setCreditSummary(null);
|
||||
setSubscription(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug(
|
||||
"[Auth Debug] Fetching credits for user:",
|
||||
currentSession.user.id,
|
||||
);
|
||||
const response = await apiClient.get<ApiCredits>("/api/v1/credits");
|
||||
const apiCredits = response.data;
|
||||
|
||||
// Map server payload to app CreditSummary
|
||||
const credits: CreditSummary = {
|
||||
currentCredits: apiCredits.totalAvailableCredits,
|
||||
maxCredits:
|
||||
apiCredits.weeklyCreditsAllocated + apiCredits.totalBoughtCredits,
|
||||
creditsUsed:
|
||||
apiCredits.weeklyCreditsAllocated -
|
||||
apiCredits.weeklyCreditsRemaining +
|
||||
(apiCredits.totalBoughtCredits - apiCredits.boughtCreditsRemaining),
|
||||
creditsRemaining: apiCredits.totalAvailableCredits,
|
||||
resetDate: apiCredits.weeklyResetDate,
|
||||
weeklyAllowance: apiCredits.weeklyCreditsAllocated,
|
||||
};
|
||||
|
||||
setCreditSummary(credits);
|
||||
setCreditBalance(credits.creditsRemaining);
|
||||
|
||||
const subscriptionInfo: SubscriptionInfo = {
|
||||
status: "active",
|
||||
tier: (credits.weeklyAllowance || 0) > 100 ? "premium" : "free",
|
||||
creditsPerWeek: credits.weeklyAllowance,
|
||||
maxCredits: credits.maxCredits,
|
||||
};
|
||||
setSubscription(subscriptionInfo);
|
||||
|
||||
console.debug("[Auth Debug] Credits fetched successfully:", credits);
|
||||
} catch (error: unknown) {
|
||||
console.debug("[Auth Debug] Failed to fetch credits:", error);
|
||||
// Don't set error state for credit fetching failures to avoid disrupting auth flow
|
||||
// Credits might not be available in all deployments
|
||||
setCreditBalance(null);
|
||||
setCreditSummary(null);
|
||||
setSubscription(null);
|
||||
}
|
||||
},
|
||||
[session],
|
||||
);
|
||||
|
||||
const refreshCredits = useCallback(async () => {
|
||||
await fetchCredits();
|
||||
}, [fetchCredits]);
|
||||
|
||||
const fetchProStatus = useCallback(
|
||||
async (sessionToUse?: Session | null) => {
|
||||
const currentSession = sessionToUse ?? session;
|
||||
|
||||
if (!currentSession?.user) {
|
||||
console.debug(
|
||||
"[Auth Debug] No user session, skipping pro status fetch",
|
||||
);
|
||||
setIsPro(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug(
|
||||
"[Auth Debug] Fetching pro status for user:",
|
||||
currentSession.user.id,
|
||||
);
|
||||
const { data: proStatus, error } = await supabase.rpc("is_pro");
|
||||
|
||||
if (error) {
|
||||
console.error("[Auth Debug] Error checking Pro status:", error);
|
||||
setIsPro(false); // Default to false if there's an error
|
||||
} else {
|
||||
const isProUser = Boolean(proStatus);
|
||||
setIsPro(isProUser);
|
||||
console.debug("[Auth Debug] Pro status fetched:", isProUser);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.debug("[Auth Debug] Failed to fetch pro status:", error);
|
||||
setIsPro(false); // Default to false if there's an error
|
||||
}
|
||||
},
|
||||
[session],
|
||||
);
|
||||
|
||||
const refreshProStatus = useCallback(async () => {
|
||||
await fetchProStatus();
|
||||
}, [fetchProStatus]);
|
||||
|
||||
const fetchTrialStatus = useCallback(
|
||||
async (sessionToUse?: Session | null) => {
|
||||
const currentSession = sessionToUse ?? session;
|
||||
|
||||
if (!currentSession?.user) {
|
||||
console.debug(
|
||||
"[Auth Debug] No user session, skipping trial status fetch",
|
||||
);
|
||||
setTrialStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug(
|
||||
"[Auth Debug] Fetching trial status for user:",
|
||||
currentSession.user.id,
|
||||
);
|
||||
const { data, error } = await supabase
|
||||
.from("billing_subscriptions")
|
||||
.select(
|
||||
"status, trial_end, has_payment_method, scheduled_subscription_id",
|
||||
)
|
||||
.in("status", ["trialing", "incomplete_expired", "canceled"])
|
||||
.order("created_at", { ascending: false })
|
||||
.limit(1)
|
||||
.maybeSingle();
|
||||
|
||||
if (error) {
|
||||
console.error("[Auth Debug] Error fetching trial status:", error);
|
||||
setTrialStatus(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data?.trial_end) {
|
||||
const trialEnd = new Date(data.trial_end);
|
||||
const now = new Date();
|
||||
const daysRemaining = Math.ceil(
|
||||
(trialEnd.getTime() - now.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
setTrialStatus({
|
||||
isTrialing: data.status === "trialing" && daysRemaining > 0,
|
||||
trialEnd: data.trial_end,
|
||||
daysRemaining: Math.max(0, daysRemaining),
|
||||
hasPaymentMethod: data.has_payment_method || false,
|
||||
hasScheduledSub: !!data.scheduled_subscription_id,
|
||||
status: data.status,
|
||||
});
|
||||
console.debug("[Auth Debug] Trial status fetched:", {
|
||||
status: data.status,
|
||||
daysRemaining: Math.max(0, daysRemaining),
|
||||
hasPaymentMethod: data.has_payment_method,
|
||||
isTrialing: data.status === "trialing" && daysRemaining > 0,
|
||||
});
|
||||
} else {
|
||||
setTrialStatus(null);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.debug("[Auth Debug] Failed to fetch trial status:", error);
|
||||
setTrialStatus(null);
|
||||
}
|
||||
},
|
||||
[session],
|
||||
);
|
||||
|
||||
const refreshTrialStatus = useCallback(async () => {
|
||||
await fetchTrialStatus();
|
||||
}, [fetchTrialStatus]);
|
||||
|
||||
const fetchProfilePicture = useCallback(
|
||||
async (sessionToUse?: Session | null) => {
|
||||
const currentSession = sessionToUse ?? session;
|
||||
|
||||
if (!currentSession?.user) {
|
||||
console.debug(
|
||||
"[Auth Debug] No user session, skipping profile picture fetch",
|
||||
);
|
||||
setProfilePictureUrl(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const PROFILE_BUCKET = "profile-pictures";
|
||||
const profilePath = `${currentSession.user.id}/avatar`;
|
||||
|
||||
console.debug(
|
||||
"[Auth Debug] Fetching profile picture for user:",
|
||||
currentSession.user.id,
|
||||
);
|
||||
const { data, error } = await supabase.storage
|
||||
.from(PROFILE_BUCKET)
|
||||
.createSignedUrl(profilePath, 60 * 60);
|
||||
|
||||
if (error) {
|
||||
// Profile picture not found is expected for users without uploads
|
||||
console.debug(
|
||||
"[Auth Debug] Profile picture not available:",
|
||||
error.message,
|
||||
);
|
||||
setProfilePictureUrl(null);
|
||||
} else {
|
||||
setProfilePictureUrl(data.signedUrl);
|
||||
console.debug(
|
||||
"[Auth Debug] Profile picture URL fetched successfully",
|
||||
);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.debug("[Auth Debug] Failed to fetch profile picture:", error);
|
||||
setProfilePictureUrl(null);
|
||||
}
|
||||
},
|
||||
[session],
|
||||
);
|
||||
|
||||
const refreshProfilePicture = useCallback(async () => {
|
||||
await fetchProfilePicture();
|
||||
}, [fetchProfilePicture]);
|
||||
|
||||
const fetchProfilePictureMetadata = useCallback(
|
||||
async (sessionToUse?: Session | null) => {
|
||||
const currentSession = sessionToUse ?? session;
|
||||
|
||||
if (!currentSession?.user) {
|
||||
console.debug(
|
||||
"[Auth Debug] No user session, skipping profile picture metadata fetch",
|
||||
);
|
||||
setProfilePictureMetadata(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.debug(
|
||||
"[Auth Debug] Fetching profile picture metadata for user:",
|
||||
currentSession.user.id,
|
||||
);
|
||||
const metadata = await getProfilePictureMetadata(
|
||||
currentSession.user.id,
|
||||
);
|
||||
setProfilePictureMetadata(metadata);
|
||||
console.debug(
|
||||
"[Auth Debug] Profile picture metadata fetched:",
|
||||
metadata,
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
console.debug(
|
||||
"[Auth Debug] Failed to fetch profile picture metadata:",
|
||||
error,
|
||||
);
|
||||
setProfilePictureMetadata(null);
|
||||
}
|
||||
},
|
||||
[session],
|
||||
);
|
||||
|
||||
const refreshProfilePictureMetadata = useCallback(async () => {
|
||||
await fetchProfilePictureMetadata();
|
||||
}, [fetchProfilePictureMetadata]);
|
||||
|
||||
const updateCredits = useCallback(
|
||||
(newBalance: number) => {
|
||||
console.debug("[Auth Debug] Updating credit balance:", {
|
||||
from: creditBalance,
|
||||
to: newBalance,
|
||||
});
|
||||
setCreditBalance(newBalance);
|
||||
// Also update the creditSummary if it exists
|
||||
if (creditSummary) {
|
||||
const updatedSummary: CreditSummary = {
|
||||
...creditSummary,
|
||||
creditsRemaining: newBalance,
|
||||
currentCredits: newBalance,
|
||||
};
|
||||
setCreditSummary(updatedSummary);
|
||||
}
|
||||
},
|
||||
[creditSummary],
|
||||
);
|
||||
|
||||
const hasSufficientCredits = useCallback(
|
||||
(requiredCredits: number): CreditCheckResult => {
|
||||
const currentBalance = creditBalance ?? 0;
|
||||
const hasSufficient = currentBalance >= requiredCredits;
|
||||
console.debug("[Auth Debug] Credit check:", {
|
||||
requiredCredits,
|
||||
currentBalance,
|
||||
hasSufficient,
|
||||
});
|
||||
|
||||
return {
|
||||
hasSufficientCredits: hasSufficient,
|
||||
currentBalance,
|
||||
requiredCredits,
|
||||
shortfall: hasSufficient ? undefined : requiredCredits - currentBalance,
|
||||
};
|
||||
},
|
||||
[creditBalance],
|
||||
);
|
||||
|
||||
const refreshSession = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const { data, error } = await supabase.auth.refreshSession();
|
||||
|
||||
if (error) {
|
||||
console.error("[Auth Debug] Session refresh error:", error);
|
||||
setError(error);
|
||||
setSession(null);
|
||||
} else {
|
||||
console.debug("[Auth Debug] Session refreshed successfully");
|
||||
setSession(data.session);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[Auth Debug] Unexpected error during session refresh:",
|
||||
err,
|
||||
);
|
||||
setError(err as AuthError);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error("[Auth Debug] Sign out error:", error);
|
||||
setError(error);
|
||||
} else {
|
||||
console.debug("[Auth Debug] Signed out successfully");
|
||||
setSession(null);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[Auth Debug] Unexpected error during sign out:", err);
|
||||
setError(err as AuthError);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up global credit update callback
|
||||
useEffect(() => {
|
||||
setGlobalCreditUpdateCallback(updateCredits);
|
||||
}, [updateCredits]);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
// Load current session on first mount
|
||||
const initializeAuth = async () => {
|
||||
try {
|
||||
console.debug("[Auth Debug] Initializing auth...");
|
||||
const { data, error } = await supabase.auth.getSession();
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (error) {
|
||||
console.error("[Auth Debug] Initial session error:", error);
|
||||
setError(error);
|
||||
} else {
|
||||
console.debug("[Auth Debug] Initial session loaded:", {
|
||||
hasSession: !!data.session,
|
||||
userId: data.session?.user?.id,
|
||||
email: data.session?.user?.email,
|
||||
});
|
||||
setSession(data.session);
|
||||
|
||||
// Fetch credits, pro status, trial status, profile picture metadata, and profile picture using the session from the response
|
||||
if (data.session?.user) {
|
||||
// Sync OAuth avatar in background
|
||||
syncOAuthAvatar(data.session.user).catch((err) => {
|
||||
console.debug(
|
||||
"[Auth Debug] Failed to sync OAuth avatar on init:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
|
||||
await fetchCredits(data.session);
|
||||
await fetchProStatus(data.session);
|
||||
await fetchTrialStatus(data.session);
|
||||
await fetchProfilePictureMetadata(data.session);
|
||||
|
||||
// Small delay to allow avatar sync to complete if quick
|
||||
setTimeout(() => {
|
||||
fetchProfilePicture(data.session);
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"[Auth Debug] Unexpected error during auth initialization:",
|
||||
err,
|
||||
);
|
||||
if (mounted) {
|
||||
setError(err as AuthError);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
initializeAuth();
|
||||
|
||||
// Subscribe to auth state changes
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange(async (event, newSession) => {
|
||||
if (!mounted) return;
|
||||
|
||||
console.debug("[Auth Debug] Auth state change:", {
|
||||
event,
|
||||
hasSession: !!newSession,
|
||||
userId: newSession?.user?.id,
|
||||
email: newSession?.user?.email,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
// Don't run supabase calls inside this callback; schedule them
|
||||
setTimeout(() => {
|
||||
if (mounted) {
|
||||
setSession(newSession);
|
||||
setError(null);
|
||||
|
||||
// Additional handling for specific events
|
||||
if (event === "SIGNED_OUT") {
|
||||
console.debug("[Auth Debug] User signed out, clearing session");
|
||||
// Clear credit data, pro status, trial status, profile picture, and metadata on sign out
|
||||
setCreditBalance(null);
|
||||
setCreditSummary(null);
|
||||
setSubscription(null);
|
||||
setIsPro(null);
|
||||
setTrialStatus(null);
|
||||
setProfilePictureUrl(null);
|
||||
setProfilePictureMetadata(null);
|
||||
} else if (event === "SIGNED_IN") {
|
||||
console.debug("[Auth Debug] User signed in successfully");
|
||||
if (newSession?.user) {
|
||||
setLoading(true);
|
||||
|
||||
// Sync OAuth avatar in background (don't block other fetches)
|
||||
syncOAuthAvatar(newSession.user).catch((err) => {
|
||||
console.debug("[Auth Debug] Failed to sync OAuth avatar:", err);
|
||||
});
|
||||
|
||||
// Fetch user data in parallel
|
||||
Promise.all([
|
||||
fetchCredits(newSession),
|
||||
fetchProStatus(newSession),
|
||||
fetchTrialStatus(newSession),
|
||||
fetchProfilePictureMetadata(newSession),
|
||||
]).then(() => {
|
||||
// Fetch profile picture AFTER sync has had time to complete
|
||||
// Use a small delay to allow avatar sync to finish if it's quick
|
||||
setTimeout(() => {
|
||||
fetchProfilePicture(newSession).finally(() => {
|
||||
setLoading(false);
|
||||
console.debug(
|
||||
"[Auth Debug] User data fully loaded after sign in",
|
||||
);
|
||||
});
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
} else if (event === "TOKEN_REFRESHED") {
|
||||
console.debug("[Auth Debug] Token refreshed");
|
||||
// Optionally refresh credits, pro status, trial status, profile picture metadata, and profile picture on token refresh
|
||||
if (newSession?.user) {
|
||||
Promise.all([
|
||||
fetchCredits(newSession),
|
||||
fetchProStatus(newSession),
|
||||
fetchTrialStatus(newSession),
|
||||
fetchProfilePictureMetadata(newSession),
|
||||
fetchProfilePicture(newSession),
|
||||
]).then(() => {
|
||||
console.debug(
|
||||
"[Auth Debug] User data refreshed after token refresh",
|
||||
);
|
||||
});
|
||||
}
|
||||
} else if (event === "USER_UPDATED") {
|
||||
console.debug("[Auth Debug] User updated");
|
||||
|
||||
// Check if this is a pending OAuth upgrade completion
|
||||
const pendingUpgrade = sessionStorage.getItem("pendingUpgrade");
|
||||
const upgradeProvider = sessionStorage.getItem("upgradeProvider");
|
||||
|
||||
if (
|
||||
pendingUpgrade &&
|
||||
newSession?.user &&
|
||||
newSession.user.is_anonymous === false
|
||||
) {
|
||||
console.debug(
|
||||
"[Auth Debug] Processing pending OAuth upgrade:",
|
||||
upgradeProvider,
|
||||
);
|
||||
|
||||
// Clear the flags first to prevent loops
|
||||
sessionStorage.removeItem("pendingUpgrade");
|
||||
sessionStorage.removeItem("upgradeProvider");
|
||||
|
||||
// Synchronize with backend
|
||||
synchronizeUserUpgrade(upgradeProvider || undefined)
|
||||
.then(() => {
|
||||
console.debug(
|
||||
"[Auth Debug] User upgrade synchronized successfully",
|
||||
);
|
||||
|
||||
// Refresh credits, pro status, trial status, profile picture metadata, and profile picture after upgrade
|
||||
if (newSession?.user) {
|
||||
return Promise.all([
|
||||
fetchCredits(newSession),
|
||||
fetchProStatus(newSession),
|
||||
fetchTrialStatus(newSession),
|
||||
fetchProfilePictureMetadata(newSession),
|
||||
fetchProfilePicture(newSession),
|
||||
]);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
console.debug(
|
||||
"[Auth Debug] User data refreshed after upgrade",
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(
|
||||
"[Auth Debug] Failed to synchronize user upgrade:",
|
||||
err,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
subscription.unsubscribe();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const value: AuthContextType = {
|
||||
session,
|
||||
user: session?.user ?? null,
|
||||
loading,
|
||||
error,
|
||||
creditBalance,
|
||||
subscription,
|
||||
creditSummary,
|
||||
isPro,
|
||||
trialStatus,
|
||||
profilePictureUrl,
|
||||
profilePictureMetadata,
|
||||
signOut,
|
||||
refreshSession,
|
||||
hasSufficientCredits,
|
||||
updateCredits,
|
||||
refreshCredits,
|
||||
refreshProStatus,
|
||||
refreshTrialStatus,
|
||||
refreshProfilePicture,
|
||||
refreshProfilePictureMetadata,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
}
|
||||
|
||||
export function useAuth() {
|
||||
const context = useContext(AuthContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
// Debug hook to expose auth state for debugging
|
||||
export function useAuthDebug() {
|
||||
const auth = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
console.debug("[Auth Debug] Current auth state:", {
|
||||
hasSession: !!auth.session,
|
||||
hasUser: !!auth.user,
|
||||
loading: auth.loading,
|
||||
hasError: !!auth.error,
|
||||
userId: auth.user?.id,
|
||||
email: auth.user?.email,
|
||||
provider: auth.user?.app_metadata?.provider,
|
||||
});
|
||||
}, [auth.session, auth.user, auth.loading, auth.error]);
|
||||
|
||||
return auth;
|
||||
}
|
||||
Reference in New Issue
Block a user