feat: mejorar el componente de ayuda para proformas y ajustar pruebas de actualización
This commit is contained in:
parent
a37d6fc96b
commit
1a8d63373d
@ -10,6 +10,7 @@ import {
|
||||
Separator,
|
||||
} from "@repo/shadcn-ui/components";
|
||||
import { AlertCircleIcon, CheckCircle2Icon, CircleDashedIcon, CircleHelpIcon } from "lucide-react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { useTranslation } from "../../../../i18n";
|
||||
import type { ProformaIssuanceReadiness } from "../../../shared";
|
||||
@ -23,7 +24,7 @@ interface ProformaUpdateHelpPopoverProps {
|
||||
onRetryReadiness?: () => void;
|
||||
}
|
||||
|
||||
interface HelpTriggerProps {
|
||||
interface HelpTriggerProps extends ComponentProps<typeof Button> {
|
||||
ariaLabel: string;
|
||||
pendingBlockersCount: number;
|
||||
}
|
||||
@ -39,9 +40,16 @@ interface HelpReadinessSectionProps {
|
||||
translate: (key: string, params?: Record<string, string | number | null | undefined>) => string;
|
||||
}
|
||||
|
||||
const HelpTrigger = ({ ariaLabel, pendingBlockersCount }: HelpTriggerProps) => {
|
||||
const HelpTrigger = ({ ariaLabel, className, pendingBlockersCount, ...props }: HelpTriggerProps) => {
|
||||
return (
|
||||
<Button aria-label={ariaLabel} className="relative" size="icon" type="button" variant="outline">
|
||||
<Button
|
||||
{...props}
|
||||
aria-label={ariaLabel}
|
||||
className={className ? `relative ${className}` : "relative"}
|
||||
size="icon"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<CircleHelpIcon aria-hidden="true" className="size-4" />
|
||||
{pendingBlockersCount > 0 ? (
|
||||
<Badge
|
||||
@ -190,9 +198,9 @@ export const ProformaUpdateHelpPopover = ({
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />
|
||||
</PopoverTrigger>
|
||||
<PopoverTrigger
|
||||
render={<HelpTrigger ariaLabel={triggerAriaLabel} pendingBlockersCount={pendingBlockersCount} />}
|
||||
/>
|
||||
|
||||
<PopoverContent
|
||||
align="end"
|
||||
|
||||
@ -31,7 +31,7 @@ const buildFormData = (value: Partial<ProformaUpdateForm> = {}): ProformaUpdateF
|
||||
...value,
|
||||
}) as ProformaUpdateForm;
|
||||
|
||||
test("excluye items vacíos no valorados al construir el PUT de update", () => {
|
||||
test("mantiene los items vacíos y respeta su orden al construir el PUT de update", () => {
|
||||
const formData = buildFormData({
|
||||
items: [
|
||||
{
|
||||
@ -66,7 +66,17 @@ test("excluye items vacíos no valorados al construir el PUT de update", () => {
|
||||
formData
|
||||
);
|
||||
|
||||
assert.equal(result.data.items?.length, 1);
|
||||
assert.equal(result.data.items?.[0]?.description, "Servicio");
|
||||
assert.equal(result.data.items?.length, 2);
|
||||
assert.equal(result.data.items?.[0]?.description, "");
|
||||
assert.equal(result.data.items?.[0]?.quantity, null);
|
||||
assert.equal(result.data.items?.[0]?.unit_amount, null);
|
||||
assert.equal(result.data.items?.[0]?.position, 0);
|
||||
assert.equal(result.data.items?.[1]?.description, "Servicio");
|
||||
assert.deepEqual(result.data.items?.[1]?.quantity, { value: "100", scale: "2" });
|
||||
assert.deepEqual(result.data.items?.[1]?.unit_amount, {
|
||||
value: "1000000",
|
||||
scale: "4",
|
||||
currency_code: "EUR",
|
||||
});
|
||||
assert.equal(result.data.items?.[1]?.position, 1);
|
||||
});
|
||||
|
||||
@ -109,9 +109,7 @@ export const buildUpdateProformaByIdParams = (
|
||||
// Si se han tocado los detalles, se envían todos
|
||||
if (shouldReplaceItems(patch)) {
|
||||
const context = buildProformaItemUpdateDTOContext(formData);
|
||||
data.items = formData.items
|
||||
.filter(isValuedProformaItem)
|
||||
.map((item, index) => toProformaItemUpdateDTO(item, index, context));
|
||||
data.items = formData.items.map((item, index) => toProformaItemUpdateDTO(item, index, context));
|
||||
}
|
||||
|
||||
return {
|
||||
@ -146,10 +144,6 @@ const toProformaItemUpdateDTO = (
|
||||
};
|
||||
};
|
||||
|
||||
const isValuedProformaItem = (item: ProformaItemUpdateForm): boolean => {
|
||||
return item.quantity !== null && item.unitAmount !== null;
|
||||
};
|
||||
|
||||
const buildProformaItemUpdateDTOContext = (
|
||||
formData: ProformaUpdateForm
|
||||
): ProformaItemUpdateDTOContext => {
|
||||
|
||||
@ -1,404 +1,404 @@
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { LoginForm } from "../login/entities";
|
||||
import {
|
||||
AvailableCompaniesResponseDTOSchema,
|
||||
activeCompanyStorage,
|
||||
authTokenStorage,
|
||||
findAvailableCompanies,
|
||||
mapAvailableCompany,
|
||||
mapCurrentSessionToAuthSession,
|
||||
resolveAuthenticatedRedirectPath,
|
||||
resolveCompanySession,
|
||||
useLoginMutation,
|
||||
useLogoutMutation,
|
||||
useRefreshSessionMutation,
|
||||
} from "../shared";
|
||||
import { getCurrentSession } from "../shared/api";
|
||||
import type { ActiveCompany, AuthSession, CompanySession } from "../shared/entities";
|
||||
|
||||
import {
|
||||
resetIdentityAuthSessionRuntimeHandlers,
|
||||
setIdentityAuthSessionRuntimeHandlers,
|
||||
} from "./identity-auth-session.runtime";
|
||||
|
||||
export interface IdentityAuthSessionContextValue {
|
||||
session: AuthSession | null;
|
||||
account: AuthSession["account"] | null;
|
||||
companySession: CompanySession;
|
||||
availableCompanies: ActiveCompany[];
|
||||
activeCompany: ActiveCompany | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (params: LoginForm) => Promise<AuthSession>;
|
||||
logout: () => Promise<void>;
|
||||
restoreSession: () => Promise<AuthSession | null>;
|
||||
refreshSession: () => Promise<AuthSession | null>;
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
selectActiveCompany: (companyId: string) => ActiveCompany | null;
|
||||
changeActiveCompany: (companyId: string) => Promise<ActiveCompany | null>;
|
||||
updateActiveCompany: (company: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
}) => void;
|
||||
updateAccount: (account: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
languageCode: string;
|
||||
}) => void;
|
||||
getAuthenticatedRedirectPath: (params?: {
|
||||
returnTo?: string | null;
|
||||
fallbackPath?: string;
|
||||
}) => string;
|
||||
clearSession: () => void;
|
||||
}
|
||||
|
||||
const IdentityAuthSessionContext = createContext<IdentityAuthSessionContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const EMPTY_COMPANY_SESSION: CompanySession = {
|
||||
availableCompanies: [],
|
||||
activeCompany: null,
|
||||
};
|
||||
|
||||
export function IdentityAuthSessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const dataSource = useDataSource();
|
||||
const loginMutation = useLoginMutation();
|
||||
const logoutMutation = useLogoutMutation();
|
||||
const refreshSessionMutation = useRefreshSessionMutation();
|
||||
|
||||
const [session, setSession] = useState<AuthSession | null>(null);
|
||||
const [companySession, setCompanySession] = useState<CompanySession>(EMPTY_COMPANY_SESSION);
|
||||
const [isRestoringSession, setIsRestoringSession] = useState(authTokenStorage.hasAccessToken());
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
authTokenStorage.clear();
|
||||
activeCompanyStorage.clear();
|
||||
setSession(null);
|
||||
setCompanySession(EMPTY_COMPANY_SESSION);
|
||||
}, []);
|
||||
|
||||
const hydrateCompanySession = useCallback(
|
||||
async (accessToken: string) => {
|
||||
const response = await findAvailableCompanies(dataSource, accessToken);
|
||||
const parsedResponse = AvailableCompaniesResponseDTOSchema.safeParse(response);
|
||||
|
||||
if (!parsedResponse.success) {
|
||||
throw new Error("Invalid available companies response");
|
||||
}
|
||||
|
||||
const availableCompanies = parsedResponse.data.companies.map(mapAvailableCompany);
|
||||
const nextCompanySession = resolveCompanySession({
|
||||
companies: availableCompanies,
|
||||
persistedActiveCompanyId: activeCompanyStorage.getActiveCompanyId(),
|
||||
});
|
||||
|
||||
if (nextCompanySession.activeCompany) {
|
||||
activeCompanyStorage.setActiveCompanyId(nextCompanySession.activeCompany.id);
|
||||
} else {
|
||||
activeCompanyStorage.clear();
|
||||
}
|
||||
|
||||
setCompanySession(nextCompanySession);
|
||||
|
||||
return nextCompanySession;
|
||||
},
|
||||
[dataSource]
|
||||
);
|
||||
|
||||
const restoreSession = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
if (!tokens.accessToken) {
|
||||
clearSession();
|
||||
setIsRestoringSession(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsRestoringSession(true);
|
||||
|
||||
try {
|
||||
const response = await getCurrentSession(dataSource, tokens.accessToken);
|
||||
const restoredSession = mapCurrentSessionToAuthSession(response, {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken ?? "",
|
||||
});
|
||||
|
||||
await hydrateCompanySession(tokens.accessToken);
|
||||
setSession(restoredSession);
|
||||
|
||||
return restoredSession;
|
||||
} catch {
|
||||
clearSession();
|
||||
return null;
|
||||
} finally {
|
||||
setIsRestoringSession(false);
|
||||
}
|
||||
}, [clearSession, dataSource, hydrateCompanySession]);
|
||||
|
||||
const login = useCallback(
|
||||
async (params: LoginForm) => {
|
||||
const nextSession = await loginMutation.mutateAsync(params);
|
||||
|
||||
authTokenStorage.setTokens({
|
||||
accessToken: nextSession.accessToken,
|
||||
refreshToken: nextSession.refreshToken,
|
||||
});
|
||||
|
||||
activeCompanyStorage.clear();
|
||||
await hydrateCompanySession(nextSession.accessToken);
|
||||
setSession(nextSession);
|
||||
|
||||
return nextSession;
|
||||
},
|
||||
[hydrateCompanySession, loginMutation]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
try {
|
||||
if (tokens.accessToken) {
|
||||
await logoutMutation.mutateAsync({
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
clearSession();
|
||||
}
|
||||
}, [clearSession, logoutMutation]);
|
||||
|
||||
const refreshSession = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
if (!tokens.refreshToken) {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshedTokens = await refreshSessionMutation.mutateAsync({
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
|
||||
authTokenStorage.setTokens(refreshedTokens);
|
||||
await hydrateCompanySession(refreshedTokens.accessToken);
|
||||
|
||||
if (session) {
|
||||
const refreshedSession: AuthSession = {
|
||||
...session,
|
||||
accessToken: refreshedTokens.accessToken,
|
||||
refreshToken: refreshedTokens.refreshToken,
|
||||
};
|
||||
|
||||
setSession(refreshedSession);
|
||||
return refreshedSession;
|
||||
}
|
||||
|
||||
const currentSessionResponse = await getCurrentSession(
|
||||
dataSource,
|
||||
refreshedTokens.accessToken
|
||||
);
|
||||
const refreshedSession = mapCurrentSessionToAuthSession(currentSessionResponse, {
|
||||
accessToken: refreshedTokens.accessToken,
|
||||
refreshToken: refreshedTokens.refreshToken,
|
||||
});
|
||||
|
||||
setSession(refreshedSession);
|
||||
|
||||
return refreshedSession;
|
||||
} catch {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
}, [clearSession, dataSource, hydrateCompanySession, refreshSessionMutation, session]);
|
||||
|
||||
const refreshAccessToken = useCallback(async () => {
|
||||
const refreshedSession = await refreshSession();
|
||||
return refreshedSession?.accessToken ?? null;
|
||||
}, [refreshSession]);
|
||||
|
||||
const selectActiveCompany = useCallback(
|
||||
(companyId: string) => {
|
||||
if (companySession.activeCompany?.id === companyId) {
|
||||
return companySession.activeCompany;
|
||||
}
|
||||
|
||||
const nextActiveCompany =
|
||||
companySession.availableCompanies.find((company) => company.id === companyId) ?? null;
|
||||
|
||||
if (!nextActiveCompany) {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeCompanyStorage.setActiveCompanyId(nextActiveCompany.id);
|
||||
setCompanySession((currentSession) => ({
|
||||
...currentSession,
|
||||
activeCompany: nextActiveCompany,
|
||||
}));
|
||||
|
||||
return nextActiveCompany;
|
||||
},
|
||||
[companySession.activeCompany, companySession.availableCompanies]
|
||||
);
|
||||
|
||||
const changeActiveCompany = useCallback(
|
||||
async (companyId: string) => {
|
||||
return selectActiveCompany(companyId);
|
||||
},
|
||||
[selectActiveCompany]
|
||||
);
|
||||
|
||||
const updateActiveCompany = useCallback(
|
||||
(company: { id: string; legalName: string; tradeName: string | null }) => {
|
||||
setCompanySession((currentSession) => {
|
||||
const availableCompanies = currentSession.availableCompanies.map((item) =>
|
||||
item.id === company.id
|
||||
? {
|
||||
...item,
|
||||
legalName: company.legalName,
|
||||
tradeName: company.tradeName,
|
||||
}
|
||||
: item
|
||||
);
|
||||
|
||||
const activeCompany =
|
||||
currentSession.activeCompany?.id === company.id
|
||||
? {
|
||||
...currentSession.activeCompany,
|
||||
legalName: company.legalName,
|
||||
tradeName: company.tradeName,
|
||||
}
|
||||
: currentSession.activeCompany;
|
||||
|
||||
return {
|
||||
availableCompanies,
|
||||
activeCompany,
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateAccount = useCallback(
|
||||
(account: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
languageCode: string;
|
||||
}) => {
|
||||
setSession((currentSession) => {
|
||||
if (!currentSession || currentSession.account.id !== account.id) {
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentSession,
|
||||
account: {
|
||||
...currentSession.account,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
avatarUrl: account.avatarUrl,
|
||||
languageCode: account.languageCode,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getAuthenticatedRedirectPath = useCallback(
|
||||
(params?: { returnTo?: string | null; fallbackPath?: string }) => {
|
||||
return resolveAuthenticatedRedirectPath({
|
||||
companySession,
|
||||
fallbackPath: params?.fallbackPath ?? "/proformas",
|
||||
returnTo: params?.returnTo,
|
||||
});
|
||||
},
|
||||
[companySession]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
restoreSession();
|
||||
}, [restoreSession]);
|
||||
|
||||
useEffect(() => {
|
||||
setIdentityAuthSessionRuntimeHandlers({
|
||||
clearSession,
|
||||
refreshAccessToken,
|
||||
});
|
||||
|
||||
return () => {
|
||||
resetIdentityAuthSessionRuntimeHandlers();
|
||||
};
|
||||
}, [clearSession, refreshAccessToken]);
|
||||
|
||||
const value = useMemo<IdentityAuthSessionContextValue>(
|
||||
() => ({
|
||||
session,
|
||||
account: session?.account ?? null,
|
||||
companySession,
|
||||
availableCompanies: companySession.availableCompanies,
|
||||
activeCompany: companySession.activeCompany,
|
||||
isAuthenticated: Boolean(session),
|
||||
isLoading:
|
||||
isRestoringSession ||
|
||||
loginMutation.isPending ||
|
||||
logoutMutation.isPending ||
|
||||
refreshSessionMutation.isPending,
|
||||
login,
|
||||
logout,
|
||||
restoreSession,
|
||||
refreshSession,
|
||||
refreshAccessToken,
|
||||
selectActiveCompany,
|
||||
changeActiveCompany,
|
||||
updateActiveCompany,
|
||||
updateAccount,
|
||||
getAuthenticatedRedirectPath,
|
||||
clearSession,
|
||||
}),
|
||||
[
|
||||
changeActiveCompany,
|
||||
clearSession,
|
||||
companySession,
|
||||
getAuthenticatedRedirectPath,
|
||||
isRestoringSession,
|
||||
login,
|
||||
loginMutation.isPending,
|
||||
logout,
|
||||
logoutMutation.isPending,
|
||||
refreshAccessToken,
|
||||
refreshSession,
|
||||
refreshSessionMutation.isPending,
|
||||
restoreSession,
|
||||
selectActiveCompany,
|
||||
updateActiveCompany,
|
||||
updateAccount,
|
||||
session,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<IdentityAuthSessionContext.Provider value={value}>
|
||||
{children}
|
||||
</IdentityAuthSessionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useIdentityAuthSession() {
|
||||
const context = useContext(IdentityAuthSessionContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useIdentityAuthSession must be used within a IdentityAuthSessionProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
import { useDataSource } from "@erp/core/hooks";
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
import type { LoginForm } from "../login/entities";
|
||||
import {
|
||||
AvailableCompaniesResponseDTOSchema,
|
||||
activeCompanyStorage,
|
||||
authTokenStorage,
|
||||
findAvailableCompanies,
|
||||
mapAvailableCompany,
|
||||
mapCurrentSessionToAuthSession,
|
||||
resolveAuthenticatedRedirectPath,
|
||||
resolveCompanySession,
|
||||
useLoginMutation,
|
||||
useLogoutMutation,
|
||||
useRefreshSessionMutation,
|
||||
} from "../shared";
|
||||
import { getCurrentSession } from "../shared/api";
|
||||
import type { ActiveCompany, AuthSession, CompanySession } from "../shared/entities";
|
||||
|
||||
import {
|
||||
resetIdentityAuthSessionRuntimeHandlers,
|
||||
setIdentityAuthSessionRuntimeHandlers,
|
||||
} from "./identity-auth-session.runtime";
|
||||
|
||||
export interface IdentityAuthSessionContextValue {
|
||||
session: AuthSession | null;
|
||||
account: AuthSession["account"] | null;
|
||||
companySession: CompanySession;
|
||||
availableCompanies: ActiveCompany[];
|
||||
activeCompany: ActiveCompany | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
login: (params: LoginForm) => Promise<AuthSession>;
|
||||
logout: () => Promise<void>;
|
||||
restoreSession: () => Promise<AuthSession | null>;
|
||||
refreshSession: () => Promise<AuthSession | null>;
|
||||
refreshAccessToken: () => Promise<string | null>;
|
||||
selectActiveCompany: (companyId: string) => ActiveCompany | null;
|
||||
changeActiveCompany: (companyId: string) => Promise<ActiveCompany | null>;
|
||||
updateActiveCompany: (company: {
|
||||
id: string;
|
||||
legalName: string;
|
||||
tradeName: string | null;
|
||||
}) => void;
|
||||
updateAccount: (account: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
languageCode: string;
|
||||
}) => void;
|
||||
getAuthenticatedRedirectPath: (params?: {
|
||||
returnTo?: string | null;
|
||||
fallbackPath?: string;
|
||||
}) => string;
|
||||
clearSession: () => void;
|
||||
}
|
||||
|
||||
const IdentityAuthSessionContext = createContext<IdentityAuthSessionContextValue | undefined>(
|
||||
undefined
|
||||
);
|
||||
|
||||
const EMPTY_COMPANY_SESSION: CompanySession = {
|
||||
availableCompanies: [],
|
||||
activeCompany: null,
|
||||
};
|
||||
|
||||
export function IdentityAuthSessionProvider({ children }: { children: React.ReactNode }) {
|
||||
const dataSource = useDataSource();
|
||||
const loginMutation = useLoginMutation();
|
||||
const logoutMutation = useLogoutMutation();
|
||||
const refreshSessionMutation = useRefreshSessionMutation();
|
||||
|
||||
const [session, setSession] = useState<AuthSession | null>(null);
|
||||
const [companySession, setCompanySession] = useState<CompanySession>(EMPTY_COMPANY_SESSION);
|
||||
const [isRestoringSession, setIsRestoringSession] = useState(authTokenStorage.hasAccessToken());
|
||||
|
||||
const clearSession = useCallback(() => {
|
||||
authTokenStorage.clear();
|
||||
activeCompanyStorage.clear();
|
||||
setSession(null);
|
||||
setCompanySession(EMPTY_COMPANY_SESSION);
|
||||
}, []);
|
||||
|
||||
const hydrateCompanySession = useCallback(
|
||||
async (accessToken: string) => {
|
||||
const response = await findAvailableCompanies(dataSource, accessToken);
|
||||
const parsedResponse = AvailableCompaniesResponseDTOSchema.safeParse(response);
|
||||
|
||||
if (!parsedResponse.success) {
|
||||
throw new Error("Invalid available companies response");
|
||||
}
|
||||
|
||||
const availableCompanies = parsedResponse.data.companies.map(mapAvailableCompany);
|
||||
const nextCompanySession = resolveCompanySession({
|
||||
companies: availableCompanies,
|
||||
persistedActiveCompanyId: activeCompanyStorage.getActiveCompanyId(),
|
||||
});
|
||||
|
||||
if (nextCompanySession.activeCompany) {
|
||||
activeCompanyStorage.setActiveCompanyId(nextCompanySession.activeCompany.id);
|
||||
} else {
|
||||
activeCompanyStorage.clear();
|
||||
}
|
||||
|
||||
setCompanySession(nextCompanySession);
|
||||
|
||||
return nextCompanySession;
|
||||
},
|
||||
[dataSource]
|
||||
);
|
||||
|
||||
const restoreSession = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
if (!tokens.accessToken) {
|
||||
clearSession();
|
||||
setIsRestoringSession(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsRestoringSession(true);
|
||||
|
||||
try {
|
||||
const response = await getCurrentSession(dataSource, tokens.accessToken);
|
||||
const restoredSession = mapCurrentSessionToAuthSession(response, {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken ?? "",
|
||||
});
|
||||
|
||||
await hydrateCompanySession(tokens.accessToken);
|
||||
setSession(restoredSession);
|
||||
|
||||
return restoredSession;
|
||||
} catch {
|
||||
clearSession();
|
||||
return null;
|
||||
} finally {
|
||||
setIsRestoringSession(false);
|
||||
}
|
||||
}, [clearSession, dataSource, hydrateCompanySession]);
|
||||
|
||||
const login = useCallback(
|
||||
async (params: LoginForm) => {
|
||||
const nextSession = await loginMutation.mutateAsync(params);
|
||||
|
||||
authTokenStorage.setTokens({
|
||||
accessToken: nextSession.accessToken,
|
||||
refreshToken: nextSession.refreshToken,
|
||||
});
|
||||
|
||||
activeCompanyStorage.clear();
|
||||
await hydrateCompanySession(nextSession.accessToken);
|
||||
setSession(nextSession);
|
||||
|
||||
return nextSession;
|
||||
},
|
||||
[hydrateCompanySession, loginMutation]
|
||||
);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
try {
|
||||
if (tokens.accessToken) {
|
||||
await logoutMutation.mutateAsync({
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
clearSession();
|
||||
}
|
||||
}, [clearSession, logoutMutation]);
|
||||
|
||||
const refreshSession = useCallback(async () => {
|
||||
const tokens = authTokenStorage.getTokens();
|
||||
|
||||
if (!tokens.refreshToken) {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const refreshedTokens = await refreshSessionMutation.mutateAsync({
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
|
||||
authTokenStorage.setTokens(refreshedTokens);
|
||||
await hydrateCompanySession(refreshedTokens.accessToken);
|
||||
|
||||
if (session) {
|
||||
const refreshedSession: AuthSession = {
|
||||
...session,
|
||||
accessToken: refreshedTokens.accessToken,
|
||||
refreshToken: refreshedTokens.refreshToken,
|
||||
};
|
||||
|
||||
setSession(refreshedSession);
|
||||
return refreshedSession;
|
||||
}
|
||||
|
||||
const currentSessionResponse = await getCurrentSession(
|
||||
dataSource,
|
||||
refreshedTokens.accessToken
|
||||
);
|
||||
const refreshedSession = mapCurrentSessionToAuthSession(currentSessionResponse, {
|
||||
accessToken: refreshedTokens.accessToken,
|
||||
refreshToken: refreshedTokens.refreshToken,
|
||||
});
|
||||
|
||||
setSession(refreshedSession);
|
||||
|
||||
return refreshedSession;
|
||||
} catch {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
}, [clearSession, dataSource, hydrateCompanySession, refreshSessionMutation, session]);
|
||||
|
||||
const refreshAccessToken = useCallback(async () => {
|
||||
const refreshedSession = await refreshSession();
|
||||
return refreshedSession?.accessToken ?? null;
|
||||
}, [refreshSession]);
|
||||
|
||||
const selectActiveCompany = useCallback(
|
||||
(companyId: string) => {
|
||||
if (companySession.activeCompany?.id === companyId) {
|
||||
return companySession.activeCompany;
|
||||
}
|
||||
|
||||
const nextActiveCompany =
|
||||
companySession.availableCompanies.find((company) => company.id === companyId) ?? null;
|
||||
|
||||
if (!nextActiveCompany) {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeCompanyStorage.setActiveCompanyId(nextActiveCompany.id);
|
||||
setCompanySession((currentSession) => ({
|
||||
...currentSession,
|
||||
activeCompany: nextActiveCompany,
|
||||
}));
|
||||
|
||||
return nextActiveCompany;
|
||||
},
|
||||
[companySession.activeCompany, companySession.availableCompanies]
|
||||
);
|
||||
|
||||
const changeActiveCompany = useCallback(
|
||||
async (companyId: string) => {
|
||||
return selectActiveCompany(companyId);
|
||||
},
|
||||
[selectActiveCompany]
|
||||
);
|
||||
|
||||
const updateActiveCompany = useCallback(
|
||||
(company: { id: string; legalName: string; tradeName: string | null }) => {
|
||||
setCompanySession((currentSession) => {
|
||||
const availableCompanies = currentSession.availableCompanies.map((item) =>
|
||||
item.id === company.id
|
||||
? {
|
||||
...item,
|
||||
legalName: company.legalName,
|
||||
tradeName: company.tradeName,
|
||||
}
|
||||
: item
|
||||
);
|
||||
|
||||
const activeCompany =
|
||||
currentSession.activeCompany?.id === company.id
|
||||
? {
|
||||
...currentSession.activeCompany,
|
||||
legalName: company.legalName,
|
||||
tradeName: company.tradeName,
|
||||
}
|
||||
: currentSession.activeCompany;
|
||||
|
||||
return {
|
||||
availableCompanies,
|
||||
activeCompany,
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const updateAccount = useCallback(
|
||||
(account: {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatarUrl: string | null;
|
||||
languageCode: string;
|
||||
}) => {
|
||||
setSession((currentSession) => {
|
||||
if (!currentSession || currentSession.account.id !== account.id) {
|
||||
return currentSession;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentSession,
|
||||
account: {
|
||||
...currentSession.account,
|
||||
email: account.email,
|
||||
name: account.name,
|
||||
avatarUrl: account.avatarUrl,
|
||||
languageCode: account.languageCode,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const getAuthenticatedRedirectPath = useCallback(
|
||||
(params?: { returnTo?: string | null; fallbackPath?: string }) => {
|
||||
return resolveAuthenticatedRedirectPath({
|
||||
companySession,
|
||||
fallbackPath: params?.fallbackPath ?? "/proformas",
|
||||
returnTo: params?.returnTo,
|
||||
});
|
||||
},
|
||||
[companySession]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
restoreSession();
|
||||
}, [restoreSession]);
|
||||
|
||||
useEffect(() => {
|
||||
setIdentityAuthSessionRuntimeHandlers({
|
||||
clearSession,
|
||||
refreshAccessToken,
|
||||
});
|
||||
|
||||
return () => {
|
||||
resetIdentityAuthSessionRuntimeHandlers();
|
||||
};
|
||||
}, [clearSession, refreshAccessToken]);
|
||||
|
||||
const value = useMemo<IdentityAuthSessionContextValue>(
|
||||
() => ({
|
||||
session,
|
||||
account: session?.account ?? null,
|
||||
companySession,
|
||||
availableCompanies: companySession.availableCompanies,
|
||||
activeCompany: companySession.activeCompany,
|
||||
isAuthenticated: Boolean(session),
|
||||
isLoading:
|
||||
isRestoringSession ||
|
||||
loginMutation.isPending ||
|
||||
logoutMutation.isPending ||
|
||||
refreshSessionMutation.isPending,
|
||||
login,
|
||||
logout,
|
||||
restoreSession,
|
||||
refreshSession,
|
||||
refreshAccessToken,
|
||||
selectActiveCompany,
|
||||
changeActiveCompany,
|
||||
updateActiveCompany,
|
||||
updateAccount,
|
||||
getAuthenticatedRedirectPath,
|
||||
clearSession,
|
||||
}),
|
||||
[
|
||||
changeActiveCompany,
|
||||
clearSession,
|
||||
companySession,
|
||||
getAuthenticatedRedirectPath,
|
||||
isRestoringSession,
|
||||
login,
|
||||
loginMutation.isPending,
|
||||
logout,
|
||||
logoutMutation.isPending,
|
||||
refreshAccessToken,
|
||||
refreshSession,
|
||||
refreshSessionMutation.isPending,
|
||||
restoreSession,
|
||||
selectActiveCompany,
|
||||
updateActiveCompany,
|
||||
updateAccount,
|
||||
session,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<IdentityAuthSessionContext.Provider value={value}>
|
||||
{children}
|
||||
</IdentityAuthSessionContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useIdentityAuthSession() {
|
||||
const context = useContext(IdentityAuthSessionContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useIdentityAuthSession must be used within a IdentityAuthSessionProvider");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user