This commit is contained in:
Stefan Pejcic
2024-11-07 19:03:37 +01:00
parent c6df945ed5
commit 09f9f9502d
2472 changed files with 620417 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
export { usePermissions } from "./usePermissions";
export { useGetIdentity } from "./useGetIdentity";
export { useLogout } from "./useLogout";
export { useLogin } from "./useLogin";
export { useRegister } from "./useRegister";
export { useForgotPassword } from "./useForgotPassword";
export { useUpdatePassword } from "./useUpdatePassword";
export { useAuthenticated, useIsAuthenticated } from "./useIsAuthenticated";
export { useCheckError, useOnError } from "./useOnError";
export { useIsExistAuthentication } from "./useIsExistAuthentication";
export { useInvalidateAuthStore } from "./useInvalidateAuthStore";

View File

@@ -0,0 +1,615 @@
import { renderHook, waitFor } from "@testing-library/react";
import {
TestWrapper,
act,
mockLegacyRouterProvider,
mockRouterProvider,
queryClient,
} from "@test";
import { useForgotPassword } from ".";
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useForgotPassword Hook", () => {
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing email") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed forgot password", async () => {
const { result } = renderHook(
() => useForgotPassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
forgotPassword: ({ email }) => {
if (email) {
return Promise.resolve();
}
return Promise.reject(new Error("Missing email"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
});
it("succeed and redirect forgot password", async () => {
const replaceMock = jest.fn();
const legacyRouterProvider = {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: replaceMock,
push: jest.fn(),
}),
};
const { result } = renderHook(
() => useForgotPassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyRouterProvider,
legacyAuthProvider: {
login: () => Promise.resolve(),
forgotPassword: ({ email }) => {
if (email) {
return Promise.resolve("/redirect-path");
}
return Promise.reject(new Error("Missing email"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
expect(result.current.data).toBe("/redirect-path");
expect(replaceMock).toBeCalledWith("/redirect-path");
});
});
it("succeed and redirect forgot password with routerProvider", async () => {
const mockFn = jest.fn();
const { result } = renderHook(
() => useForgotPassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
routerProvider: mockRouterProvider({
fns: {
go: () => mockFn,
},
}),
legacyAuthProvider: {
login: () => Promise.resolve(),
forgotPassword: ({ email }) => {
if (email) {
return Promise.resolve("/redirect-path");
}
return Promise.reject(new Error("Missing email"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
expect(result.current.data).toBe("/redirect-path");
expect(mockFn).toBeCalledWith({
to: "/redirect-path",
type: "replace",
});
});
});
it("fail forgot password", async () => {
const { result } = renderHook(
() => useForgotPassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
forgotPassword: () => Promise.reject(new Error("Missing email")),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
const { error } = result.current ?? { error: undefined };
expect(error).toEqual(new Error("Missing email"));
});
});
describe("useForgotPassword Hook", () => {
const mockAuthProvider = {
login: () =>
Promise.resolve({
success: true,
}),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
};
beforeEach(() => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing email") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed forgot password", async () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: (params: any) => {
if (!params?.["email"]) {
return Promise.resolve({ success: false });
}
return Promise.resolve({ success: true });
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.data?.success).toBeTruthy();
});
});
it("succeed and redirect forgot password", async () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: (params: any) => {
if (!params?.["email"]) {
return Promise.resolve({ success: false });
}
return Promise.resolve({
success: true,
redirectTo: "/rediect-path",
});
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.data?.success).toBeTruthy();
expect(result.current.data?.redirectTo).toBe("/rediect-path");
});
});
it("succeed and redirect forgot password with legacyRouterProvider", async () => {
const mockFn = jest.fn();
const legacyRouterProvider = {
...mockLegacyRouterProvider(),
useHistory: () => ({
replace: mockFn,
}),
};
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: (params: any) => {
if (!params?.["email"]) {
return Promise.resolve({ success: false });
}
return Promise.resolve({
success: true,
redirectTo: "/rediect-path",
});
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(result.current.data?.success).toBeTruthy();
expect(result.current.data?.redirectTo).toBe("/rediect-path");
});
});
it("fail forgot password", async () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: () =>
Promise.resolve({
success: false,
error: new Error("Missing email"),
}),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(result.current.data).toEqual({
success: false,
error: new Error("Missing email"),
});
});
});
it("should open notification when has error is true", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
forgotPassword: () =>
Promise.resolve({
success: false,
error: new Error("Missing email"),
}),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "forgot-password-error",
type: "error",
message: "Error",
description: "Missing email",
});
});
});
it("should open notification when has success is false, error is undefined", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
forgotPassword: () =>
Promise.resolve({
success: false,
}),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "forgot-password-error",
type: "error",
message: "Forgot Password Error",
description: "Error while resetting password",
});
});
});
it("should open notification when throw error", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
forgotPassword: () => {
throw new Error("Unhandled error");
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "forgot-password-error",
type: "error",
message: "Error",
description: "Unhandled error",
});
});
});
it("should override `mutationFn` with mutationOptions.mutationFn", async () => {
const forgotPasswordMock = jest.fn().mockResolvedValue({ data: {} });
const mutationFnMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useForgotPassword({
mutationOptions: {
// mutationFn is omitted in types. So we need to use @ts-ignore test it.
// @ts-ignore
mutationFn: mutationFnMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: forgotPasswordMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(forgotPasswordMock).not.toBeCalled();
expect(mutationFnMock).toBeCalled();
});
it("should override `mutationKey` with `mutationOptions.mutationKey`", async () => {
const forgotPasswordMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useForgotPassword({
mutationOptions: {
mutationKey: ["foo", "bar"],
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
forgotPassword: forgotPasswordMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(
queryClient.getMutationCache().findAll({
mutationKey: ["foo", "bar"],
}),
).toHaveLength(1);
});
it("should open success notification when successNotification is passed", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
forgotPassword: () =>
Promise.resolve({
success: true,
successNotification: {
message: "Password reset successful",
description: "Your password has been successfully reset",
},
}),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({ email: "test@test.com" });
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "forgot-password-success",
type: "success",
message: "Password reset successful",
description: "Your password has been successfully reset",
});
});
});
});
// NOTE : Will be removed in v5
describe("useForgotPassword Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyForgotPasswordMock = jest.fn(() => Promise.resolve());
const forgotPasswordMock = jest.fn(() =>
Promise.resolve({
success: true,
}),
);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
forgotPassword: () => legacyForgotPasswordMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
forgotPassword: () => forgotPasswordMock(),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyForgotPasswordMock).not.toHaveBeenCalled();
expect(forgotPasswordMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyForgotPasswordMock = jest.fn(() => Promise.resolve());
const forgotPasswordMock = jest.fn(() =>
Promise.resolve({ success: true }),
);
const { result } = renderHook(
() => useForgotPassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
forgotPassword: () => legacyForgotPasswordMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
forgotPassword: () => forgotPasswordMock(),
},
}),
},
);
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyForgotPasswordMock).toHaveBeenCalled();
expect(forgotPasswordMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,222 @@
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseMutationOptions,
type UseMutationResult,
useMutation,
} from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import {
useGo,
useKeys,
useNavigation,
useNotification,
useRouterType,
} from "@hooks";
import type {
AuthActionResponse,
SuccessNotificationResponse,
TForgotPasswordData,
} from "../../../contexts/auth/types";
import type { RefineError } from "../../../contexts/data/types";
import type { OpenNotificationParams } from "../../../contexts/notification/types";
export type UseForgotPasswordLegacyProps<TVariables> = {
v3LegacyAuthProviderCompatible: true;
mutationOptions?: Omit<
UseMutationOptions<
TForgotPasswordData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn" | "onError" | "onSuccess"
>;
};
export type UseForgotPasswordProps<TVariables> = {
v3LegacyAuthProviderCompatible?: false;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseForgotPasswordCombinedProps<TVariables> = {
v3LegacyAuthProviderCompatible: boolean;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse | TForgotPasswordData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseForgotPasswordLegacyReturnType<TVariables> = UseMutationResult<
TForgotPasswordData,
Error | RefineError,
TVariables,
unknown
>;
export type UseForgotPasswordReturnType<TVariables> = UseMutationResult<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>;
export type UseForgotPasswordCombinedReturnType<TVariables> = UseMutationResult<
AuthActionResponse | TForgotPasswordData,
Error | RefineError,
TVariables,
unknown
>;
export function useForgotPassword<TVariables = {}>(
props: UseForgotPasswordLegacyProps<TVariables>,
): UseForgotPasswordLegacyReturnType<TVariables>;
export function useForgotPassword<TVariables = {}>(
props?: UseForgotPasswordProps<TVariables>,
): UseForgotPasswordReturnType<TVariables>;
export function useForgotPassword<TVariables = {}>(
props?: UseForgotPasswordCombinedProps<TVariables>,
): UseForgotPasswordCombinedReturnType<TVariables>;
/**
* `useForgotPassword` calls `forgotPassword` method from {@link https://refine.dev/docs/api-reference/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useForgotPassword} for more details.
*
* @typeParam TData - Result data of the query
* @typeParam TVariables - Values for mutation function. default `{}`
*
*/
export function useForgotPassword<TVariables = {}>({
v3LegacyAuthProviderCompatible,
mutationOptions,
}:
| UseForgotPasswordProps<TVariables>
| UseForgotPasswordLegacyProps<TVariables> = {}):
| UseForgotPasswordReturnType<TVariables>
| UseForgotPasswordLegacyReturnType<TVariables> {
const routerType = useRouterType();
const go = useGo();
const { replace } = useNavigation();
const {
forgotPassword: v3LegacyAuthProviderCompatibleForgotPasswordFromContext,
} = useLegacyAuthContext();
const { forgotPassword: forgotPasswordFromContext } =
useAuthBindingsContext();
const { close, open } = useNotification();
const { keys, preferLegacyKeys } = useKeys();
const mutation = useMutation<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: keys().auth().action("forgotPassword").get(preferLegacyKeys),
mutationFn: forgotPasswordFromContext,
onSuccess: ({ success, redirectTo, error, successNotification }) => {
if (success) {
close?.("forgot-password-error");
if (successNotification) {
open?.(buildSuccessNotification(successNotification));
}
}
if (error || !success) {
open?.(buildNotification(error));
}
if (redirectTo) {
if (routerType === "legacy") {
replace(redirectTo);
} else {
go({ to: redirectTo, type: "replace" });
}
}
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions?.meta),
...getXRay("useForgotPassword", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation<
TForgotPasswordData,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: [
...keys().auth().action("forgotPassword").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: v3LegacyAuthProviderCompatibleForgotPasswordFromContext,
onSuccess: (redirectPathFromAuth) => {
if (redirectPathFromAuth !== false) {
if (redirectPathFromAuth) {
if (routerType === "legacy") {
replace(redirectPathFromAuth);
} else {
go({ to: redirectPathFromAuth, type: "replace" });
}
}
}
close?.("forgot-password-error");
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible ? mutationOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? mutationOptions?.meta : {}),
...getXRay("useForgotPassword", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
const buildNotification = (
error?: Error | RefineError,
): OpenNotificationParams => {
return {
message: error?.name || "Forgot Password Error",
description: error?.message || "Error while resetting password",
key: "forgot-password-error",
type: "error",
};
};
const buildSuccessNotification = (
successNotification: SuccessNotificationResponse,
): OpenNotificationParams => {
return {
message: successNotification.message,
description: successNotification.description,
key: "forgot-password-success",
type: "success",
};
};

View File

@@ -0,0 +1,286 @@
import { renderHook, waitFor } from "@testing-library/react";
import { TestWrapper, queryClient } from "@test";
import { useGetIdentity } from "./";
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useGetIdentity Hook", () => {
it("returns object useGetIdentity", async () => {
const { result } = renderHook(
() => useGetIdentity({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current?.data).toEqual({ id: 1 });
});
it("throw error useGetIdentity", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Not Authenticated") return;
console.warn(message);
});
const { result } = renderHook(
() => useGetIdentity({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () =>
Promise.reject(new Error("Not Authenticated")),
},
}),
},
);
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
expect(result.current?.error).toEqual(new Error("Not Authenticated"));
});
it("throw error useGetIdentity undefined", async () => {
const { result } = renderHook(
() => useGetIdentity({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
},
}),
},
);
await waitFor(() => {
expect(!result.current.isFetching).toBeTruthy();
});
expect(result.current.status).toEqual("loading");
expect(result.current.data).not.toBeDefined();
});
});
describe("useGetIdentity Hook", () => {
const mockAuthProvider = {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
};
it("returns object useGetIdentity", async () => {
const { result } = renderHook(
() => useGetIdentity({ v3LegacyAuthProviderCompatible: false }),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
getIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.data).toBeTruthy();
});
expect(result.current?.data).toEqual({ id: 1 });
});
it("return error useGetIdentity", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Not Authenticated") return;
console.warn(message);
});
const { result } = renderHook(() => useGetIdentity(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
getIdentity: () => Promise.resolve(new Error("Not Authenticated")),
},
}),
});
await waitFor(() => {
expect(result.current.data).toBeTruthy();
});
expect(result.current?.data).toEqual(new Error("Not Authenticated"));
});
it("throw error useGetIdentity undefined", async () => {
const { result } = renderHook(() => useGetIdentity(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
},
}),
});
await waitFor(() => {
expect(!result.current.isFetching).toBeTruthy();
});
expect(result.current.status).toEqual("loading");
expect(result.current.data).not.toBeDefined();
});
it("should override `queryKey` with `queryOptions.queryKey`", async () => {
const getIdentityMock = jest.fn().mockResolvedValue({
data: { id: 1, title: "foo" },
});
const { result } = renderHook(
() =>
useGetIdentity({
queryOptions: {
queryKey: ["foo", "bar"],
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
getIdentity: getIdentityMock,
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(
queryClient.getQueryCache().findAll({
queryKey: ["foo", "bar"],
}),
).toHaveLength(1);
});
it("should override `queryFn` with `queryOptions.queryFn`", async () => {
const getIdentityMock = jest.fn().mockResolvedValue({
data: [{ id: 1, title: "foo" }],
});
const queryFnMock = jest.fn().mockResolvedValue({
data: [{ id: 1, title: "foo" }],
});
const { result } = renderHook(
() =>
useGetIdentity({
queryOptions: {
queryFn: queryFnMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
getIdentity: getIdentityMock,
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(getIdentityMock).not.toBeCalled();
expect(queryFnMock).toBeCalled();
});
});
// NOTE : Will be removed in v5
describe("useGetIdentity Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyGetUserIdentityMock = jest.fn(() => Promise.resolve());
const getIdentitiyMock = jest.fn(() => Promise.resolve());
const { result } = renderHook(() => useGetIdentity(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getUserIdentity: () => legacyGetUserIdentityMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getIdentity: () => getIdentitiyMock(),
},
}),
});
await waitFor(() => {
expect(!result.current.isLoading).toBeFalsy();
});
expect(legacyGetUserIdentityMock).not.toHaveBeenCalled();
expect(getIdentitiyMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyGetUserIdentityMock = jest.fn(() => Promise.resolve());
const getIdentitiyMock = jest.fn(() => Promise.resolve());
const { result } = renderHook(
() => useGetIdentity({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getUserIdentity: () => legacyGetUserIdentityMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getIdentity: () => getIdentitiyMock(),
},
}),
},
);
await waitFor(() => {
expect(!result.current.isLoading).toBeFalsy();
});
expect(legacyGetUserIdentityMock).toHaveBeenCalled();
expect(getIdentitiyMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,104 @@
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseQueryOptions,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import { useKeys } from "@hooks/useKeys";
import type { IdentityResponse } from "../../../contexts/auth/types";
export type UseGetIdentityLegacyProps<TData> = {
v3LegacyAuthProviderCompatible: true;
queryOptions?: UseQueryOptions<TData>;
};
export type UseGetIdentityProps<TData = IdentityResponse> = {
v3LegacyAuthProviderCompatible?: false;
queryOptions?: UseQueryOptions<TData>;
};
export type UseGetIdentityCombinedProps<TData = any> = {
v3LegacyAuthProviderCompatible: boolean;
queryOptions?: UseQueryOptions<TData> | UseQueryOptions<IdentityResponse>;
};
export type UseGetIdentityLegacyReturnType<TData> = UseQueryResult<
TData,
unknown
>;
export type UseGetIdentityReturnType<TData = IdentityResponse> = UseQueryResult<
TData,
unknown
>;
export type UsePermissionsCombinedReturnType<TData = any> =
| UseQueryResult<TData, unknown>
| UseQueryResult<IdentityResponse, unknown>;
export function useGetIdentity<TData = any>(
props: UseGetIdentityLegacyProps<TData>,
): UseGetIdentityLegacyReturnType<TData>;
export function useGetIdentity<TData = IdentityResponse>(
props?: UseGetIdentityProps<TData>,
): UseGetIdentityReturnType<TData>;
export function useGetIdentity<TData = any>(
props?: UseGetIdentityCombinedProps<TData>,
): UsePermissionsCombinedReturnType<TData>;
/**
* `useGetIdentity` calls the `getUserIdentity` method from the {@link https://refine.dev/docs/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useGetIdentity} for more details.
*
* @typeParam TData - Result data of the query
*
*/
export function useGetIdentity<TData = any>({
v3LegacyAuthProviderCompatible = false,
queryOptions,
}: UseGetIdentityProps<TData> | UseGetIdentityLegacyProps<TData> = {}):
| UseGetIdentityReturnType<TData>
| UseGetIdentityLegacyReturnType<TData> {
const { getUserIdentity: legacyGetUserIdentity } = useLegacyAuthContext();
const { getIdentity } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const queryResponse = useQuery<TData>({
queryKey: keys().auth().action("identity").get(preferLegacyKeys),
// Enabled check for `getIdentity` is enough to be sure that it's defined in the query function but TS is not smart enough to know that.
queryFn:
(getIdentity as (params?: unknown) => Promise<TData>) ??
(() => Promise.resolve({})),
enabled: !v3LegacyAuthProviderCompatible && !!getIdentity,
retry: false,
...(v3LegacyAuthProviderCompatible === true ? {} : queryOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : queryOptions?.meta),
...getXRay("useGetIdentity", preferLegacyKeys),
},
});
const legacyQueryResponse = useQuery<TData>({
queryKey: [
...keys().auth().action("identity").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
// Enabled check for `getUserIdentity` is enough to be sure that it's defined in the query function but TS is not smart enough to know that.
queryFn: legacyGetUserIdentity ?? (() => Promise.resolve({})),
enabled: v3LegacyAuthProviderCompatible && !!legacyGetUserIdentity,
retry: false,
...(v3LegacyAuthProviderCompatible ? queryOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? queryOptions?.meta : {}),
...getXRay("useGetIdentity", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible ? legacyQueryResponse : queryResponse;
}

View File

@@ -0,0 +1,19 @@
import { useKeys } from "@hooks/useKeys";
import { useQueryClient } from "@tanstack/react-query";
export const useInvalidateAuthStore = () => {
const queryClient = useQueryClient();
const { keys, preferLegacyKeys } = useKeys();
const invalidate = async () => {
await Promise.all(
(["check", "identity", "permissions"] as const).map((action) =>
queryClient.invalidateQueries(
keys().auth().action(action).get(preferLegacyKeys),
),
),
);
};
return invalidate;
};

View File

@@ -0,0 +1,307 @@
import { renderHook, waitFor } from "@testing-library/react";
import { TestWrapper, mockLegacyRouterProvider } from "@test";
import { useIsAuthenticated } from ".";
import * as authContext from "../../../contexts/auth";
const mockFn = jest.fn();
const legacyRouterProvider = {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockFn,
push: jest.fn(),
}),
};
describe("useIsAuthenticated Hook", () => {
it("returns authenticated true", async () => {
const { result } = renderHook(() => useIsAuthenticated(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: false }),
check: () =>
Promise.resolve({
authenticated: true,
}),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: false }),
},
}),
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
await waitFor(() => {
expect(result.current.data?.authenticated).toBeTruthy();
});
});
it("returns authenticated false and called checkError", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Not Authenticated") return;
console.warn(message);
});
const { result } = renderHook(() => useIsAuthenticated(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: false }),
check: () =>
Promise.resolve({
authenticated: false,
error: new Error("Not Authenticated"),
}),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: false }),
},
}),
});
await waitFor(() => {
expect(result.current.data?.error).toBeTruthy();
});
});
it("returns authenticated false and called checkError with custom redirect path", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.redirectPath === "/custom-url") return;
console.warn(message);
});
const checkErrorMock = jest.fn();
const { result } = renderHook(() => useIsAuthenticated(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: false }),
check: () =>
Promise.resolve({
authenticated: false,
redirectTo: "/custom-url",
error: new Error("Not Authenticated"),
}),
onError: checkErrorMock,
logout: () => Promise.resolve({ success: false }),
},
legacyRouterProvider,
}),
});
await waitFor(() => {
expect(result.current.data?.error).toBeTruthy();
});
});
it("should resolve {} if no authProvider is provided", async () => {
jest.spyOn(authContext, "useAuthBindingsContext").mockReturnValue({
...jest.requireActual("../../../contexts/auth"),
check: undefined,
});
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: false }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: false }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({});
});
});
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useIsAuthenticated Hook", () => {
it("returns authenticated true", async () => {
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
});
it("returns authenticated false and called checkError", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Not Authenticated") return;
console.warn(message);
});
const checkErrorMock = jest.fn();
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.reject(new Error("Not Authenticated")),
checkError: checkErrorMock,
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
});
it("returns authenticated false and called checkError with custom redirect path", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.redirectPath === "/custom-url") return;
console.warn(message);
});
const checkErrorMock = jest.fn();
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.reject({ redirectPath: "/custom-url" }),
checkError: checkErrorMock,
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
});
it("should resolve {} if no authProvider is provided", async () => {
jest.spyOn(authContext, "useLegacyAuthContext").mockReturnValue({
...jest.requireActual("../../../contexts/auth"),
checkAuth: undefined,
});
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: undefined,
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({});
});
});
// NOTE : Will be removed in v5
describe("useIsAuthenticated Hook authProvider selection", () => {
beforeEach(() => {
jest.restoreAllMocks();
});
it("selects new authProvider", async () => {
const legacyCheckAuthMock = jest.fn(() => Promise.resolve());
const checkMock = jest.fn(() =>
Promise.resolve({
authenticated: true,
}),
);
const { result } = renderHook(() => useIsAuthenticated(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => legacyCheckAuthMock(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => checkMock(),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
},
}),
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyCheckAuthMock).not.toHaveBeenCalled();
expect(checkMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyCheckAuthMock = jest.fn(() => Promise.resolve());
const checkMock = jest.fn(() => Promise.resolve({ authenticated: true }));
const { result } = renderHook(
() => useIsAuthenticated({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => legacyCheckAuthMock(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => checkMock(),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
},
}),
},
);
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyCheckAuthMock).toHaveBeenCalled();
expect(checkMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,93 @@
import { getXRay } from "@refinedev/devtools-internal";
import { type UseQueryResult, useQuery } from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import { useKeys } from "@hooks/useKeys";
import type { CheckResponse } from "../../../contexts/auth/types";
export type UseIsAuthenticatedLegacyProps = {
v3LegacyAuthProviderCompatible: true;
params?: any;
};
export type UseIsAuthenticatedProps = {
v3LegacyAuthProviderCompatible?: false;
params?: any;
};
export type UseIsAuthenticatedCombinedProps = {
v3LegacyAuthProviderCompatible: boolean;
params?: any;
};
export type UseIsAuthenticatedLegacyReturnType = UseQueryResult<any, any>;
export type UseIsAuthenticatedReturnType = UseQueryResult<CheckResponse, any>;
export type UseIsAuthenticatedCombinedReturnType = UseQueryResult<
CheckResponse | any,
any
>;
export function useIsAuthenticated(
props: UseIsAuthenticatedLegacyProps,
): UseIsAuthenticatedLegacyReturnType;
export function useIsAuthenticated(
props?: UseIsAuthenticatedProps,
): UseIsAuthenticatedReturnType;
export function useIsAuthenticated(
props?: UseIsAuthenticatedCombinedProps,
): UseIsAuthenticatedCombinedReturnType;
/**
* `useIsAuthenticated` calls the `checkAuth` method from the {@link https://refine.dev/docs/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useAuthenticated} for more details.
*/
export function useIsAuthenticated({
v3LegacyAuthProviderCompatible = false,
params,
}: UseIsAuthenticatedProps | UseIsAuthenticatedLegacyProps = {}):
| UseIsAuthenticatedReturnType
| UseIsAuthenticatedLegacyReturnType {
const { checkAuth } = useLegacyAuthContext();
const { check } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const queryResponse = useQuery({
queryKey: keys()
.auth()
.action("check")
.params(params)
.get(preferLegacyKeys),
queryFn: async () => (await check?.(params)) ?? {},
retry: false,
enabled: !v3LegacyAuthProviderCompatible,
meta: {
...getXRay("useIsAuthenticated", preferLegacyKeys),
},
});
const legacyQueryResponse = useQuery({
queryKey: [
...keys().auth().action("check").params(params).get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
queryFn: async () => (await checkAuth?.(params)) ?? {},
retry: false,
enabled: v3LegacyAuthProviderCompatible,
meta: {
...getXRay("useIsAuthenticated", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible ? legacyQueryResponse : queryResponse;
}
/**
* @deprecated `useAuthenticated` is deprecated with refine@4, use `useIsAuthenticated` instead, however, we still support `useAuthenticated` for backward compatibility.
*/
export const useAuthenticated = useIsAuthenticated;

View File

@@ -0,0 +1,43 @@
import { TestWrapper } from "@test/index";
import { renderHook } from "@testing-library/react";
import { useIsExistAuthentication } from ".";
describe("useActiveAuthProvider", () => {
it("authProvider", async () => {
const { result } = renderHook(() => useIsExistAuthentication(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
},
}),
});
expect(result.current).toBeTruthy();
});
it("v3LegacyAuthProviderCompatible authProvider", async () => {
const { result } = renderHook(() => useIsExistAuthentication(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
logout: () => Promise.resolve(),
checkError: () => Promise.resolve(),
},
}),
});
expect(result.current).toBeTruthy();
});
it("returns false", async () => {
const { result } = renderHook(() => useIsExistAuthentication(), {
wrapper: TestWrapper({}),
});
expect(result.current).toBe(false);
});
});

View File

@@ -0,0 +1,12 @@
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
/**
* A hook that the UI uses
* @internal
*/
export const useIsExistAuthentication = (): boolean => {
const { isProvided: legacyIsProvided } = useLegacyAuthContext();
const { isProvided } = useAuthBindingsContext();
return Boolean(isProvided || legacyIsProvided);
};

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,261 @@
import React from "react";
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseMutationOptions,
type UseMutationResult,
useMutation,
} from "@tanstack/react-query";
import qs from "qs";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import {
useGo,
useKeys,
useNavigation,
useNotification,
useParsed,
useRouterContext,
useRouterType,
} from "@hooks";
import type {
AuthActionResponse,
SuccessNotificationResponse,
TLoginData,
} from "../../../contexts/auth/types";
import type { RefineError } from "../../../contexts/data/types";
import type { OpenNotificationParams } from "../../../contexts/notification/types";
import { useInvalidateAuthStore } from "../useInvalidateAuthStore";
export type UseLoginLegacyProps<TVariables> = {
v3LegacyAuthProviderCompatible: true;
mutationOptions?: Omit<
UseMutationOptions<TLoginData, Error | RefineError, TVariables, unknown>,
"mutationFn" | "onError" | "onSuccess"
>;
};
export type UseLoginProps<TVariables> = {
v3LegacyAuthProviderCompatible?: false;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseLoginCombinedProps<TVariables> = {
v3LegacyAuthProviderCompatible: boolean;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse | TLoginData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseLoginLegacyReturnType<TVariables> = UseMutationResult<
TLoginData,
Error | RefineError,
TVariables,
unknown
>;
export type UseLoginReturnType<TVariables> = UseMutationResult<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>;
export type UseLoginCombinedReturnType<TVariables> = UseMutationResult<
AuthActionResponse | TLoginData,
Error | RefineError,
TVariables,
unknown
>;
export function useLogin<TVariables = {}>(
props: UseLoginLegacyProps<TVariables>,
): UseLoginLegacyReturnType<TVariables>;
export function useLogin<TVariables = {}>(
props?: UseLoginProps<TVariables>,
): UseLoginReturnType<TVariables>;
export function useLogin<TVariables = {}>(
props?: UseLoginCombinedProps<TVariables>,
): UseLoginCombinedReturnType<TVariables>;
/**
* `useLogin` calls `login` method from {@link https://refine.dev/docs/api-reference/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useLogin} for more details.
*
* @typeParam TData - Result data of the query
* @typeParam TVariables - Values for mutation function. default `{}`
*
*/
export function useLogin<TVariables = {}>({
v3LegacyAuthProviderCompatible,
mutationOptions,
}: UseLoginProps<TVariables> | UseLoginLegacyProps<TVariables> = {}):
| UseLoginLegacyReturnType<TVariables>
| UseLoginReturnType<TVariables> {
const invalidateAuthStore = useInvalidateAuthStore();
const routerType = useRouterType();
const go = useGo();
const { replace } = useNavigation();
const parsed = useParsed();
const { useLocation } = useRouterContext();
const { search } = useLocation();
const { close, open } = useNotification();
const { login: legacyLoginFromContext } = useLegacyAuthContext();
const { login: loginFromContext } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const to = React.useMemo(() => {
if (routerType === "legacy") {
const legacySearch = qs.parse(search, {
ignoreQueryPrefix: true,
});
return legacySearch.to;
}
return parsed.params?.to;
}, [routerType, parsed.params, search]);
const mutation = useMutation<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: keys().auth().action("login").get(preferLegacyKeys),
mutationFn: loginFromContext,
onSuccess: async ({ success, redirectTo, error, successNotification }) => {
if (success) {
close?.("login-error");
if (successNotification) {
open?.(buildSuccessNotification(successNotification));
}
}
if (error || !success) {
open?.(buildNotification(error));
}
if (to && success) {
if (routerType === "legacy") {
replace(to as string);
} else {
go({ to: to as string, type: "replace" });
}
} else if (redirectTo) {
if (routerType === "legacy") {
replace(redirectTo);
} else {
go({ to: redirectTo, type: "replace" });
}
} else {
if (routerType === "legacy") {
replace("/");
}
}
await invalidateAuthStore();
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions?.meta),
...getXRay("useLogin", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation<
TLoginData,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: [
...keys().auth().action("login").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: legacyLoginFromContext,
onSuccess: async (redirectPathFromAuth) => {
if (to) {
replace(to as string);
}
if (redirectPathFromAuth !== false && !to) {
if (typeof redirectPathFromAuth === "string") {
if (routerType === "legacy") {
replace(redirectPathFromAuth);
} else {
go({ to: redirectPathFromAuth, type: "replace" });
}
} else {
if (routerType === "legacy") {
replace("/");
} else {
go({ to: "/", type: "replace" });
}
}
}
await invalidateAuthStore();
close?.("login-error");
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible ? mutationOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? mutationOptions?.meta : {}),
...getXRay("useLogin", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
const buildNotification = (
error?: Error | RefineError,
): OpenNotificationParams => {
return {
message: error?.name || "Login Error",
description: error?.message || "Invalid credentials",
key: "login-error",
type: "error",
};
};
const buildSuccessNotification = (
successNotification: SuccessNotificationResponse,
): OpenNotificationParams => {
return {
message: successNotification.message,
description: successNotification.description,
key: "login-success",
type: "success",
};
};

View File

@@ -0,0 +1,804 @@
import { renderHook, waitFor } from "@testing-library/react";
import { TestWrapper, act, mockRouterProvider, queryClient } from "@test";
import { useCheckError, useOnError } from "../useOnError";
import { useLogout } from "./";
const mockGo = jest.fn();
const routerProvider = mockRouterProvider({
fns: {
go: () => mockGo,
},
});
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useLogout Hook", () => {
beforeEach(() => {
mockGo.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (
message?.message === "Logout rejected" ||
typeof message === "undefined"
)
return;
console.warn(message);
});
});
it("logout and redirect to login", async () => {
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => {
return Promise.resolve();
},
getUserIdentity: () => Promise.resolve(),
},
routerProvider,
}),
},
);
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
return !result.current?.isLoading;
});
await act(async () => {
expect(mockGo).toBeCalledWith({ to: "/login" });
});
});
it("logout and not redirect", async () => {
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => {
return Promise.resolve(false);
},
getUserIdentity: () => Promise.resolve(),
},
routerProvider,
}),
},
);
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
return !result.current?.isLoading;
});
await act(async () => {
expect(mockGo).not.toBeCalled();
});
});
it("logout and redirect to custom path", async () => {
const { result } = renderHook(
() =>
useLogout<{ redirectPath: string }>({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: ({ redirectPath }) => {
return Promise.resolve(redirectPath);
},
getUserIdentity: () => Promise.resolve(),
},
routerProvider,
}),
},
);
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout({ redirectPath: "/custom-path" });
});
await waitFor(() => {
return result.current?.status === "success";
});
await act(async () => {
expect(mockGo).toBeCalledWith({ to: "/custom-path" });
});
});
it("logout rejected", async () => {
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.reject(new Error("Logout rejected")),
getUserIdentity: () => Promise.resolve(),
},
}),
},
);
const { mutateAsync: logout } = result.current;
await act(async () => {
try {
await logout();
} catch (error) {
expect(error).toEqual(new Error("Logout rejected"));
}
});
});
it("logout rejected with undefined error", async () => {
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.reject(),
getUserIdentity: () => Promise.resolve(),
},
}),
},
);
const { mutateAsync: logout } = result.current;
await act(async () => {
try {
await logout();
} catch (error) {
expect(error).not.toBeDefined();
}
});
});
it("logout and not redirect if check error rejected with false", async () => {
const { result } = renderHook(() => useCheckError(), {
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.reject(false),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve(),
},
}),
});
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockGo).toBeCalledTimes(0);
});
});
it("logout and not redirect if logout resolved false", async () => {
const { result } = renderHook(() => useCheckError(), {
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.reject(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(false),
getUserIdentity: () => Promise.resolve(),
},
routerProvider,
}),
});
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockGo).toBeCalledTimes(0);
});
});
it("logout and redirect to resolved custom path", async () => {
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.reject(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve("/custom-path"),
getUserIdentity: () => Promise.resolve(),
},
routerProvider,
}),
},
);
const { mutate: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
return result.current?.status === "success";
});
await act(async () => {
expect(mockGo).toBeCalledWith({ to: "/custom-path" });
});
});
});
describe("useLogout Hook", () => {
const mockAuthProvider = {
login: () => Promise.resolve({ success: false }),
check: () => Promise.resolve({ authenticated: false }),
onError: () => Promise.resolve({}),
};
beforeEach(() => {
mockGo.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (
message?.message === "Logout rejected" ||
typeof message === "undefined"
)
return;
console.warn(message);
});
});
it("logout and redirect to login", async () => {
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: true,
redirectTo: "/login",
});
},
},
routerProvider,
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
return !result.current?.isLoading;
});
await act(async () => {
expect(mockGo).toBeCalledWith({ to: "/login" });
});
});
it("logout and not redirect", async () => {
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: true,
});
},
},
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
return !result.current?.isLoading;
});
await act(async () => {
expect(mockGo).not.toBeCalled();
});
});
it("logout and redirect to custom path, pass redirect with hook's param", async () => {
const { result } = renderHook(() => useLogout<{ redirectPath: string }>(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: true,
});
},
},
routerProvider,
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout({ redirectPath: "/custom-path" });
});
await act(async () => {
expect(mockGo).toBeCalledWith(
expect.objectContaining({ to: "/custom-path" }),
);
});
});
it("logout and redirect to custom path, pass redirect with authProvider return value", async () => {
const { result } = renderHook(() => useLogout<{ redirectPath: string }>(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: true,
redirectTo: "/custom-path",
});
},
},
routerProvider,
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await act(async () => {
expect(mockGo).toBeCalledWith({ to: "/custom-path" });
});
});
it("logout rejected", async () => {
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => {
return Promise.resolve({
success: false,
error: new Error("Logout rejected"),
});
},
},
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
expect(result.current.data).toEqual({
success: false,
error: new Error("Logout rejected"),
});
});
});
it("logout rejected with undefined error", async () => {
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: false,
});
},
},
}),
});
const { mutateAsync: logout } = result.current;
await act(async () => {
await logout();
});
await waitFor(() => {
expect(result.current.data).toEqual({
success: false,
});
});
expect(result.current.data?.error).toBeUndefined();
});
it("logout and not redirect if success false", async () => {
const { result } = renderHook(() => useOnError(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: () => {
return Promise.resolve({
success: false,
});
},
},
}),
});
const { mutate: onError } = result.current;
await act(async () => {
await onError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockGo).toBeCalledTimes(0);
});
});
it("should open notification when has error is true", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
logout: () =>
Promise.resolve({
success: false,
error: new Error("Error"),
}),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "useLogout-error",
type: "error",
message: "Error",
description: "Error",
});
});
});
it("should open notification when has success is false, error is undefined", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
logout: () => Promise.resolve({ success: false }),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "useLogout-error",
type: "error",
message: "Logout Error",
description: "Something went wrong during logout",
});
});
});
it("should open notification when throw error", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
logout: () => {
throw new Error("Unhandled error");
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "useLogout-error",
type: "error",
message: "Error",
description: "Unhandled error",
});
});
});
it("should override `mutationFn` with mutationOptions.mutationFn", async () => {
const logoutMock = jest.fn().mockResolvedValue({ data: {} });
const mutationFnMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useLogout({
mutationOptions: {
// mutationFn is omitted in types. So we need to use @ts-ignore test it.
// @ts-ignore
mutationFn: mutationFnMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: logoutMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(logoutMock).not.toBeCalled();
expect(mutationFnMock).toBeCalled();
});
it("should override `mutationKey` with `mutationOptions.mutationKey`", async () => {
const logoutMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useLogout({
mutationOptions: {
mutationKey: ["foo", "bar"],
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
logout: logoutMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(
queryClient.getMutationCache().findAll({
mutationKey: ["foo", "bar"],
}),
).toHaveLength(1);
});
it("should open success notification when successNotification is passed", async () => {
const openNotificationMock = jest.fn();
const successNotification = {
message: "Logged out successfully!",
description: "Operation completed successfully",
};
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
logout: () =>
Promise.resolve({
success: true,
successNotification,
}),
},
}),
});
await act(async () => {
await result.current.mutateAsync();
});
expect(openNotificationMock).toHaveBeenCalledWith({
key: "logout-success",
type: "success",
message: "Logged out successfully!",
description: "Operation completed successfully",
});
});
});
// NOTE : Will be removed in v5
describe("useLogout Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyLogoutMock = jest.fn(() => Promise.resolve());
const logoutMock = jest.fn(() =>
Promise.resolve({
success: true,
}),
);
const { result } = renderHook(() => useLogout(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => legacyLogoutMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => logoutMock(),
},
}),
});
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyLogoutMock).not.toHaveBeenCalled();
expect(logoutMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyLogoutMock = jest.fn(() => Promise.resolve());
const logoutMock = jest.fn(() => Promise.resolve({ success: true }));
const { result } = renderHook(
() => useLogout({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => legacyLogoutMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => logoutMock(),
},
}),
},
);
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyLogoutMock).toHaveBeenCalled();
expect(logoutMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,241 @@
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseMutationOptions,
type UseMutationResult,
useMutation,
} from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import {
useGo,
useKeys,
useNavigation,
useNotification,
useRouterType,
} from "@hooks";
import type {
AuthActionResponse,
SuccessNotificationResponse,
TLogoutData,
} from "../../../contexts/auth/types";
import type { RefineError } from "../../../contexts/data/types";
import type { OpenNotificationParams } from "../../../contexts/notification/types";
import { useInvalidateAuthStore } from "../useInvalidateAuthStore";
type Variables = {
redirectPath?: string | false;
};
export type UseLogoutLegacyProps<TVariables> = {
v3LegacyAuthProviderCompatible: true;
mutationOptions?: Omit<
UseMutationOptions<
TLogoutData,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>,
"mutationFn" | "onError" | "onSuccess"
>;
};
export type UseLogoutProps<TVariables> = {
v3LegacyAuthProviderCompatible?: false;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>,
"mutationFn"
>;
};
export type UseLogoutCombinedProps<TVariables> = {
v3LegacyAuthProviderCompatible: boolean;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse | TLogoutData,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>,
"mutationFn"
>;
};
export type UseLogoutLegacyReturnType<TVariables> = UseMutationResult<
TLogoutData,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>;
export type UseLogoutReturnType<TVariables> = UseMutationResult<
AuthActionResponse,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>;
export type UseLogoutCombinedReturnType<TVariables> = UseMutationResult<
AuthActionResponse | TLogoutData,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>;
export function useLogout<TVariables = {}>(
props: UseLogoutLegacyProps<TVariables>,
): UseLogoutLegacyReturnType<TVariables>;
export function useLogout<TVariables = {}>(
props?: UseLogoutProps<TVariables>,
): UseLogoutReturnType<TVariables>;
export function useLogout<TVariables = {}>(
props?: UseLogoutCombinedProps<TVariables>,
): UseLogoutCombinedReturnType<TVariables>;
/**
* `useLogout` calls the `logout` method from the {@link https://refine.dev/docs/api-reference/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useLogout} for more details.
*
*/
export function useLogout<TVariables = {}>({
v3LegacyAuthProviderCompatible,
mutationOptions,
}: UseLogoutProps<TVariables> | UseLogoutLegacyProps<TVariables> = {}):
| UseLogoutLegacyReturnType<TVariables>
| UseLogoutReturnType<TVariables> {
const invalidateAuthStore = useInvalidateAuthStore();
const routerType = useRouterType();
const go = useGo();
const { push } = useNavigation();
const { open, close } = useNotification();
const { logout: legacyLogoutFromContext } = useLegacyAuthContext();
const { logout: logoutFromContext } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const mutation = useMutation<
AuthActionResponse,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>({
mutationKey: keys().auth().action("logout").get(preferLegacyKeys),
mutationFn: logoutFromContext,
onSuccess: async (data, variables) => {
const { success, error, redirectTo, successNotification } = data;
const { redirectPath } = variables ?? {};
const redirect = redirectPath ?? redirectTo;
if (success) {
close?.("useLogout-error");
if (successNotification) {
open?.(buildSuccessNotification(successNotification));
}
}
if (error || !success) {
open?.(buildNotification(error));
}
if (redirect !== false) {
if (routerType === "legacy") {
push(redirect ?? "/login");
} else {
if (redirect) {
go({ to: redirect });
}
}
}
await invalidateAuthStore();
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions?.meta),
...getXRay("useLogout", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation<
TLogoutData,
Error | RefineError,
(TVariables & Variables) | void,
unknown
>({
mutationKey: [
...keys().auth().action("logout").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: legacyLogoutFromContext,
onSuccess: async (data, variables) => {
const redirectPath = variables?.redirectPath ?? data;
if (redirectPath === false) {
return;
}
if (redirectPath) {
if (routerType === "legacy") {
push(redirectPath);
} else {
go({ to: redirectPath });
}
return;
}
if (routerType === "legacy") {
push("/login");
} else {
go({ to: "/login" });
}
await invalidateAuthStore();
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible ? mutationOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? mutationOptions?.meta : {}),
...getXRay("useLogout", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
const buildNotification = (
error?: Error | RefineError,
): OpenNotificationParams => {
return {
key: "useLogout-error",
type: "error",
message: error?.name || "Logout Error",
description: error?.message || "Something went wrong during logout",
};
};
const buildSuccessNotification = (
successNotification: SuccessNotificationResponse,
): OpenNotificationParams => {
return {
message: successNotification.message,
description: successNotification.description,
key: "logout-success",
type: "success",
};
};

View File

@@ -0,0 +1,304 @@
import { renderHook, waitFor } from "@testing-library/react";
import {
TestWrapper,
act,
mockLegacyRouterProvider,
mockRouterProvider,
} from "@test";
import { useOnError } from ".";
import type { LegacyRouterProvider } from "../../../contexts/router/legacy/types";
const mockReplace = jest.fn();
const mockPush = jest.fn();
const legacyRouterProvider: LegacyRouterProvider = {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockReplace,
push: mockPush,
}),
};
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useOnError Hook", () => {
beforeEach(() => {
mockReplace.mockReset();
mockPush.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message === "rejected" || message === "/customPath") return;
console.warn(message);
});
});
it("logout and redirect to login if check error rejected", async () => {
const onErrorMock = jest.fn();
const { result } = renderHook(
() => useOnError({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.reject(),
getPermissions: () => Promise.resolve(),
logout: onErrorMock,
getUserIdentity: () => Promise.resolve(),
},
legacyRouterProvider,
}),
},
);
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
expect(onErrorMock).toBeCalledTimes(1);
expect(mockPush).toBeCalledWith("/login");
});
it("logout and redirect to custom path if check error rejected", async () => {
const { result } = renderHook(
() => useOnError({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
isProvided: true,
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.reject("/customPath"),
getPermissions: () => Promise.resolve(),
logout: ({ redirectPath }) => {
return Promise.resolve(redirectPath);
},
getUserIdentity: () => Promise.resolve(),
},
legacyRouterProvider,
}),
},
);
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockPush).toBeCalledWith("/customPath");
});
});
});
describe("useOnError Hook", () => {
beforeEach(() => {
mockReplace.mockReset();
mockPush.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message === "rejected" || message === "/customPath") return;
console.warn(message);
});
});
it("logout and redirect to given path if check error rejected", async () => {
const { result } = renderHook(() => useOnError(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () =>
Promise.resolve({
error: new Error("rejected"),
redirectTo: "/login",
logout: true,
}),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve({ success: true }),
},
legacyRouterProvider,
}),
});
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockPush).toBeCalledWith("/login");
});
});
it("not logout and redirect to given path if check error rejected with legacyRouterProvider", async () => {
const { result } = renderHook(() => useOnError(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () =>
Promise.resolve({
error: new Error("rejected"),
redirectTo: "/login",
logout: false,
}),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve({ success: true }),
},
legacyRouterProvider,
}),
});
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockReplace).toBeCalledWith("/login");
});
});
it("not logout and redirect to given path if check error rejected", async () => {
const mockGo = jest.fn();
const { result } = renderHook(() => useOnError(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () =>
Promise.resolve({
error: new Error("rejected"),
redirectTo: "/login",
logout: false,
}),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve({ success: true }),
},
routerProvider: mockRouterProvider({
fns: {
go: () => mockGo,
},
}),
}),
});
const { mutate: checkError } = result.current;
await act(async () => {
await checkError({});
});
await waitFor(() => {
expect(!result.current.isLoading).toBeTruthy();
});
await act(async () => {
expect(mockGo).toBeCalledWith({
to: "/login",
type: "replace",
});
});
});
});
// NOTE : Will be removed in v5
describe("useOnError Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyCheckErrorMock = jest.fn(() => Promise.resolve());
const onErrorMock = jest.fn(() => Promise.resolve({}));
const { result } = renderHook(() => useOnError(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => legacyCheckErrorMock(),
logout: () => Promise.resolve(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => onErrorMock(),
logout: () => Promise.resolve({ success: true }),
},
}),
});
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyCheckErrorMock).not.toHaveBeenCalled();
expect(onErrorMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyCheckErrorMock = jest.fn(() => Promise.resolve());
const onErrorMock = jest.fn(() => Promise.resolve({}));
const { result } = renderHook(
() => useOnError({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => legacyCheckErrorMock(),
logout: () => Promise.resolve(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => onErrorMock(),
logout: () => Promise.resolve({ success: true }),
},
}),
},
);
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyCheckErrorMock).toHaveBeenCalled();
expect(onErrorMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,130 @@
import { getXRay } from "@refinedev/devtools-internal";
import { type UseMutationResult, useMutation } from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import { useGo, useLogout, useNavigation, useRouterType } from "@hooks";
import { useKeys } from "@hooks/useKeys";
import type { OnErrorResponse } from "../../../contexts/auth/types";
export type UseOnErrorLegacyProps = {
v3LegacyAuthProviderCompatible: true;
};
export type UseOnErrorProps = {
v3LegacyAuthProviderCompatible?: false;
};
export type UseOnErrorCombinedProps = {
v3LegacyAuthProviderCompatible: boolean;
};
export type UseOnErrorLegacyReturnType = UseMutationResult<
void,
string | undefined,
any,
unknown
>;
export type UseOnErrorReturnType = UseMutationResult<
OnErrorResponse,
unknown,
unknown,
unknown
>;
export type UseOnErrorCombinedReturnType = UseMutationResult<
OnErrorResponse | void,
unknown,
unknown,
unknown
>;
export function useOnError(
props: UseOnErrorLegacyProps,
): UseOnErrorLegacyReturnType;
export function useOnError(props?: UseOnErrorProps): UseOnErrorReturnType;
export function useOnError(
props?: UseOnErrorCombinedProps,
): UseOnErrorCombinedReturnType;
/**
* `useOnError` calls the `checkError` method from the {@link https://refine.dev/docs/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useCheckError} for more details.
*
*/
export function useOnError({
v3LegacyAuthProviderCompatible = false,
}: UseOnErrorProps | UseOnErrorLegacyProps = {}):
| UseOnErrorReturnType
| UseOnErrorLegacyReturnType {
const routerType = useRouterType();
const go = useGo();
const { replace } = useNavigation();
const { checkError: legacyCheckErrorFromContext } = useLegacyAuthContext();
const { onError: onErrorFromContext } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const { mutate: legacyLogout } = useLogout({
v3LegacyAuthProviderCompatible: Boolean(v3LegacyAuthProviderCompatible),
});
const { mutate: logout } = useLogout({
v3LegacyAuthProviderCompatible: Boolean(v3LegacyAuthProviderCompatible),
});
const mutation = useMutation<OnErrorResponse, unknown, unknown, unknown>({
mutationKey: keys().auth().action("onError").get(preferLegacyKeys),
...(onErrorFromContext
? {
mutationFn: onErrorFromContext,
onSuccess: ({ logout: shouldLogout, redirectTo }) => {
if (shouldLogout) {
logout({ redirectPath: redirectTo });
return;
}
if (redirectTo) {
if (routerType === "legacy") {
replace(redirectTo);
} else {
go({ to: redirectTo, type: "replace" });
}
return;
}
},
}
: {
mutationFn: () => ({}) as Promise<OnErrorResponse>,
}),
meta: {
...getXRay("useOnError", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation({
mutationKey: [
...keys().auth().action("onError").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: legacyCheckErrorFromContext,
onError: (redirectPath?: string) => {
legacyLogout({ redirectPath });
},
meta: {
...getXRay("useOnError", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
/**
* @deprecated `useCheckError` is deprecated with refine@4, use `useOnError` instead, however, we still support `useCheckError` for backward compatibility.
*/
export const useCheckError = useOnError;

View File

@@ -0,0 +1,270 @@
import { renderHook, waitFor } from "@testing-library/react";
import { MockJSONServer, TestWrapper } from "@test";
import { usePermissions } from "./";
describe("usePermissions Hook", () => {
it("returns authenticated userPermissions", async () => {
const { result } = renderHook(() => usePermissions(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getPermissions: () => Promise.resolve(["admin"]),
},
dataProvider: MockJSONServer,
resources: [{ name: "posts" }],
}),
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual(["admin"]);
});
it("returns error for not authenticated", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (!message.includes("Not Authenticated")) console.warn(message);
});
const { result } = renderHook(() => usePermissions(), {
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: false }),
check: () => Promise.resolve({ authenticated: false }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: false }),
getPermissions: () => Promise.resolve("Not Authenticated"),
},
}),
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(result.current.data).toEqual("Not Authenticated");
});
it("should resolve undefined if no authProvider is provided", async () => {
const { result } = renderHook(() => usePermissions(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: undefined,
},
}),
});
result.current.refetch();
expect(result.current.data).toBeUndefined();
});
it("should accept params", async () => {
const mockGetPermissions = jest.fn(() => Promise.resolve(["admin"]));
const { result } = renderHook((props) => usePermissions({ ...props }), {
initialProps: {
params: { currentRole: "admin" },
v3LegacyAuthProviderCompatible: false,
},
wrapper: TestWrapper({
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getPermissions: mockGetPermissions,
},
dataProvider: MockJSONServer,
resources: [{ name: "posts" }],
}),
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGetPermissions).toHaveBeenCalledWith({ currentRole: "admin" });
expect(result.current.data).toEqual(["admin"]);
});
});
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible usePermissions Hook", () => {
it("returns authenticated userPermissions", async () => {
const getPermissionMock = jest.fn(() => Promise.resolve(["admin"]));
const { result } = renderHook(
() => usePermissions({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: getPermissionMock,
},
}),
},
);
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(getPermissionMock).toHaveBeenCalledTimes(1);
expect(result.current.data).toEqual(["admin"]);
result.current.refetch();
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(getPermissionMock).toHaveBeenCalledTimes(2);
expect(result.current.data).toEqual(["admin"]);
});
it("returns error for not authenticated", async () => {
jest.spyOn(console, "error").mockImplementation((message) => {
if (!message.includes("Not Authenticated")) console.warn(message);
});
const { result } = renderHook(
() => usePermissions({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.reject("Not Authenticated"),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.reject("Not Authenticated"),
logout: () => Promise.resolve(),
},
}),
},
);
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
expect(result.current.error).toEqual("Not Authenticated");
});
it("should resolve undefined if no authProvider is provided", async () => {
const { result } = renderHook(
() => usePermissions({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: undefined,
},
}),
},
);
result.current.refetch();
expect(result.current.data).toBeUndefined();
});
it("should accept params with v3LegacyAuthProviderCompatible", async () => {
const legacyGetPermissionMock = jest.fn(() => Promise.resolve(["admin"]));
const { result } = renderHook((props) => usePermissions({ ...props }), {
initialProps: {
params: { currentRole: "admin" },
v3LegacyAuthProviderCompatible: true,
},
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: legacyGetPermissionMock,
},
}),
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyGetPermissionMock).toHaveBeenCalledWith({
currentRole: "admin",
});
expect(result.current.data).toEqual(["admin"]);
});
});
// NOTE : Will be removed in v5
describe("usePermissions Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyGetPermissionMock = jest.fn(() => Promise.resolve());
const getPermissionMock = jest.fn(() => Promise.resolve());
const { result } = renderHook(() => usePermissions(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => legacyGetPermissionMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getPermissions: () => getPermissionMock(),
},
}),
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyGetPermissionMock).not.toHaveBeenCalled();
expect(getPermissionMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyGetPermissionMock = jest.fn(() => Promise.resolve());
const getPermissionMock = jest.fn(() => Promise.resolve());
const { result } = renderHook(
() => usePermissions({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => legacyGetPermissionMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
getPermissions: () => getPermissionMock(),
},
}),
},
);
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyGetPermissionMock).toHaveBeenCalled();
expect(getPermissionMock).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,133 @@
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseQueryOptions,
type UseQueryResult,
useQuery,
} from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import { useKeys } from "@hooks/useKeys";
import type { PermissionResponse } from "../../../contexts/auth/types";
export type UsePermissionsLegacyProps<
TData = any,
TParams extends Record<string, any> = Record<string, any>,
> = {
v3LegacyAuthProviderCompatible: true;
options?: UseQueryOptions<TData>;
params?: TParams;
};
export type UsePermissionsProps<
TData = PermissionResponse,
TParams extends Record<string, any> = Record<string, any>,
> = {
v3LegacyAuthProviderCompatible?: false;
options?: UseQueryOptions<TData>;
params?: TParams;
};
export type UsePermissionsCombinedProps<
TData = any,
TParams extends Record<string, any> = Record<string, any>,
> = {
v3LegacyAuthProviderCompatible: boolean;
options?: UseQueryOptions<TData> | UseQueryOptions<PermissionResponse>;
params?: TParams;
};
export type UsePermissionsLegacyReturnType<TData = any> = UseQueryResult<
TData,
unknown
>;
export type UsePermissionsReturnType<TData = PermissionResponse> =
UseQueryResult<TData, unknown>;
export type UsePermissionsCombinedReturnType<TData = any> =
| UseQueryResult<TData, unknown>
| UseQueryResult<PermissionResponse, unknown>;
export function usePermissions<
TData = any,
TParams extends Record<string, any> = Record<string, any>,
>(
props: UsePermissionsLegacyProps<TData, TParams>,
): UsePermissionsLegacyReturnType<TData>;
export function usePermissions<
TData = PermissionResponse,
TParams extends Record<string, any> = Record<string, any>,
>(props?: UsePermissionsProps<TData, TParams>): UsePermissionsReturnType<TData>;
export function usePermissions<
TData = any,
TParams extends Record<string, any> = Record<string, any>,
>(
props?: UsePermissionsCombinedProps<TData, TParams>,
): UsePermissionsCombinedReturnType<TData>;
/**
* `usePermissions` calls the `getPermissions` method from the {@link https://refine.dev/docs/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/usePermissions} for more details.
*
* @typeParam TData - Result data of the query
*
* @typeParam TParams - Params will be passed to the `getPermissions` method of {@link https://refine.dev/docs/core/providers/auth-provider `authProvider`}.
*
*/
export function usePermissions<
TData = any,
TParams extends Record<string, any> = Record<string, any>,
>({
v3LegacyAuthProviderCompatible = false,
options,
params,
}:
| UsePermissionsProps<TData, TParams>
| UsePermissionsLegacyProps<TData, TParams> = {}):
| UsePermissionsReturnType
| UsePermissionsLegacyReturnType<TData> {
const { getPermissions: legacyGetPermission } = useLegacyAuthContext();
const { getPermissions } = useAuthBindingsContext();
const { keys, preferLegacyKeys } = useKeys();
const queryResponse = useQuery<TData>({
queryKey: keys().auth().action("permissions").get(preferLegacyKeys),
// Enabled check for `getPermissions` is enough to be sure that it's defined in the query function but TS is not smart enough to know that.
queryFn: (getPermissions
? () => getPermissions(params)
: () => Promise.resolve(undefined)) as (
params?: unknown,
) => Promise<TData>,
enabled: !v3LegacyAuthProviderCompatible && !!getPermissions,
...(v3LegacyAuthProviderCompatible ? {} : options),
meta: {
...(v3LegacyAuthProviderCompatible ? {} : options?.meta),
...getXRay("usePermissions", preferLegacyKeys),
},
});
const legacyQueryResponse = useQuery<TData>({
queryKey: [
...keys().auth().action("permissions").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
// Enabled check for `getPermissions` is enough to be sure that it's defined in the query function but TS is not smart enough to know that.
queryFn: (legacyGetPermission
? () => legacyGetPermission(params)
: () => Promise.resolve(undefined)) as (
params?: unknown,
) => Promise<TData>,
enabled: v3LegacyAuthProviderCompatible && !!legacyGetPermission,
...(v3LegacyAuthProviderCompatible ? options : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? options?.meta : {}),
...getXRay("usePermissions", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible ? legacyQueryResponse : queryResponse;
}

View File

@@ -0,0 +1,821 @@
import { renderHook, waitFor } from "@testing-library/react";
import {
TestWrapper,
act,
mockLegacyRouterProvider,
mockRouterProvider,
queryClient,
} from "@test";
import { useRegister } from ".";
const mockGo = jest.fn();
const routerProvider = mockRouterProvider({
fns: {
go: () => mockGo,
},
});
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useRegister Hook", () => {
beforeEach(() => {
mockGo.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing fields") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed register", async () => {
const { result } = renderHook(
() => useRegister({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
register: ({ email, password }) => {
if (email && password) {
return Promise.resolve();
}
return Promise.reject(new Error("Missing fields"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
routerProvider,
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGo).toBeCalledWith({ to: "/", type: "replace" });
});
it("should successfully register with no redirect", async () => {
const { result } = renderHook(
() => useRegister({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
register: ({ email, password }) => {
if (email && password) {
return Promise.resolve(false);
}
return Promise.reject(new Error("Missing fields"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGo).not.toBeCalled();
});
it("fail register", async () => {
const { result } = renderHook(
() => useRegister({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
register: () => Promise.reject(new Error("Missing fields")),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "demo" });
});
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
const { error } = result.current ?? { error: undefined };
expect(error).toEqual(new Error("Missing fields"));
});
it("register rejected with undefined error", async () => {
const { result } = renderHook(
() => useRegister({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
register: () => Promise.reject(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "demo" });
});
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
const { error } = result.current ?? { error: undefined };
expect(error).not.toBeDefined();
});
});
describe("useRegister Hook", () => {
const mockAuthProvider = {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
logout: () => Promise.resolve({ success: true }),
onError: () => Promise.resolve({}),
};
beforeEach(() => {
mockGo.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing fields") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed register", async () => {
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
redirectTo: "/",
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
routerProvider,
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data?.success).toBeTruthy();
expect(mockGo).toBeCalledWith({ to: "/", type: "replace" });
});
it("should successfully register with no redirect", async () => {
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
routerProvider,
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGo).not.toBeCalled();
});
it("should successfully register with redirect on legacyRouterProvider", async () => {
const mockReplace = jest.fn();
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
redirectTo: "redirectTo",
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
legacyRouterProvider: {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockReplace,
push: jest.fn(),
}),
},
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockReplace).toBeCalledWith("redirectTo");
});
it("should successfully register without redirect on legacyRouterProvider", async () => {
const mockReplace = jest.fn();
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
legacyRouterProvider: {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockReplace,
push: jest.fn(),
}),
},
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockReplace).toBeCalledWith("/");
});
it("fail register", async () => {
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "demo" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({
success: false,
error: new Error("Missing fields"),
});
});
it("register rejected with undefined error", async () => {
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: ({ email, password }: any) => {
if (email && password) {
return Promise.resolve({
success: true,
});
}
return Promise.resolve({
success: false,
});
},
},
}),
});
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "demo" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data?.error).toBeUndefined();
});
it("should open notification when has error is true", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
register: () => {
return Promise.resolve({
success: false,
error: new Error("Error"),
});
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "register-error",
type: "error",
message: "Error",
description: "Error",
});
});
});
it("should open notification when has success is false, error is undefined", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
register: () => Promise.resolve({ success: false }),
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "register-error",
type: "error",
message: "Register Error",
description: "Error while registering",
});
});
});
it("should open notification when throw error", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
register: () => {
throw new Error("Unhandled error");
},
},
}),
});
const { mutate: forgotPassword } = result.current;
await act(async () => {
forgotPassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "register-error",
type: "error",
message: "Error",
description: "Unhandled error",
});
});
});
it("should override `mutationFn` with mutationOptions.mutationFn", async () => {
const registerMock = jest.fn().mockResolvedValue({ data: {} });
const mutationFnMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useRegister({
mutationOptions: {
// mutationFn is omitted in types. So we need to use @ts-ignore test it.
// @ts-ignore
mutationFn: mutationFnMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: registerMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(registerMock).not.toBeCalled();
expect(mutationFnMock).toBeCalled();
});
it("should override `mutationKey` with `mutationOptions.mutationKey`", async () => {
const registerMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useRegister({
mutationOptions: {
mutationKey: ["foo", "bar"],
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
register: registerMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(
queryClient.getMutationCache().findAll({
mutationKey: ["foo", "bar"],
}),
).toHaveLength(1);
});
it("should open success notification when successNotification is passed", async () => {
const openNotificationMock = jest.fn();
const successNotification = {
message: "Success!",
description: "Operation completed successfully",
};
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
register: () =>
Promise.resolve({
success: true,
successNotification,
}),
},
}),
});
await act(async () => {
result.current.mutate({});
});
expect(openNotificationMock).toHaveBeenCalledWith({
key: "register-success",
type: "success",
message: "Success!",
description: "Operation completed successfully",
});
});
});
// NOTE : Will be removed in v5
describe("useRegister Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyRegisterMock = jest.fn(() => Promise.resolve());
const registerMock = jest.fn(() => Promise.resolve({ success: true }));
const { result } = renderHook(() => useRegister(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => legacyRegisterMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
register: () => registerMock(),
},
}),
});
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyRegisterMock).not.toHaveBeenCalled();
expect(registerMock).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyRegisterMock = jest.fn(() => Promise.resolve());
const registerMock = jest.fn(() => Promise.resolve({ success: true }));
const { result } = renderHook(
() => useRegister({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => legacyRegisterMock(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
register: () => registerMock(),
},
}),
},
);
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyRegisterMock).toHaveBeenCalled();
expect(registerMock).not.toHaveBeenCalled();
});
});
describe("useRegister Hook use v3LegacyAuthProviderCompatible", () => {
it("should successfully register with redirect on legacyRouterProvider", async () => {
const mockReplace = jest.fn();
const { result } = renderHook(
() =>
useRegister({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => Promise.resolve("redirectTo"),
},
legacyRouterProvider: {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockReplace,
push: jest.fn(),
}),
},
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockReplace).toBeCalledWith("redirectTo");
});
it("should successfully register without redirect on legacyRouterProvider", async () => {
const mockReplace = jest.fn();
const { result } = renderHook(
() =>
useRegister({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => Promise.resolve(),
},
legacyRouterProvider: {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockReplace,
push: jest.fn(),
}),
},
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockReplace).toBeCalledWith("/");
});
it("should successfully register with redirect", async () => {
const mockGo = jest.fn();
const { result } = renderHook(
() =>
useRegister({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
register: () => Promise.resolve("redirectTo"),
},
routerProvider: mockRouterProvider({
fns: {
go: () => mockGo,
},
}),
}),
},
);
const { mutate: register } = result.current ?? { mutate: () => 0 };
await act(async () => {
register({ email: "test", password: "test" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGo).toBeCalledWith({ to: "redirectTo", type: "replace" });
});
});

View File

@@ -0,0 +1,231 @@
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseMutationOptions,
type UseMutationResult,
useMutation,
} from "@tanstack/react-query";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import {
useGo,
useKeys,
useNavigation,
useNotification,
useRouterType,
} from "@hooks";
import type {
AuthActionResponse,
SuccessNotificationResponse,
TLoginData,
TRegisterData,
} from "../../../contexts/auth/types";
import type { RefineError } from "../../../contexts/data/types";
import type { OpenNotificationParams } from "../../../contexts/notification/types";
import { useInvalidateAuthStore } from "../useInvalidateAuthStore";
export type UseRegisterLegacyProps<TVariables> = {
v3LegacyAuthProviderCompatible: true;
mutationOptions?: Omit<
UseMutationOptions<TRegisterData, Error | RefineError, TVariables, unknown>,
"mutationFn" | "onError" | "onSuccess"
>;
};
export type UseRegisterProps<TVariables> = {
v3LegacyAuthProviderCompatible?: false;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseRegisterCombinedProps<TVariables> = {
v3LegacyAuthProviderCompatible: boolean;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse | TRegisterData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseRegisterLegacyReturnType<TVariables> = UseMutationResult<
TRegisterData,
Error | RefineError,
TVariables,
unknown
>;
export type UseRegisterReturnType<TVariables> = UseMutationResult<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>;
export type UseRegisterCombinedReturnType<TVariables> = UseMutationResult<
AuthActionResponse | TLoginData,
Error | RefineError,
TVariables,
unknown
>;
export function useRegister<TVariables = {}>(
props: UseRegisterLegacyProps<TVariables>,
): UseRegisterLegacyReturnType<TVariables>;
export function useRegister<TVariables = {}>(
props?: UseRegisterProps<TVariables>,
): UseRegisterReturnType<TVariables>;
export function useRegister<TVariables = {}>(
props?: UseRegisterCombinedProps<TVariables>,
): UseRegisterCombinedReturnType<TVariables>;
/**
* `useRegister` calls `register` method from {@link https://refine.dev/docs/api-reference/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useRegister} for more details.
*
* @typeParam TData - Result data of the query
* @typeParam TVariables - Values for mutation function. default `{}`
*
*/
export function useRegister<TVariables = {}>({
v3LegacyAuthProviderCompatible,
mutationOptions,
}: UseRegisterProps<TVariables> | UseRegisterLegacyProps<TVariables> = {}):
| UseRegisterReturnType<TVariables>
| UseRegisterLegacyReturnType<TVariables> {
const invalidateAuthStore = useInvalidateAuthStore();
const routerType = useRouterType();
const go = useGo();
const { replace } = useNavigation();
const { register: legacyRegisterFromContext } = useLegacyAuthContext();
const { register: registerFromContext } = useAuthBindingsContext();
const { close, open } = useNotification();
const { keys, preferLegacyKeys } = useKeys();
const mutation = useMutation<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: keys().auth().action("register").get(preferLegacyKeys),
mutationFn: registerFromContext,
onSuccess: async ({ success, redirectTo, error, successNotification }) => {
if (success) {
close?.("register-error");
if (successNotification) {
open?.(buildSuccessNotification(successNotification));
}
}
if (error || !success) {
open?.(buildNotification(error));
}
if (redirectTo) {
if (routerType === "legacy") {
replace(redirectTo);
} else {
go({ to: redirectTo, type: "replace" });
}
} else {
if (routerType === "legacy") {
replace("/");
}
}
await invalidateAuthStore();
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions?.meta),
...getXRay("useRegister", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation<
TRegisterData,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: [
...keys().auth().action("register").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: legacyRegisterFromContext,
onSuccess: async (redirectPathFromAuth) => {
if (redirectPathFromAuth !== false) {
if (redirectPathFromAuth) {
if (routerType === "legacy") {
replace(redirectPathFromAuth);
} else {
go({ to: redirectPathFromAuth, type: "replace" });
}
} else {
if (routerType === "legacy") {
replace("/");
} else {
go({ to: "/", type: "replace" });
}
}
await invalidateAuthStore();
close?.("register-error");
}
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible ? mutationOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? mutationOptions?.meta : {}),
...getXRay("useRegister", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
const buildNotification = (
error?: Error | RefineError,
): OpenNotificationParams => {
return {
message: error?.name || "Register Error",
description: error?.message || "Error while registering",
key: "register-error",
type: "error",
};
};
const buildSuccessNotification = (
successNotification: SuccessNotificationResponse,
): OpenNotificationParams => {
return {
message: successNotification.message,
description: successNotification.description,
key: "register-success",
type: "success",
};
};

View File

@@ -0,0 +1,857 @@
import { renderHook, waitFor } from "@testing-library/react";
import {
TestWrapper,
act,
mockLegacyAuthProvider,
mockLegacyRouterProvider,
mockRouterProvider,
queryClient,
} from "@test";
import type { LegacyRouterProvider } from "../../../contexts/router/legacy/types";
import { useUpdatePassword } from "./";
const mockFn = jest.fn();
const legacyRouterProvider: LegacyRouterProvider = {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
push: jest.fn(),
replace: mockFn,
}),
};
const mockAuthProvider = {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
};
// NOTE : Will be removed in v5
describe("v3LegacyAuthProviderCompatible useUpdatePassword Hook", () => {
beforeEach(() => {
mockFn.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing fields") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed update password", async () => {
const { result } = renderHook(
() => useUpdatePassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
updatePassword: ({ password, confirmPassword }) => {
if (password && confirmPassword) {
return Promise.resolve();
}
return Promise.reject(new Error("Missing fields"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
legacyRouterProvider,
}),
},
);
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockFn).not.toBeCalledWith();
});
it("fail update password", async () => {
const { result } = renderHook(
() => useUpdatePassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
updatePassword: () => Promise.reject(new Error("Missing fields")),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
legacyRouterProvider,
}),
},
);
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123" });
});
await waitFor(() => {
expect(result.current.isError).toBeTruthy();
});
const { error } = result.current ?? { error: undefined };
expect(error).toEqual(new Error("Missing fields"));
});
it("should open notification when has success is false, error is undefined", async () => {
const updatePasswordMock = jest.fn();
const openNotificationMock = jest.fn();
const closeNotificationMock = jest.fn();
const { result } = renderHook(
() => useUpdatePassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: closeNotificationMock,
},
legacyAuthProvider: {
...mockLegacyAuthProvider,
updatePassword: updatePasswordMock,
},
routerProvider: legacyRouterProvider,
}),
},
);
const { mutate: updatePassword } = result.current;
updatePasswordMock.mockRejectedValueOnce({});
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "update-password-error",
type: "error",
message: "Update Password Error",
description: "Error while updating password",
});
});
updatePasswordMock.mockResolvedValueOnce(false);
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(closeNotificationMock).toBeCalledWith("update-password-error");
});
});
it("should open notification when throw error", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(
() => useUpdatePassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
routerProvider: legacyRouterProvider,
legacyAuthProvider: {
...mockLegacyAuthProvider,
updatePassword: () => {
throw new Error("Unhandled error");
},
},
}),
},
);
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "update-password-error",
type: "error",
message: "Error",
description: "Unhandled error",
});
});
});
});
describe("useUpdatePassword Hook", () => {
beforeEach(() => {
mockFn.mockReset();
jest.spyOn(console, "error").mockImplementation((message) => {
if (message?.message === "Missing fields") return;
if (typeof message === "undefined") return;
console.warn(message);
});
});
it("succeed update password with legacyRouterProvider", async () => {
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: ({ password, confirmPassword }: any) => {
if (password && confirmPassword) {
return Promise.resolve({ success: true });
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({ success: true });
expect(mockFn).not.toBeCalledWith();
});
it("succeed update password", async () => {
const mockGo = jest.fn();
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: ({ password, confirmPassword }: any) => {
if (password && confirmPassword) {
return Promise.resolve({
success: true,
redirectTo: "redirectTo",
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
routerProvider: mockRouterProvider({
fns: {
go: () => mockGo,
},
}),
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockGo).toBeCalledWith({
to: "redirectTo",
type: "replace",
});
});
it("fail update password", async () => {
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: ({ password, confirmPassword }: any) => {
if (password && confirmPassword) {
return Promise.resolve({ success: true });
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({
success: false,
error: new Error("Missing fields"),
});
expect(mockFn).not.toBeCalledWith();
});
it("success and redirect", async () => {
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: ({ password, confirmPassword }: any) => {
if (password && confirmPassword) {
return Promise.resolve({
success: true,
redirectTo: "/login",
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
});
},
},
legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({
success: true,
redirectTo: "/login",
});
expect(mockFn).toBeCalledWith("/login");
});
it("fail and redirect", async () => {
mockFn;
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: ({ password, confirmPassword }: any) => {
if (password && confirmPassword) {
return Promise.resolve({
success: true,
});
}
return Promise.resolve({
success: false,
error: new Error("Missing fields"),
redirectTo: "/register",
});
},
},
legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(result.current.data).toEqual({
success: false,
error: new Error("Missing fields"),
redirectTo: "/register",
});
expect(mockFn).toBeCalledWith("/register");
});
it("should open notification when has error is true", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
updatePassword: () =>
Promise.resolve({
success: false,
error: new Error("Error"),
}),
},
legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "update-password-error",
type: "error",
message: "Error",
description: "Error",
});
});
});
it("should open notification when has success is false, error is undefined", async () => {
const updatePasswordMock = jest.fn();
const openNotificationMock = jest.fn();
const closeNotificationMock = jest.fn();
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: closeNotificationMock,
},
authProvider: {
...mockAuthProvider,
updatePassword: updatePasswordMock,
},
routerProvider: legacyRouterProvider,
}),
});
const { mutate: updatePassword } = result.current;
updatePasswordMock.mockResolvedValueOnce({
success: false,
});
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "update-password-error",
type: "error",
message: "Update Password Error",
description: "Error while updating password",
});
});
updatePasswordMock.mockResolvedValueOnce({
success: true,
});
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(closeNotificationMock).toBeCalledWith("update-password-error");
});
});
it("should open notification when throw error", async () => {
const openNotificationMock = jest.fn();
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
updatePassword: () => {
throw new Error("Unhandled error");
},
},
}),
});
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(openNotificationMock).toBeCalledWith({
key: "update-password-error",
type: "error",
message: "Error",
description: "Unhandled error",
});
});
});
it("should work notificationProvider", async () => {
const onErrorMock = jest.fn();
const onSuccessMock = jest.fn();
const updatePasswordMock = jest.fn();
const { result } = renderHook(
() =>
useUpdatePassword({
mutationOptions: {
onSuccess: onSuccessMock,
onError: onErrorMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: updatePasswordMock,
},
}),
},
);
const { mutate: updatePassword } = result.current;
updatePasswordMock.mockRejectedValueOnce(new Error("Error"));
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(onErrorMock).toBeCalledTimes(1);
});
updatePasswordMock.mockResolvedValueOnce({
success: false,
});
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(onSuccessMock).toBeCalledTimes(1);
});
updatePasswordMock.mockResolvedValueOnce({
success: true,
});
await act(async () => {
updatePassword({});
});
await waitFor(() => {
expect(onSuccessMock).toBeCalledTimes(2);
});
});
it("should override `mutationFn` with mutationOptions.mutationFn", async () => {
const updatePasswordMock = jest.fn().mockResolvedValue({ data: {} });
const mutationFnMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useUpdatePassword({
mutationOptions: {
// mutationFn is omitted in types. So we need to use @ts-ignore test it.
// @ts-ignore
mutationFn: mutationFnMock,
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: updatePasswordMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(updatePasswordMock).not.toBeCalled();
expect(mutationFnMock).toBeCalled();
});
it("should override `mutationKey` with `mutationOptions.mutationKey`", async () => {
const updatePasswordMock = jest.fn().mockResolvedValue({ data: {} });
const { result } = renderHook(
() =>
useUpdatePassword({
mutationOptions: {
mutationKey: ["foo", "bar"],
},
}),
{
wrapper: TestWrapper({
authProvider: {
...mockAuthProvider,
updatePassword: updatePasswordMock,
},
}),
},
);
result.current.mutate({});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(
queryClient.getMutationCache().findAll({
mutationKey: ["foo", "bar"],
}),
).toHaveLength(1);
});
it("should open success notification when successNotification is passed", async () => {
const openNotificationMock = jest.fn();
const successNotification = {
message: "Success!",
description: "Operation completed successfully",
};
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
notificationProvider: {
open: openNotificationMock,
close: jest.fn(),
},
authProvider: {
...mockAuthProvider,
updatePassword: () =>
Promise.resolve({
success: true,
successNotification,
}),
},
}),
});
await act(async () => {
result.current.mutate({});
});
expect(openNotificationMock).toHaveBeenCalledWith({
key: "update-password-success",
type: "success",
message: "Success!",
description: "Operation completed successfully",
});
});
});
// NOTE : Will be removed in v5
describe("useUpdatePassword Hook authProvider selection", () => {
it("selects new authProvider", async () => {
const legacyUpdatePassword = jest.fn(() => Promise.resolve());
const updatePassword = jest.fn(() => Promise.resolve({ success: true }));
const { result } = renderHook(() => useUpdatePassword(), {
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
updatePassword: () => legacyUpdatePassword(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
updatePassword: () => updatePassword(),
},
legacyRouterProvider,
}),
});
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyUpdatePassword).not.toHaveBeenCalled();
expect(updatePassword).toHaveBeenCalled();
});
it("selects v3LegacyAuthProviderCompatible authProvider", async () => {
const legacyUpdatePassword = jest.fn(() => Promise.resolve());
const updatePassword = jest.fn(() => Promise.resolve({ success: true }));
const { result } = renderHook(
() => useUpdatePassword({ v3LegacyAuthProviderCompatible: true }),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
logout: () => Promise.resolve(),
updatePassword: () => legacyUpdatePassword(),
},
authProvider: {
login: () => Promise.resolve({ success: true }),
check: () => Promise.resolve({ authenticated: true }),
onError: () => Promise.resolve({}),
logout: () => Promise.resolve({ success: true }),
updatePassword: () => updatePassword(),
},
legacyRouterProvider,
}),
},
);
const { mutate: login } = result.current;
await act(async () => {
login({});
});
await waitFor(() => {
expect(result.current.isLoading).toBeFalsy();
});
expect(legacyUpdatePassword).toHaveBeenCalled();
expect(updatePassword).not.toHaveBeenCalled();
});
});
describe("useUpdatePassword Hook with v3LegacyAuthProviderCompatible", () => {
it("should be redirect legacyRouterProvider", async () => {
const { result } = renderHook(
() =>
useUpdatePassword({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
updatePassword: ({ password, confirmPassword }) => {
if (password && confirmPassword) {
return Promise.resolve("redirectTo");
}
return Promise.reject(new Error("Missing fields"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
legacyRouterProvider: {
...mockLegacyRouterProvider(),
useHistory: () => ({
goBack: jest.fn(),
replace: mockFn,
push: jest.fn(),
}),
},
}),
},
);
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockFn).toBeCalledWith("redirectTo");
});
it("should be redirect routerProvider", async () => {
const { result } = renderHook(
() =>
useUpdatePassword({
v3LegacyAuthProviderCompatible: true,
}),
{
wrapper: TestWrapper({
legacyAuthProvider: {
login: () => Promise.resolve(),
updatePassword: ({ password, confirmPassword }) => {
if (password && confirmPassword) {
return Promise.resolve("redirectTo");
}
return Promise.reject(new Error("Missing fields"));
},
checkAuth: () => Promise.resolve(),
checkError: () => Promise.resolve(),
getPermissions: () => Promise.resolve(),
logout: () => Promise.resolve(),
getUserIdentity: () => Promise.resolve({ id: 1 }),
},
routerProvider: {
...mockRouterProvider({
fns: {
go: () => mockFn,
},
}),
},
}),
},
);
const { mutate: updatePassword } = result.current;
await act(async () => {
updatePassword({ password: "123", confirmPassword: "321" });
});
await waitFor(() => {
expect(result.current.isSuccess).toBeTruthy();
});
expect(mockFn).toBeCalledWith("redirectTo");
});
});

View File

@@ -0,0 +1,261 @@
import React from "react";
import { getXRay } from "@refinedev/devtools-internal";
import {
type UseMutationOptions,
type UseMutationResult,
useMutation,
} from "@tanstack/react-query";
import qs from "qs";
import { useAuthBindingsContext, useLegacyAuthContext } from "@contexts/auth";
import {
useGo,
useKeys,
useNavigation,
useNotification,
useParsed,
useRouterContext,
useRouterType,
} from "@hooks";
import type { UpdatePasswordFormTypes } from "../../../components/pages/auth/types";
import type {
AuthActionResponse,
SuccessNotificationResponse,
TUpdatePasswordData,
} from "../../../contexts/auth/types";
import type { RefineError } from "../../../contexts/data/types";
import type { OpenNotificationParams } from "../../../contexts/notification/types";
export type UseUpdatePasswordLegacyProps<
TVariables extends UpdatePasswordFormTypes,
> = {
v3LegacyAuthProviderCompatible: true;
mutationOptions?: Omit<
UseMutationOptions<
TUpdatePasswordData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn" | "onError" | "onSuccess"
>;
};
export type UseUpdatePasswordProps<TVariables extends UpdatePasswordFormTypes> =
{
v3LegacyAuthProviderCompatible?: false;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseUpdatePasswordCombinedProps<
TVariables extends UpdatePasswordFormTypes,
> = {
v3LegacyAuthProviderCompatible: boolean;
mutationOptions?: Omit<
UseMutationOptions<
AuthActionResponse | TUpdatePasswordData,
Error | RefineError,
TVariables,
unknown
>,
"mutationFn"
>;
};
export type UseUpdatePasswordLegacyReturnType<
TVariables extends UpdatePasswordFormTypes,
> = UseMutationResult<
TUpdatePasswordData,
Error | RefineError,
TVariables,
unknown
>;
export type UseUpdatePasswordReturnType<
TVariables extends UpdatePasswordFormTypes,
> = UseMutationResult<
AuthActionResponse,
Error | RefineError,
TVariables,
unknown
>;
export type UseUpdatePasswordCombinedReturnType<
TVariables extends UpdatePasswordFormTypes,
> = UseMutationResult<
AuthActionResponse | TUpdatePasswordData,
Error | RefineError,
TVariables,
unknown
>;
export function useUpdatePassword<TVariables extends UpdatePasswordFormTypes>(
props: UseUpdatePasswordLegacyProps<TVariables>,
): UseUpdatePasswordLegacyReturnType<TVariables>;
export function useUpdatePassword<TVariables extends UpdatePasswordFormTypes>(
props?: UseUpdatePasswordProps<TVariables>,
): UseUpdatePasswordReturnType<TVariables>;
export function useUpdatePassword<TVariables extends UpdatePasswordFormTypes>(
props?: UseUpdatePasswordCombinedProps<TVariables>,
): UseUpdatePasswordCombinedReturnType<TVariables>;
/**
* `useUpdatePassword` calls `updatePassword` method from {@link https://refine.dev/docs/api-reference/core/providers/auth-provider `authProvider`} under the hood.
*
* @see {@link https://refine.dev/docs/api-reference/core/hooks/auth/useUpdatePassword} for more details.
*
* @typeParam TData - Result data of the query
* @typeParam TVariables - Values for mutation function. default `{}`
*
*/
export function useUpdatePassword<
TVariables extends UpdatePasswordFormTypes = {},
>({
v3LegacyAuthProviderCompatible,
mutationOptions,
}:
| UseUpdatePasswordProps<TVariables>
| UseUpdatePasswordLegacyProps<TVariables> = {}):
| UseUpdatePasswordReturnType<TVariables>
| UseUpdatePasswordLegacyReturnType<TVariables> {
const routerType = useRouterType();
const go = useGo();
const { replace } = useNavigation();
const { updatePassword: legacyUpdatePasswordFromContext } =
useLegacyAuthContext();
const { updatePassword: updatePasswordFromContext } =
useAuthBindingsContext();
const { close, open } = useNotification();
const { keys, preferLegacyKeys } = useKeys();
const parsed = useParsed();
const { useLocation } = useRouterContext();
const { search } = useLocation();
const params = React.useMemo(() => {
if (routerType === "legacy") {
const queryStrings = qs.parse(search, {
ignoreQueryPrefix: true,
});
return queryStrings ?? {};
}
return parsed.params ?? {};
}, [search, parsed, routerType]);
const mutation = useMutation<AuthActionResponse, Error, TVariables, unknown>({
mutationKey: keys().auth().action("updatePassword").get(preferLegacyKeys),
mutationFn: async (variables) => {
return updatePasswordFromContext?.({
...params,
...variables,
}) as Promise<AuthActionResponse>;
},
onSuccess: ({ success, redirectTo, error, successNotification }) => {
if (success) {
close?.("update-password-error");
if (successNotification) {
open?.(buildSuccessNotification(successNotification));
}
}
if (error || !success) {
open?.(buildNotification(error));
}
if (redirectTo) {
if (routerType === "legacy") {
replace(redirectTo);
} else {
go({ to: redirectTo, type: "replace" });
}
}
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions),
meta: {
...(v3LegacyAuthProviderCompatible === true ? {} : mutationOptions?.meta),
...getXRay("useUpdatePassword", preferLegacyKeys),
},
});
const v3LegacyAuthProviderCompatibleMutation = useMutation<
TUpdatePasswordData,
Error | RefineError,
TVariables,
unknown
>({
mutationKey: [
...keys().auth().action("updatePassword").get(preferLegacyKeys),
"v3LegacyAuthProviderCompatible",
],
mutationFn: async (variables) => {
return legacyUpdatePasswordFromContext?.({
...params,
...variables,
});
},
onSuccess: (redirectPathFromAuth) => {
if (redirectPathFromAuth !== false) {
if (redirectPathFromAuth) {
if (routerType === "legacy") {
replace(redirectPathFromAuth);
} else {
go({ to: redirectPathFromAuth, type: "replace" });
}
}
}
close?.("update-password-error");
},
onError: (error: any) => {
open?.(buildNotification(error));
},
...(v3LegacyAuthProviderCompatible ? mutationOptions : {}),
meta: {
...(v3LegacyAuthProviderCompatible ? mutationOptions?.meta : {}),
...getXRay("useUpdatePassword", preferLegacyKeys),
},
});
return v3LegacyAuthProviderCompatible
? v3LegacyAuthProviderCompatibleMutation
: mutation;
}
const buildNotification = (
error?: Error | RefineError,
): OpenNotificationParams => {
return {
message: error?.name || "Update Password Error",
description: error?.message || "Error while updating password",
key: "update-password-error",
type: "error",
};
};
const buildSuccessNotification = (
successNotification: SuccessNotificationResponse,
): OpenNotificationParams => {
return {
message: successNotification.message,
description: successNotification.description,
key: "update-password-success",
type: "success",
};
};