1 import type { FC, PropsWithChildren } from 'react';
2 import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
4 import { c } from 'ttag';
6 import { usePassCore } from '@proton/pass/components/Core/PassCoreProvider';
7 import { useInviteContext } from '@proton/pass/components/Invite/InviteContext';
8 import { PendingNewUsersApprovalModal } from '@proton/pass/components/Share/PendingNewUsersApprovalModal';
9 import { PendingShareAccessModal } from '@proton/pass/components/Share/PendingShareAccessModal';
10 import type { SpotlightMessageDefinition } from '@proton/pass/components/Spotlight/SpotlightContent';
11 import type { UpsellType } from '@proton/pass/components/Upsell/UpsellingModal';
12 import { UpsellingModal } from '@proton/pass/components/Upsell/UpsellingModal';
13 import type { UpsellRef } from '@proton/pass/constants';
14 import { type MaybeNull, SpotlightMessage } from '@proton/pass/types';
15 import noop from '@proton/utils/noop';
17 import { InviteIcon } from './SpotlightIcon';
19 type UpsellingState = { type: UpsellType; upsellRef: UpsellRef };
21 type SpotlightState = {
23 upselling: MaybeNull<UpsellingState>;
24 pendingShareAccess: boolean;
25 message: MaybeNull<SpotlightMessageDefinition>;
28 export type SpotlightContextValue = {
29 /** Acknowledges the provided spotlight message type.
30 * Resets the SpotlightContext's current message to `null` */
31 acknowledge: (messageType: SpotlightMessage) => void;
32 /** Controls the Pending Share Access modal */
33 setPendingShareAccess: (value: boolean) => void;
34 /** Controls the Upselling modal */
35 setUpselling: (value: MaybeNull<UpsellingState>) => void;
36 /** Sets the current message - if an invite */
37 setSpotlight: (message: MaybeNull<SpotlightMessageDefinition>) => void;
38 state: SpotlightState;
41 const INITIAL_STATE: SpotlightState = { open: false, message: null, upselling: null, pendingShareAccess: false };
43 export const SpotlightContext = createContext<SpotlightContextValue>({
46 setPendingShareAccess: noop,
51 export const SpotlightProvider: FC<PropsWithChildren> = ({ children }) => {
52 const { spotlight } = usePassCore();
53 const { latestInvite, respondToInvite } = useInviteContext();
55 const [state, setState] = useState<SpotlightState>(INITIAL_STATE);
56 const [spotlightMessage, setSpotlightMessage] = useState<MaybeNull<SpotlightMessageDefinition>>(null);
58 const timer = useRef<NodeJS.Timeout>();
59 const messageRef = useRef(state.message);
60 messageRef.current = state.message;
62 const closeUpselling = () => {
63 state.message?.onClose?.();
64 setState((prev) => ({ ...prev, upselling: null }));
67 const closePendingShareAccess = () => {
68 state.message?.onClose?.();
69 setState((prev) => ({ ...prev, pendingShareAccess: false }));
72 const setMessage = useCallback((next: MaybeNull<SpotlightMessageDefinition>) => {
73 if (messageRef.current?.id !== next?.id) {
74 setState((curr) => ({ ...curr, open: false }));
75 timer.current = setTimeout(
76 () => setState((curr) => ({ ...curr, message: next, open: next !== null })),
82 const acknowledge = useCallback((spotlightMessageType: SpotlightMessage) => {
83 void spotlight.acknowledge(spotlightMessageType);
84 setSpotlightMessage(null);
87 const ctx = useMemo<SpotlightContextValue>(
90 setPendingShareAccess: (pendingShareAccess) => setState((prev) => ({ ...prev, pendingShareAccess })),
91 setUpselling: (upselling) => setState((prev) => ({ ...prev, upselling })),
92 setSpotlight: setSpotlightMessage,
98 const inviteMessage = useMemo<MaybeNull<SpotlightMessageDefinition>>(
100 latestInvite && !latestInvite.fromNewUser
102 type: SpotlightMessage.NOOP,
104 id: latestInvite.token,
107 title: c('Title').t`Vault shared with you`,
108 message: c('Info').t`You're invited to a vault.`,
111 label: c('Label').t`View details`,
113 onClick: () => respondToInvite(latestInvite),
120 useEffect(() => () => clearTimeout(timer.current), []);
123 /** For now, always prioritize invites over spotlight messages */
124 const activeMessage = inviteMessage ?? spotlightMessage;
125 setMessage(activeMessage);
126 }, [inviteMessage, spotlightMessage]);
129 <SpotlightContext.Provider value={ctx}>
132 {state.upselling && (
135 onClose={closeUpselling}
136 upsellType={state.upselling.type}
137 upsellRef={state.upselling.upsellRef}
141 <PendingShareAccessModal open={state.pendingShareAccess} onClose={closePendingShareAccess} />
142 <PendingNewUsersApprovalModal />
143 </SpotlightContext.Provider>
147 export const useSpotlight = () => useContext(SpotlightContext);