@outseta/react 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -7
- package/dist/index.js +514 -148
- package/package.json +13 -5
- package/src/components/AuthButton.tsx +33 -0
- package/src/components/AuthEmbed.tsx +4 -0
- package/src/components/EmailListButton.tsx +38 -0
- package/src/components/EmailListEmbed.tsx +28 -0
- package/src/components/EmailListForm.tsx +34 -0
- package/src/components/LeadCaptureButton.tsx +38 -0
- package/src/components/LeadCaptureEmbed.tsx +28 -0
- package/src/components/LoginButton.tsx +18 -0
- package/src/components/LogoutButton.tsx +8 -1
- package/src/components/OutsetaProvider.tsx +36 -7
- package/src/components/ProfileButton.tsx +35 -0
- package/src/components/ProfileEmbed.tsx +8 -2
- package/src/components/PurchaseAddonButton.tsx +8 -4
- package/src/components/SignupButton.tsx +18 -0
- package/src/components/SupportButton.tsx +35 -0
- package/src/components/SupportEmbed.tsx +17 -0
- package/src/components/index.ts +27 -0
- package/src/components/types.ts +33 -4
- package/src/components/utils.ts +13 -0
- package/src/generated/activity/activity.ts +2 -2
- package/src/generated/attribute/attribute.ts +2 -2
- package/src/generated/billing/billing.ts +2 -2
- package/src/generated/crm/crm.ts +2 -2
- package/src/generated/email/email.ts +2 -2
- package/src/generated/outsetaAPI.schemas.ts +5 -0
- package/src/generated/public/public.ts +2 -2
- package/src/generated/support/support.ts +2 -2
- package/src/index.ts +1 -1
- package/src/mutator.ts +3 -3
- package/src/query.test.ts +276 -0
- package/src/query.ts +352 -0
package/src/query.ts
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal vanilla-React implementation of the small slice of the TanStack
|
|
5
|
+
* Query v5 API surface that Orval's generated hooks depend on.
|
|
6
|
+
*
|
|
7
|
+
* This lets `@outseta/react` ship the generated `useX` hooks without a
|
|
8
|
+
* `@tanstack/react-query` dependency or a `QueryClientProvider`. It is a
|
|
9
|
+
* lightweight fetch-on-mount implementation: each hook instance owns its own
|
|
10
|
+
* request lifecycle. There is intentionally no cross-component caching,
|
|
11
|
+
* request deduplication, or background refetching — reach for TanStack Query
|
|
12
|
+
* (or another data layer) directly if you need those.
|
|
13
|
+
*
|
|
14
|
+
* `scripts/post-generate.mjs` rewrites the generated `@tanstack/react-query`
|
|
15
|
+
* imports to point at this module.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type QueryKey = ReadonlyArray<unknown>;
|
|
19
|
+
|
|
20
|
+
export interface QueryFunctionContext<TQueryKey extends QueryKey = QueryKey> {
|
|
21
|
+
signal: AbortSignal;
|
|
22
|
+
queryKey: TQueryKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type QueryFunction<
|
|
26
|
+
T = unknown,
|
|
27
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
28
|
+
> = (context: QueryFunctionContext<TQueryKey>) => T | Promise<T>;
|
|
29
|
+
|
|
30
|
+
export type MutationFunction<TData = unknown, TVariables = unknown> = (
|
|
31
|
+
variables: TVariables,
|
|
32
|
+
) => Promise<TData>;
|
|
33
|
+
|
|
34
|
+
export type QueryStatus = "pending" | "error" | "success";
|
|
35
|
+
export type MutationStatus = "idle" | "pending" | "error" | "success";
|
|
36
|
+
|
|
37
|
+
export interface UseQueryOptions<
|
|
38
|
+
TQueryFnData = unknown,
|
|
39
|
+
// `TError` is unused here but kept so the generated call sites'
|
|
40
|
+
// `UseQueryOptions<TQueryFnData, TError, TData>` type arguments line up.
|
|
41
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
42
|
+
TError = unknown,
|
|
43
|
+
TData = TQueryFnData,
|
|
44
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
45
|
+
> {
|
|
46
|
+
queryKey?: TQueryKey;
|
|
47
|
+
queryFn?: QueryFunction<TQueryFnData, TQueryKey>;
|
|
48
|
+
/** Skip fetching while `false`. Defaults to `true`. */
|
|
49
|
+
enabled?: boolean;
|
|
50
|
+
/** Transform the raw response before it is returned as `data`. */
|
|
51
|
+
select?: (data: TQueryFnData) => TData;
|
|
52
|
+
initialData?: TData | (() => TData);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface UseQueryResult<TData = unknown, TError = unknown> {
|
|
56
|
+
data: TData | undefined;
|
|
57
|
+
error: TError | null;
|
|
58
|
+
status: QueryStatus;
|
|
59
|
+
isPending: boolean;
|
|
60
|
+
isLoading: boolean;
|
|
61
|
+
isError: boolean;
|
|
62
|
+
isSuccess: boolean;
|
|
63
|
+
isFetching: boolean;
|
|
64
|
+
refetch: () => Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface MutateOptions<
|
|
68
|
+
TData = unknown,
|
|
69
|
+
TError = unknown,
|
|
70
|
+
TVariables = void,
|
|
71
|
+
TContext = unknown,
|
|
72
|
+
> {
|
|
73
|
+
onSuccess?: (
|
|
74
|
+
data: TData,
|
|
75
|
+
variables: TVariables,
|
|
76
|
+
context: TContext | undefined,
|
|
77
|
+
) => unknown;
|
|
78
|
+
onError?: (
|
|
79
|
+
error: TError,
|
|
80
|
+
variables: TVariables,
|
|
81
|
+
context: TContext | undefined,
|
|
82
|
+
) => unknown;
|
|
83
|
+
onSettled?: (
|
|
84
|
+
data: TData | undefined,
|
|
85
|
+
error: TError | null,
|
|
86
|
+
variables: TVariables,
|
|
87
|
+
context: TContext | undefined,
|
|
88
|
+
) => unknown;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface UseMutationOptions<
|
|
92
|
+
TData = unknown,
|
|
93
|
+
TError = unknown,
|
|
94
|
+
TVariables = void,
|
|
95
|
+
TContext = unknown,
|
|
96
|
+
> extends MutateOptions<TData, TError, TVariables, TContext> {
|
|
97
|
+
mutationFn?: MutationFunction<TData, TVariables>;
|
|
98
|
+
mutationKey?: unknown[];
|
|
99
|
+
onMutate?: (
|
|
100
|
+
variables: TVariables,
|
|
101
|
+
) => Promise<TContext | undefined> | TContext | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface UseMutationResult<
|
|
105
|
+
TData = unknown,
|
|
106
|
+
TError = unknown,
|
|
107
|
+
TVariables = void,
|
|
108
|
+
TContext = unknown,
|
|
109
|
+
> {
|
|
110
|
+
data: TData | undefined;
|
|
111
|
+
error: TError | null;
|
|
112
|
+
variables: TVariables | undefined;
|
|
113
|
+
status: MutationStatus;
|
|
114
|
+
isIdle: boolean;
|
|
115
|
+
isPending: boolean;
|
|
116
|
+
isError: boolean;
|
|
117
|
+
isSuccess: boolean;
|
|
118
|
+
mutate: (
|
|
119
|
+
variables: TVariables,
|
|
120
|
+
options?: MutateOptions<TData, TError, TVariables, TContext>,
|
|
121
|
+
) => void;
|
|
122
|
+
mutateAsync: (
|
|
123
|
+
variables: TVariables,
|
|
124
|
+
options?: MutateOptions<TData, TError, TVariables, TContext>,
|
|
125
|
+
) => Promise<TData>;
|
|
126
|
+
reset: () => void;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
interface QueryState<TData, TError> {
|
|
130
|
+
status: QueryStatus;
|
|
131
|
+
data: TData | undefined;
|
|
132
|
+
error: TError | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Vanilla-React `useQuery`. Fetches on mount, re-fetches when the serialized
|
|
137
|
+
* `queryKey` changes, and exposes a `refetch()` for manual reloads.
|
|
138
|
+
*/
|
|
139
|
+
export function useQuery<
|
|
140
|
+
TQueryFnData = unknown,
|
|
141
|
+
TError = unknown,
|
|
142
|
+
TData = TQueryFnData,
|
|
143
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
144
|
+
>(
|
|
145
|
+
options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
|
146
|
+
): UseQueryResult<TData, TError> {
|
|
147
|
+
const { queryKey, enabled = true } = options;
|
|
148
|
+
const keyHash = JSON.stringify(queryKey ?? []);
|
|
149
|
+
|
|
150
|
+
const optionsRef = useRef(options);
|
|
151
|
+
optionsRef.current = options;
|
|
152
|
+
|
|
153
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
154
|
+
const mountedRef = useRef(true);
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
mountedRef.current = true;
|
|
157
|
+
return () => {
|
|
158
|
+
mountedRef.current = false;
|
|
159
|
+
};
|
|
160
|
+
}, []);
|
|
161
|
+
|
|
162
|
+
const [state, setState] = useState<QueryState<TData, TError>>(() => {
|
|
163
|
+
const { initialData } = optionsRef.current;
|
|
164
|
+
const resolved =
|
|
165
|
+
typeof initialData === "function"
|
|
166
|
+
? (initialData as () => TData)()
|
|
167
|
+
: initialData;
|
|
168
|
+
return resolved !== undefined
|
|
169
|
+
? { status: "success", data: resolved, error: null }
|
|
170
|
+
: { status: "pending", data: undefined, error: null };
|
|
171
|
+
});
|
|
172
|
+
const [isFetching, setIsFetching] = useState(false);
|
|
173
|
+
|
|
174
|
+
const runQuery = useCallback(async (): Promise<void> => {
|
|
175
|
+
const { queryFn, queryKey: key, select } = optionsRef.current;
|
|
176
|
+
if (!queryFn) return;
|
|
177
|
+
|
|
178
|
+
abortRef.current?.abort();
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
abortRef.current = controller;
|
|
181
|
+
setIsFetching(true);
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const raw = await queryFn({
|
|
185
|
+
signal: controller.signal,
|
|
186
|
+
queryKey: (key ?? []) as TQueryKey,
|
|
187
|
+
});
|
|
188
|
+
if (controller.signal.aborted || !mountedRef.current) return;
|
|
189
|
+
const data = select ? select(raw) : (raw as unknown as TData);
|
|
190
|
+
setState({ status: "success", data, error: null });
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (controller.signal.aborted || !mountedRef.current) return;
|
|
193
|
+
setState((prev) => ({
|
|
194
|
+
status: "error",
|
|
195
|
+
data: prev.data,
|
|
196
|
+
error: error as TError,
|
|
197
|
+
}));
|
|
198
|
+
} finally {
|
|
199
|
+
if (!controller.signal.aborted && mountedRef.current) {
|
|
200
|
+
setIsFetching(false);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
if (!enabled) return;
|
|
207
|
+
void runQuery();
|
|
208
|
+
return () => {
|
|
209
|
+
abortRef.current?.abort();
|
|
210
|
+
};
|
|
211
|
+
}, [enabled, keyHash, runQuery]);
|
|
212
|
+
|
|
213
|
+
const refetch = useCallback(async (): Promise<void> => {
|
|
214
|
+
await runQuery();
|
|
215
|
+
}, [runQuery]);
|
|
216
|
+
|
|
217
|
+
const status = state.status;
|
|
218
|
+
return {
|
|
219
|
+
data: state.data,
|
|
220
|
+
error: state.error,
|
|
221
|
+
status,
|
|
222
|
+
isPending: status === "pending",
|
|
223
|
+
isLoading: status === "pending" && isFetching,
|
|
224
|
+
isError: status === "error",
|
|
225
|
+
isSuccess: status === "success",
|
|
226
|
+
isFetching,
|
|
227
|
+
refetch,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
interface MutationState<TData, TError, TVariables> {
|
|
232
|
+
status: MutationStatus;
|
|
233
|
+
data: TData | undefined;
|
|
234
|
+
error: TError | null;
|
|
235
|
+
variables: TVariables | undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Vanilla-React `useMutation`. Returns `mutate`/`mutateAsync` plus the current
|
|
240
|
+
* status flags, and invokes the `onMutate`/`onSuccess`/`onError`/`onSettled`
|
|
241
|
+
* lifecycle callbacks (both hook-level and per-call).
|
|
242
|
+
*/
|
|
243
|
+
export function useMutation<
|
|
244
|
+
TData = unknown,
|
|
245
|
+
TError = unknown,
|
|
246
|
+
TVariables = void,
|
|
247
|
+
TContext = unknown,
|
|
248
|
+
>(
|
|
249
|
+
options: UseMutationOptions<TData, TError, TVariables, TContext>,
|
|
250
|
+
): UseMutationResult<TData, TError, TVariables, TContext> {
|
|
251
|
+
const optionsRef = useRef(options);
|
|
252
|
+
optionsRef.current = options;
|
|
253
|
+
|
|
254
|
+
const mountedRef = useRef(true);
|
|
255
|
+
useEffect(() => {
|
|
256
|
+
mountedRef.current = true;
|
|
257
|
+
return () => {
|
|
258
|
+
mountedRef.current = false;
|
|
259
|
+
};
|
|
260
|
+
}, []);
|
|
261
|
+
|
|
262
|
+
const [state, setState] = useState<MutationState<TData, TError, TVariables>>({
|
|
263
|
+
status: "idle",
|
|
264
|
+
data: undefined,
|
|
265
|
+
error: null,
|
|
266
|
+
variables: undefined,
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const mutateAsync = useCallback(
|
|
270
|
+
async (
|
|
271
|
+
variables: TVariables,
|
|
272
|
+
mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>,
|
|
273
|
+
): Promise<TData> => {
|
|
274
|
+
const opts = optionsRef.current;
|
|
275
|
+
const { mutationFn } = opts;
|
|
276
|
+
if (!mutationFn) {
|
|
277
|
+
throw new Error("@outseta/react: useMutation called without a mutationFn.");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (mountedRef.current) {
|
|
281
|
+
setState({
|
|
282
|
+
status: "pending",
|
|
283
|
+
data: undefined,
|
|
284
|
+
error: null,
|
|
285
|
+
variables,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let context: TContext | undefined;
|
|
290
|
+
try {
|
|
291
|
+
context = await opts.onMutate?.(variables);
|
|
292
|
+
const data = await mutationFn(variables);
|
|
293
|
+
if (mountedRef.current) {
|
|
294
|
+
setState({ status: "success", data, error: null, variables });
|
|
295
|
+
}
|
|
296
|
+
await opts.onSuccess?.(data, variables, context);
|
|
297
|
+
await mutateOptions?.onSuccess?.(data, variables, context);
|
|
298
|
+
await opts.onSettled?.(data, null, variables, context);
|
|
299
|
+
await mutateOptions?.onSettled?.(data, null, variables, context);
|
|
300
|
+
return data;
|
|
301
|
+
} catch (error) {
|
|
302
|
+
const typedError = error as TError;
|
|
303
|
+
if (mountedRef.current) {
|
|
304
|
+
setState({
|
|
305
|
+
status: "error",
|
|
306
|
+
data: undefined,
|
|
307
|
+
error: typedError,
|
|
308
|
+
variables,
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
await opts.onError?.(typedError, variables, context);
|
|
312
|
+
await mutateOptions?.onError?.(typedError, variables, context);
|
|
313
|
+
await opts.onSettled?.(undefined, typedError, variables, context);
|
|
314
|
+
await mutateOptions?.onSettled?.(undefined, typedError, variables, context);
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
[],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const mutate = useCallback(
|
|
322
|
+
(
|
|
323
|
+
variables: TVariables,
|
|
324
|
+
mutateOptions?: MutateOptions<TData, TError, TVariables, TContext>,
|
|
325
|
+
): void => {
|
|
326
|
+
void mutateAsync(variables, mutateOptions).catch(() => {
|
|
327
|
+
// Errors are surfaced through the returned `error`/`status`; swallow
|
|
328
|
+
// the rejection so `mutate` stays fire-and-forget like TanStack Query.
|
|
329
|
+
});
|
|
330
|
+
},
|
|
331
|
+
[mutateAsync],
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
const reset = useCallback((): void => {
|
|
335
|
+
setState({ status: "idle", data: undefined, error: null, variables: undefined });
|
|
336
|
+
}, []);
|
|
337
|
+
|
|
338
|
+
const status = state.status;
|
|
339
|
+
return {
|
|
340
|
+
data: state.data,
|
|
341
|
+
error: state.error,
|
|
342
|
+
variables: state.variables,
|
|
343
|
+
status,
|
|
344
|
+
isIdle: status === "idle",
|
|
345
|
+
isPending: status === "pending",
|
|
346
|
+
isError: status === "error",
|
|
347
|
+
isSuccess: status === "success",
|
|
348
|
+
mutate,
|
|
349
|
+
mutateAsync,
|
|
350
|
+
reset,
|
|
351
|
+
};
|
|
352
|
+
}
|