dograh/ui/src/lib/auth/providers/StackProviderWrapper.tsx

140 lines
4.3 KiB
TypeScript
Raw Normal View History

2025-09-09 14:37:32 +05:30
'use client';
import { StackClientApp, StackProvider, StackTheme, useUser as useStackUser } from '@stackframe/stack';
import React, { useMemo, useRef } from 'react';
2025-09-09 14:37:32 +05:30
import type { AuthUser } from '../types';
import { AuthContext } from './AuthProvider';
2025-09-09 19:10:18 +05:30
// Create a singleton StackClientApp instance to prevent multiple initializations
let stackClientAppInstance: StackClientApp<true, string> | null = null;
function getStackClientApp(
projectId: string,
publishableClientKey: string,
): StackClientApp<true, string> {
2025-09-09 19:10:18 +05:30
if (!stackClientAppInstance) {
// projectId / publishableClientKey are passed explicitly (fetched from the
// backend at runtime) instead of being read from inlined NEXT_PUBLIC_* env,
// so the prebuilt image works without build-time configuration.
2025-09-09 19:10:18 +05:30
stackClientAppInstance = new StackClientApp({
tokenStore: "nextjs-cookie",
projectId,
publishableClientKey,
2025-09-09 19:10:18 +05:30
urls: {
afterSignIn: "/after-sign-in"
}
});
}
return stackClientAppInstance;
}
2025-09-09 14:37:32 +05:30
interface StackProviderWrapperProps {
children: React.ReactNode;
projectId: string;
publishableClientKey: string;
2025-09-09 14:37:32 +05:30
}
// Simple context provider that uses Stack's useUser directly
function StackAuthContextProvider({ children }: { children: React.ReactNode }) {
const stackUser = useStackUser();
2025-09-09 19:10:18 +05:30
// Store user in ref for callbacks to access latest value without creating new callbacks
const userRef = useRef(stackUser);
userRef.current = stackUser;
2025-09-09 19:10:18 +05:30
// Derive loading state: loading if we don't have a user yet
const isLoading = stackUser === null;
2025-09-09 19:10:18 +05:30
// Stable callbacks that use ref to access current user
const getAccessToken = React.useCallback(async () => {
const user = userRef.current;
if (!user) {
throw new Error('User not authenticated');
2025-09-09 19:10:18 +05:30
}
const authJson = await user.getAuthJson();
if (!authJson.accessToken) {
throw new Error('No access token available');
}
return authJson.accessToken;
}, []);
2025-09-09 19:10:18 +05:30
const redirectToLogin = React.useCallback(() => {
if (typeof window !== 'undefined') {
window.location.href = '/handler/sign-in';
}
}, []);
2025-09-09 19:10:18 +05:30
const logout = React.useCallback(async () => {
2026-02-18 13:16:49 +05:30
// Redirect to Stack's server-side sign-out handler instead of calling
// signOut() client-side. Client-side signOut triggers an internal
// re-render that causes a hooks ordering violation in Stack's components.
if (typeof window !== 'undefined') {
window.location.href = '/handler/sign-out';
2025-09-09 14:37:32 +05:30
}
}, []);
const getSelectedTeam = React.useCallback(() => {
return userRef.current?.selectedTeam ?? null;
}, []);
2025-09-09 19:10:18 +05:30
const listPermissions = React.useCallback(async (team?: unknown) => {
const user = userRef.current;
if (!user?.listPermissions) {
return [];
}
const targetTeam = team || user.selectedTeam;
if (!targetTeam) {
return [];
}
try {
const perms = await user.listPermissions(targetTeam);
return Array.isArray(perms) ? perms : [];
} catch {
return [];
}
}, []);
// IMPORTANT: Use primitive values (userId, isLoading) in deps, NOT stackUser object
// Stack's useUser() returns a new object reference on every render, which would cause infinite re-renders
const userId = stackUser?.id;
const contextValue = useMemo(() => ({
user: userRef.current as AuthUser,
isAuthenticated: !!userId,
loading: isLoading,
getAccessToken,
redirectToLogin,
logout,
provider: 'stack' as const,
getSelectedTeam,
listPermissions,
}), [userId, isLoading, getAccessToken, redirectToLogin, logout, getSelectedTeam, listPermissions]);
2025-09-09 14:37:32 +05:30
return (
<AuthContext.Provider value={contextValue}>
{children}
</AuthContext.Provider>
);
}
const translationOverrides = {
"Email": "Business Email",
"Sign in with {provider}": "Sign in with {provider} Business",
"Sign up with {provider}": "Sign up with {provider} Business",
};
export function StackProviderWrapper({ children, projectId, publishableClientKey }: StackProviderWrapperProps) {
const stackClientApp = getStackClientApp(projectId, publishableClientKey);
2025-09-09 14:37:32 +05:30
return (
<StackProvider app={stackClientApp} translationOverrides={translationOverrides}>
2025-09-09 14:37:32 +05:30
<StackTheme>
<StackAuthContextProvider>
2025-09-09 14:37:32 +05:30
{children}
</StackAuthContextProvider>
</StackTheme>
</StackProvider>
);
}