Skip to content

Commit bf0ae8b

Browse files
committed
feat(storefront-data): add auth module
1 parent 5540788 commit bf0ae8b

4 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import {
2+
useMutation,
3+
useQuery,
4+
useQueryClient,
5+
useSuspenseQuery,
6+
} from "@tanstack/react-query"
7+
import { createCacheConfig, type CacheConfig } from "../shared/cache-config"
8+
import type { QueryNamespace } from "../shared/query-keys"
9+
import { createAuthQueryKeys } from "./query-keys"
10+
import type {
11+
AuthQueryInput,
12+
AuthQueryKeys,
13+
AuthService,
14+
UseAuthResult,
15+
UseSuspenseAuthResult,
16+
} from "./types"
17+
18+
export type CreateAuthHooksConfig<
19+
TCustomer,
20+
TLoginInput,
21+
TRegisterInput,
22+
TUpdateInput,
23+
> = {
24+
service: AuthService<TCustomer, TLoginInput, TRegisterInput, TUpdateInput>
25+
queryKeys?: AuthQueryKeys
26+
queryKeyNamespace?: QueryNamespace
27+
cacheConfig?: CacheConfig
28+
}
29+
30+
export type AuthMutationOptions<TData, TVariables> = {
31+
onSuccess?: (data: TData, variables: TVariables) => void
32+
onError?: (error: unknown) => void
33+
}
34+
35+
export function createAuthHooks<
36+
TCustomer,
37+
TLoginInput,
38+
TRegisterInput,
39+
TUpdateInput,
40+
>({
41+
service,
42+
queryKeys,
43+
queryKeyNamespace = "storefront-data",
44+
cacheConfig,
45+
}: CreateAuthHooksConfig<TCustomer, TLoginInput, TRegisterInput, TUpdateInput>) {
46+
const resolvedCacheConfig = cacheConfig ?? createCacheConfig()
47+
const resolvedQueryKeys =
48+
queryKeys ?? createAuthQueryKeys(queryKeyNamespace)
49+
50+
function useAuth(options?: AuthQueryInput): UseAuthResult<TCustomer> {
51+
const { data, isLoading, isFetching, isSuccess, error } = useQuery({
52+
queryKey: resolvedQueryKeys.customer(),
53+
queryFn: ({ signal }) => service.getCustomer(signal),
54+
enabled: options?.enabled ?? true,
55+
retry: false,
56+
...resolvedCacheConfig.userData,
57+
})
58+
59+
const customer = data ?? null
60+
61+
return {
62+
customer,
63+
isAuthenticated: customer !== null,
64+
isLoading,
65+
isFetching,
66+
isSuccess,
67+
error:
68+
error instanceof Error ? error.message : error ? String(error) : null,
69+
}
70+
}
71+
72+
function useSuspenseAuth(): UseSuspenseAuthResult<TCustomer> {
73+
const { data, isFetching } = useSuspenseQuery<TCustomer | null, Error>({
74+
queryKey: resolvedQueryKeys.customer(),
75+
queryFn: ({ signal }) => service.getCustomer(signal),
76+
...resolvedCacheConfig.userData,
77+
})
78+
const customer = data ?? null
79+
80+
return {
81+
customer,
82+
isAuthenticated: customer !== null,
83+
isFetching,
84+
}
85+
}
86+
87+
function useLogin(options?: AuthMutationOptions<unknown, TLoginInput>) {
88+
const queryClient = useQueryClient()
89+
return useMutation({
90+
mutationFn: (input: TLoginInput) => service.login(input),
91+
onSuccess: (data, variables) => {
92+
queryClient.invalidateQueries({
93+
queryKey: resolvedQueryKeys.customer(),
94+
})
95+
options?.onSuccess?.(data, variables)
96+
},
97+
onError: (error) => {
98+
options?.onError?.(error)
99+
},
100+
})
101+
}
102+
103+
function useRegister(options?: AuthMutationOptions<unknown, TRegisterInput>) {
104+
const queryClient = useQueryClient()
105+
return useMutation({
106+
mutationFn: (input: TRegisterInput) => service.register(input),
107+
onSuccess: (data, variables) => {
108+
queryClient.invalidateQueries({
109+
queryKey: resolvedQueryKeys.customer(),
110+
})
111+
options?.onSuccess?.(data, variables)
112+
},
113+
onError: (error) => {
114+
options?.onError?.(error)
115+
},
116+
})
117+
}
118+
119+
function useLogout(options?: AuthMutationOptions<void, void>) {
120+
const queryClient = useQueryClient()
121+
return useMutation({
122+
mutationFn: () => service.logout(),
123+
onSuccess: () => {
124+
queryClient.setQueryData(resolvedQueryKeys.customer(), null)
125+
queryClient.invalidateQueries({
126+
queryKey: resolvedQueryKeys.all(),
127+
})
128+
queryClient.removeQueries({
129+
queryKey: resolvedQueryKeys.all(),
130+
})
131+
options?.onSuccess?.(undefined, undefined)
132+
},
133+
onError: (error) => {
134+
options?.onError?.(error)
135+
},
136+
})
137+
}
138+
139+
function useUpdateCustomer(
140+
options?: AuthMutationOptions<TCustomer, TUpdateInput>
141+
) {
142+
const queryClient = useQueryClient()
143+
return useMutation({
144+
mutationFn: (input: TUpdateInput) => {
145+
if (!service.updateCustomer) {
146+
throw new Error("updateCustomer service is not configured")
147+
}
148+
return service.updateCustomer(input)
149+
},
150+
onSuccess: (data, variables) => {
151+
queryClient.setQueryData(resolvedQueryKeys.customer(), data)
152+
options?.onSuccess?.(data, variables)
153+
},
154+
onError: (error) => {
155+
options?.onError?.(error)
156+
},
157+
})
158+
}
159+
160+
function useRefreshAuth(options?: AuthMutationOptions<unknown, void>) {
161+
const queryClient = useQueryClient()
162+
return useMutation({
163+
mutationFn: () => {
164+
if (!service.refresh) {
165+
throw new Error("refresh service is not configured")
166+
}
167+
return service.refresh()
168+
},
169+
onSuccess: (data) => {
170+
queryClient.invalidateQueries({
171+
queryKey: resolvedQueryKeys.customer(),
172+
})
173+
options?.onSuccess?.(data, undefined)
174+
},
175+
onError: (error) => {
176+
options?.onError?.(error)
177+
},
178+
})
179+
}
180+
181+
return {
182+
useAuth,
183+
useSuspenseAuth,
184+
useLogin,
185+
useRegister,
186+
useLogout,
187+
useUpdateCustomer,
188+
useRefreshAuth,
189+
}
190+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export { createAuthHooks } from "./hooks"
2+
export { createAuthQueryKeys } from "./query-keys"
3+
export type { AuthMutationOptions, CreateAuthHooksConfig } from "./hooks"
4+
export type {
5+
AuthQueryInput,
6+
AuthQueryKeys,
7+
AuthService,
8+
UseAuthResult,
9+
UseSuspenseAuthResult,
10+
} from "./types"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { createQueryKey } from "../shared/query-keys"
2+
import type { QueryNamespace } from "../shared/query-keys"
3+
import type { AuthQueryKeys } from "./types"
4+
5+
export function createAuthQueryKeys(
6+
namespace: QueryNamespace
7+
): AuthQueryKeys {
8+
return {
9+
all: () => createQueryKey(namespace, "auth"),
10+
customer: () => createQueryKey(namespace, "auth", "customer"),
11+
session: () => createQueryKey(namespace, "auth", "session"),
12+
}
13+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { QueryKey } from "../shared/query-keys"
2+
3+
export type AuthQueryKeys = {
4+
all: () => QueryKey
5+
customer: () => QueryKey
6+
session: () => QueryKey
7+
}
8+
9+
export type AuthService<
10+
TCustomer,
11+
TLoginInput,
12+
TRegisterInput,
13+
TUpdateInput,
14+
> = {
15+
getCustomer: (signal?: AbortSignal) => Promise<TCustomer | null>
16+
login: (input: TLoginInput) => Promise<unknown>
17+
logout: () => Promise<void>
18+
register: (input: TRegisterInput) => Promise<unknown>
19+
updateCustomer?: (input: TUpdateInput) => Promise<TCustomer>
20+
refresh?: () => Promise<unknown>
21+
}
22+
23+
export type AuthQueryInput = {
24+
enabled?: boolean
25+
}
26+
27+
export type UseAuthResult<TCustomer> = {
28+
customer: TCustomer | null
29+
isAuthenticated: boolean
30+
isLoading: boolean
31+
isFetching: boolean
32+
isSuccess: boolean
33+
error: string | null
34+
}
35+
36+
export type UseSuspenseAuthResult<TCustomer> = {
37+
customer: TCustomer | null
38+
isAuthenticated: boolean
39+
isFetching: boolean
40+
}

0 commit comments

Comments
 (0)