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,
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?.loading ?? false);
38 const [error, setError] = useState(false);
39 const [requestId, setRequestId] = useState<string>(options?.requestId ?? '');
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 /** Initial loading state */
96 /** Initial request parameters
97 * - Pass `IntentDTO` if request key preparator requires parameters
98 * - Pass true to start tracking immediately if no parameters needed
99 * - Omit to skip initial tracking */
100 initial?: Parameters<T['requestID']>[0] extends infer U ? (U extends void ? true : U) : never;
101 /** Called when request is initiated. Receives intent action request metadata */
102 onStart?: (request: ActionRequestEntry<ReturnType<T['intent']>>) => MaybePromise<void>;
103 /** Called when request fails. Receives failure action request metadata */
104 onFailure?: (request: ActionRequestEntry<ReturnType<T['failure']>>) => MaybePromise<void>;
105 /** Called when request succeeds. Receives success action request metadata */
106 onSuccess?: (request: ActionRequestEntry<ReturnType<T['success']>>) => MaybePromise<void>;
109 export const useRequest = <T extends RequestFlow<any, any, any>>(
111 { initial, ...options }: UseRequestOptions<T> = {}
113 const [data, setData] = useState<Maybe<ActionRequestEntry<ReturnType<T['success']>>['data']>>();
115 const requestId = (() => {
116 if (!initial) return;
117 return actions.requestID(initial === true ? undefined : initial);
120 const req = useActionRequest<T['intent'], T['success'], T['failure']>(actions.intent, {
123 onSuccess: (req) => {
124 if (req.data) setData(req.data);
125 return options?.onSuccess?.(req);
129 return useMemo(() => ({ ...req, data }), [req, data]);