2025-09-09 14:37:32 +05:30
|
|
|
'use client';
|
|
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
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;
|
|
|
|
|
|
2026-06-18 14:17:28 +05:30
|
|
|
function getStackClientApp(
|
|
|
|
|
projectId: string,
|
|
|
|
|
publishableClientKey: string,
|
|
|
|
|
): StackClientApp<true, string> {
|
2025-09-09 19:10:18 +05:30
|
|
|
if (!stackClientAppInstance) {
|
2026-06-18 14:17:28 +05:30
|
|
|
// 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",
|
2026-06-18 14:17:28 +05:30
|
|
|
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;
|
2026-06-18 14:17:28 +05:30
|
|
|
projectId: string;
|
|
|
|
|
publishableClientKey: string;
|
2025-09-09 14:37:32 +05:30
|
|
|
}
|
|
|
|
|
|
2025-11-29 15:39:57 +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
|
|
|
|
2025-11-29 15:39:57 +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
|
|
|
|
2025-11-29 15:39:57 +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
|
|
|
|
2025-11-29 15:39:57 +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
|
|
|
}
|
2025-11-29 15:39:57 +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
|
|
|
|
2025-11-29 15:39:57 +05:30
|
|
|
const redirectToLogin = React.useCallback(() => {
|
|
|
|
|
if (typeof window !== 'undefined') {
|
|
|
|
|
window.location.href = '/handler/sign-in';
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2025-09-09 19:10:18 +05:30
|
|
|
|
2025-11-29 15:39:57 +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
|
|
|
}
|
2025-11-29 15:39:57 +05:30
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const getSelectedTeam = React.useCallback(() => {
|
|
|
|
|
return userRef.current?.selectedTeam ?? null;
|
|
|
|
|
}, []);
|
2025-09-09 19:10:18 +05:30
|
|
|
|
2025-11-29 15:39:57 +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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-15 16:46:07 +05:30
|
|
|
const translationOverrides = {
|
|
|
|
|
"Email": "Business Email",
|
|
|
|
|
"Sign in with {provider}": "Sign in with {provider} Business",
|
|
|
|
|
"Sign up with {provider}": "Sign up with {provider} Business",
|
|
|
|
|
};
|
|
|
|
|
|
2026-06-18 14:17:28 +05:30
|
|
|
export function StackProviderWrapper({ children, projectId, publishableClientKey }: StackProviderWrapperProps) {
|
|
|
|
|
const stackClientApp = getStackClientApp(projectId, publishableClientKey);
|
2025-09-09 14:37:32 +05:30
|
|
|
|
|
|
|
|
return (
|
2026-05-15 16:46:07 +05:30
|
|
|
<StackProvider app={stackClientApp} translationOverrides={translationOverrides}>
|
2025-09-09 14:37:32 +05:30
|
|
|
<StackTheme>
|
2025-11-29 15:39:57 +05:30
|
|
|
<StackAuthContextProvider>
|
2025-09-09 14:37:32 +05:30
|
|
|
{children}
|
|
|
|
|
</StackAuthContextProvider>
|
|
|
|
|
</StackTheme>
|
|
|
|
|
</StackProvider>
|
|
|
|
|
);
|
|
|
|
|
}
|