Skip to content

Commit 89c4816

Browse files
committed
feat(storefront-data): add orders and customer hooks
1 parent 26db477 commit 89c4816

9 files changed

Lines changed: 633 additions & 0 deletions

File tree

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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 { createCustomerQueryKeys } from "./query-keys"
10+
import type {
11+
CustomerAddressCreateInputBase,
12+
CustomerAddressListInputBase,
13+
CustomerAddressUpdateInputBase,
14+
CustomerAddressValidationResult,
15+
CustomerMutationOptions,
16+
CustomerProfileUpdateInputBase,
17+
CustomerQueryKeys,
18+
CustomerService,
19+
UseCustomerAddressesResult,
20+
} from "./types"
21+
22+
const handleAddressValidation = (result: CustomerAddressValidationResult) => {
23+
if (!result) {
24+
return
25+
}
26+
if (result instanceof Error) {
27+
throw result
28+
}
29+
if (Array.isArray(result)) {
30+
throw new Error(result.filter(Boolean).join(", "))
31+
}
32+
if (typeof result === "string") {
33+
throw new Error(result)
34+
}
35+
}
36+
37+
export type CreateCustomerHooksConfig<
38+
TCustomer,
39+
TAddress,
40+
TListInput extends CustomerAddressListInputBase,
41+
TListParams,
42+
TCreateInput extends CustomerAddressCreateInputBase,
43+
TCreateParams,
44+
TUpdateInput extends CustomerAddressUpdateInputBase,
45+
TUpdateParams,
46+
TUpdateCustomerInput extends CustomerProfileUpdateInputBase,
47+
TUpdateCustomerParams,
48+
> = {
49+
service: CustomerService<
50+
TCustomer,
51+
TAddress,
52+
TListParams,
53+
TCreateParams,
54+
TUpdateParams,
55+
TUpdateCustomerParams
56+
>
57+
buildListParams?: (input: TListInput) => TListParams
58+
buildCreateParams?: (input: TCreateInput) => TCreateParams
59+
buildUpdateParams?: (input: TUpdateInput) => TUpdateParams
60+
buildUpdateCustomerParams?: (
61+
input: TUpdateCustomerInput
62+
) => TUpdateCustomerParams
63+
normalizeCreateAddressInput?: (input: TCreateInput) => TCreateInput
64+
normalizeUpdateAddressInput?: (input: TUpdateInput) => TUpdateInput
65+
validateCreateAddressInput?: (
66+
input: TCreateInput
67+
) => CustomerAddressValidationResult
68+
validateUpdateAddressInput?: (
69+
input: TUpdateInput
70+
) => CustomerAddressValidationResult
71+
queryKeys?: CustomerQueryKeys<TListParams>
72+
queryKeyNamespace?: QueryNamespace
73+
cacheConfig?: CacheConfig
74+
}
75+
76+
export function createCustomerHooks<
77+
TCustomer,
78+
TAddress,
79+
TListInput extends CustomerAddressListInputBase,
80+
TListParams = TListInput,
81+
TCreateInput extends CustomerAddressCreateInputBase = CustomerAddressCreateInputBase,
82+
TCreateParams = TCreateInput,
83+
TUpdateInput extends CustomerAddressUpdateInputBase = CustomerAddressUpdateInputBase,
84+
TUpdateParams = TUpdateInput,
85+
TUpdateCustomerInput extends CustomerProfileUpdateInputBase = CustomerProfileUpdateInputBase,
86+
TUpdateCustomerParams = TUpdateCustomerInput,
87+
>({
88+
service,
89+
buildListParams,
90+
buildCreateParams,
91+
buildUpdateParams,
92+
buildUpdateCustomerParams,
93+
normalizeCreateAddressInput,
94+
normalizeUpdateAddressInput,
95+
validateCreateAddressInput,
96+
validateUpdateAddressInput,
97+
queryKeys,
98+
queryKeyNamespace = "storefront-data",
99+
cacheConfig,
100+
}: CreateCustomerHooksConfig<
101+
TCustomer,
102+
TAddress,
103+
TListInput,
104+
TListParams,
105+
TCreateInput,
106+
TCreateParams,
107+
TUpdateInput,
108+
TUpdateParams,
109+
TUpdateCustomerInput,
110+
TUpdateCustomerParams
111+
>) {
112+
const resolvedCacheConfig = cacheConfig ?? createCacheConfig()
113+
const resolvedQueryKeys =
114+
queryKeys ?? createCustomerQueryKeys<TListParams>(queryKeyNamespace)
115+
const buildList =
116+
buildListParams ?? ((input: TListInput) => input as unknown as TListParams)
117+
const buildCreate =
118+
buildCreateParams ??
119+
((input: TCreateInput) => input as unknown as TCreateParams)
120+
const buildUpdate =
121+
buildUpdateParams ??
122+
((input: TUpdateInput) => input as unknown as TUpdateParams)
123+
const buildUpdateCustomer =
124+
buildUpdateCustomerParams ??
125+
((input: TUpdateCustomerInput) => input as unknown as TUpdateCustomerParams)
126+
127+
function useCustomerAddresses(
128+
input: TListInput
129+
): UseCustomerAddressesResult<TAddress> {
130+
const listInput = { ...input } as TListInput & { enabled?: boolean }
131+
delete listInput.enabled
132+
const listParams = buildList(listInput)
133+
const queryKey = resolvedQueryKeys.addresses(listParams)
134+
const enabled = input.enabled ?? true
135+
136+
const { data, isLoading, isFetching, isSuccess, error } = useQuery({
137+
queryKey,
138+
queryFn: ({ signal }) => service.getAddresses(listParams, signal),
139+
enabled,
140+
...resolvedCacheConfig.userData,
141+
})
142+
143+
return {
144+
addresses: data?.addresses ?? [],
145+
isLoading,
146+
isFetching,
147+
isSuccess,
148+
error:
149+
error instanceof Error ? error.message : error ? String(error) : null,
150+
}
151+
}
152+
153+
function useSuspenseCustomerAddresses(input: TListInput) {
154+
const listInput = { ...input } as TListInput & { enabled?: boolean }
155+
delete listInput.enabled
156+
const listParams = buildList(listInput)
157+
const { data, isFetching } = useSuspenseQuery({
158+
queryKey: resolvedQueryKeys.addresses(listParams),
159+
queryFn: ({ signal }) => service.getAddresses(listParams, signal),
160+
...resolvedCacheConfig.userData,
161+
})
162+
163+
return {
164+
addresses: data?.addresses ?? [],
165+
isFetching,
166+
}
167+
}
168+
169+
function useCreateCustomerAddress(
170+
options?: CustomerMutationOptions<TAddress, TCreateInput>
171+
) {
172+
const queryClient = useQueryClient()
173+
return useMutation({
174+
mutationFn: (input: TCreateInput) => {
175+
const normalized = normalizeCreateAddressInput
176+
? normalizeCreateAddressInput(input)
177+
: input
178+
handleAddressValidation(validateCreateAddressInput?.(normalized))
179+
return service.createAddress(buildCreate(normalized))
180+
},
181+
onSuccess: (address, variables) => {
182+
queryClient.invalidateQueries({
183+
queryKey: resolvedQueryKeys.all(),
184+
})
185+
options?.onSuccess?.(address, variables)
186+
},
187+
onError: (error) => {
188+
options?.onError?.(error)
189+
},
190+
})
191+
}
192+
193+
function useUpdateCustomerAddress(
194+
options?: CustomerMutationOptions<TAddress, TUpdateInput>
195+
) {
196+
const queryClient = useQueryClient()
197+
return useMutation({
198+
mutationFn: (input: TUpdateInput) => {
199+
const addressId = input.addressId
200+
if (!addressId) {
201+
throw new Error("Address id is required")
202+
}
203+
const normalized = normalizeUpdateAddressInput
204+
? normalizeUpdateAddressInput(input)
205+
: input
206+
const updateInput = {
207+
...(normalized as TUpdateInput & { addressId?: string }),
208+
}
209+
delete updateInput.addressId
210+
handleAddressValidation(validateUpdateAddressInput?.(normalized))
211+
return service.updateAddress(addressId, buildUpdate(updateInput))
212+
},
213+
onSuccess: (address, variables) => {
214+
queryClient.invalidateQueries({
215+
queryKey: resolvedQueryKeys.all(),
216+
})
217+
options?.onSuccess?.(address, variables)
218+
},
219+
onError: (error) => {
220+
options?.onError?.(error)
221+
},
222+
})
223+
}
224+
225+
function useDeleteCustomerAddress(
226+
options?: CustomerMutationOptions<void, { addressId?: string }>
227+
) {
228+
const queryClient = useQueryClient()
229+
return useMutation<void, unknown, { addressId?: string }>({
230+
mutationFn: ({ addressId }) => {
231+
if (!addressId) {
232+
throw new Error("Address id is required")
233+
}
234+
return service.deleteAddress(addressId)
235+
},
236+
onSuccess: (data, variables) => {
237+
queryClient.invalidateQueries({
238+
queryKey: resolvedQueryKeys.all(),
239+
})
240+
options?.onSuccess?.(data, variables)
241+
},
242+
onError: (error) => {
243+
options?.onError?.(error)
244+
},
245+
})
246+
}
247+
248+
function useUpdateCustomer(
249+
options?: CustomerMutationOptions<TCustomer, TUpdateCustomerInput>
250+
) {
251+
const queryClient = useQueryClient()
252+
return useMutation({
253+
mutationFn: (input: TUpdateCustomerInput) => {
254+
if (!service.updateCustomer) {
255+
throw new Error("updateCustomer service is not configured")
256+
}
257+
return service.updateCustomer(buildUpdateCustomer(input))
258+
},
259+
onSuccess: (customer, variables) => {
260+
queryClient.invalidateQueries({
261+
queryKey: resolvedQueryKeys.profile(),
262+
})
263+
options?.onSuccess?.(customer, variables)
264+
},
265+
onError: (error) => {
266+
options?.onError?.(error)
267+
},
268+
})
269+
}
270+
271+
return {
272+
useCustomerAddresses,
273+
useSuspenseCustomerAddresses,
274+
useCreateCustomerAddress,
275+
useUpdateCustomerAddress,
276+
useDeleteCustomerAddress,
277+
useUpdateCustomer,
278+
}
279+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export { createCustomerHooks } from "./hooks"
2+
export type {
3+
CustomerAddressInputBase,
4+
CustomerAddressCreateInputBase,
5+
CustomerAddressUpdateInputBase,
6+
CustomerAddressListInputBase,
7+
CustomerAddressListResponse,
8+
CustomerAddressValidationResult,
9+
CustomerMutationOptions,
10+
CustomerProfileUpdateInputBase,
11+
CustomerQueryKeys,
12+
CustomerService,
13+
UseCustomerAddressesResult,
14+
} from "./types"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { createQueryKey } from "../shared/query-keys"
2+
import type { QueryNamespace } from "../shared/query-keys"
3+
import type { CustomerQueryKeys } from "./types"
4+
5+
export function createCustomerQueryKeys<TListParams>(
6+
namespace: QueryNamespace
7+
): CustomerQueryKeys<TListParams> {
8+
return {
9+
all: () => createQueryKey(namespace, "customer"),
10+
profile: () => createQueryKey(namespace, "customer", "profile"),
11+
addresses: (params) =>
12+
createQueryKey(namespace, "customer", "addresses", params),
13+
}
14+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import type { QueryKey } from "../shared/query-keys"
2+
3+
export type CustomerAddressListInputBase = {
4+
enabled?: boolean
5+
}
6+
7+
export type CustomerAddressInputBase = {
8+
first_name?: string
9+
last_name?: string
10+
company?: string
11+
address_1?: string
12+
address_2?: string
13+
city?: string
14+
province?: string
15+
postal_code?: string
16+
country_code?: string
17+
phone?: string
18+
is_default_shipping?: boolean
19+
is_default_billing?: boolean
20+
metadata?: Record<string, unknown>
21+
}
22+
23+
export type CustomerAddressCreateInputBase = CustomerAddressInputBase
24+
25+
export type CustomerAddressUpdateInputBase = CustomerAddressInputBase & {
26+
addressId?: string
27+
}
28+
29+
export type CustomerProfileUpdateInputBase = {
30+
metadata?: Record<string, unknown>
31+
}
32+
33+
export type CustomerAddressListResponse<TAddress> = {
34+
addresses: TAddress[]
35+
}
36+
37+
export type CustomerService<
38+
TCustomer,
39+
TAddress,
40+
TListParams,
41+
TCreateParams,
42+
TUpdateParams,
43+
TUpdateCustomerParams,
44+
> = {
45+
getAddresses: (
46+
params: TListParams,
47+
signal?: AbortSignal
48+
) => Promise<CustomerAddressListResponse<TAddress>>
49+
createAddress: (params: TCreateParams) => Promise<TAddress>
50+
updateAddress: (addressId: string, params: TUpdateParams) => Promise<TAddress>
51+
deleteAddress: (addressId: string) => Promise<void>
52+
updateCustomer?: (params: TUpdateCustomerParams) => Promise<TCustomer>
53+
}
54+
55+
export type CustomerQueryKeys<TListParams> = {
56+
all: () => QueryKey
57+
profile: () => QueryKey
58+
addresses: (params: TListParams) => QueryKey
59+
}
60+
61+
export type UseCustomerAddressesResult<TAddress> = {
62+
addresses: TAddress[]
63+
isLoading: boolean
64+
isFetching: boolean
65+
isSuccess: boolean
66+
error: string | null
67+
}
68+
69+
export type CustomerMutationOptions<TData, TVariables> = {
70+
onSuccess?: (data: TData, variables: TVariables) => void
71+
onError?: (error: unknown) => void
72+
}
73+
74+
export type CustomerAddressValidationResult =
75+
| string
76+
| string[]
77+
| Error
78+
| null
79+
| undefined

libs/storefront-data/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ export * from "./regions"
88
export * from "./auth"
99
export * from "./cart"
1010
export * from "./checkout"
11+
export * from "./orders"
12+
export * from "./customers"

0 commit comments

Comments
 (0)