1 import { useEffect, useMemo, useRef, useState } from 'react';
2 import { useDispatch, useSelector } from 'react-redux';
4 import type { ActionCreatorWithPreparedPayload } from '@reduxjs/toolkit';
5 import type { Action } from 'redux';
7 import { withRevalidate } from '@proton/pass/store/request/enhancers';
8 import type { RequestFlow } from '@proton/pass/store/request/flow';
9 import type { ActionRequestEntry, RequestMeta, WithRequest } from '@proton/pass/store/request/types';
10 import { selectRequest } from '@proton/pass/store/selectors';
11 import type { Maybe, MaybePromise } from '@proton/pass/types/utils';
13 export type UseActionRequestOptions<
14 IntentAction extends WithRequest<Action, 'start', any> = any,
15 SuccessAction extends WithRequest<Action, 'success', any> = any,
16 FailureAction extends WithRequest<Action, 'failure', any> = any,
18 initialLoading?: boolean;
19 initialRequestId?: string;
20 onStart?: (request: ActionRequestEntry<IntentAction>) => MaybePromise<void>;
21 onSuccess?: (request: ActionRequestEntry<SuccessAction>) => MaybePromise<void>;
22 onFailure?: (request: ActionRequestEntry<FailureAction>) => MaybePromise<void>;
25 /* `options` is wrapped in a ref to avoid setting it as
26 * a dependency to the status change effect. We only want
27 * to trigger the callbacks once. */
28 export const useActionRequest = <
29 IntentAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'start', any>>,
30 SuccessAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'success', any>>,
31 FailureAction extends ActionCreatorWithPreparedPayload<any[], any, string, never, RequestMeta<'failure', any>>,
34 options?: UseActionRequestOptions<ReturnType<IntentAction>, ReturnType<SuccessAction>, ReturnType<FailureAction>>
36 const dispatch = useDispatch();
37 const [loading, setLoading] = useState(options?.initialLoading ?? false);
38 const [error, setError] = useState(false);
39 const [requestId, setRequestId] = useState<string>(options?.initialRequestId ?? '');
40 const request = useSelector(selectRequest(requestId));
42 const optionsRef = useRef(options);
43 optionsRef.current = options;
45 const progress = (() => {
46 if (!request) return 0;
47 return request?.status === 'start' ? (request.progress ?? 0) : 100;
51 if (!request) return setLoading(false);
53 switch (request.status) {
57 void optionsRef.current?.onStart?.(request as any);
62 void optionsRef.current?.onSuccess?.(request as any);
67 void optionsRef.current?.onFailure?.(request as any);
72 return useMemo(() => {
73 const actionCreator = (...args: Parameters<IntentAction>) => {
74 const action = intent(...args);
75 setRequestId(action.meta.request.id);
80 dispatch: (...args: Parameters<IntentAction>) => {
81 dispatch(actionCreator(...args));
83 revalidate: (...args: Parameters<IntentAction>) => {
84 dispatch(withRevalidate(actionCreator(...args)));
90 }, [request, progress, loading]);
93 type UseRequestOptions<T extends RequestFlow<any, any, any>> = {
94 initialLoading?: boolean;
95 initialRequestId?: string;
96 onStart?: (request: ActionRequestEntry<ReturnType<T['intent']>>) => MaybePromise<void>;
97 onFailure?: (request: ActionRequestEntry<ReturnType<T['failure']>>) => MaybePromise<void>;
98 onSuccess?: (request: ActionRequestEntry<ReturnType<T['success']>>) => MaybePromise<void>;
101 export const useRequest = <T extends RequestFlow<any, any, any>>(actions: T, options: UseRequestOptions<T>) => {
102 const [data, setData] = useState<Maybe<ActionRequestEntry<ReturnType<T['success']>>['data']>>();
104 const req = useActionRequest<T['intent'], T['success'], T['failure']>(actions.intent, {
106 onSuccess: (req) => {
107 if (req.data) setData(req.data);
108 return options?.onSuccess?.(req);
112 return useMemo(() => ({ ...req, data }), [req, data]);