Merge branch 'renovate/playwright' into 'main'
[ProtonMail-WebClient.git] / packages / account / sso / AuthDevicesSettings.tsx
blob2e008d7a3d11a802fc98e615d1170b5109be170f
1 import { useEffect, useState } from 'react';
3 import type { Action, ThunkDispatch } from '@reduxjs/toolkit';
4 import { c } from 'ttag';
6 import { Button } from '@proton/atoms';
7 import {
8     Badge,
9     ButtonGroup,
10     Icon,
11     Prompt,
12     SettingsSectionWide,
13     Table,
14     TableBody,
15     TableHeader,
16     TableRow,
17     Time,
18     useErrorHandler,
19     useModalState,
20     useNotifications,
21 } from '@proton/components';
22 import useLoading, { useLoadingByKey } from '@proton/hooks/useLoading';
23 import { baseUseDispatch } from '@proton/react-redux-store';
24 import type { ProtonThunkArguments } from '@proton/redux-shared-store-types';
25 import {
26     type AuthDeviceOutput,
27     AuthDeviceState,
28     getLocalizedDeviceState,
29     getPersistedAuthDeviceDataByUser,
30 } from '@proton/shared/lib/keys/device';
31 import noop from '@proton/utils/noop';
33 import { userThunk } from '../user';
34 import { AuthDevicesModal } from './AuthDevicesModal';
35 import ConfirmRejectAuthDevice from './ConfirmRejectAuthDevice';
36 import ConfirmRemoveAuthDevice from './ConfirmRemoveAuthDevice';
37 import { deleteAllOtherAuthDevice, rejectAuthDevice } from './authDeviceActions';
38 import type { AuthDevicesState } from './authDevices';
39 import { useAuthDevices } from './authDevicesHooks';
41 const AuthDevicesSettings = () => {
42     const [authDevices, authDevicesLoading] = useAuthDevices();
43     const { createNotification } = useNotifications();
44     const dispatch = baseUseDispatch<ThunkDispatch<AuthDevicesState, ProtonThunkArguments, Action>>();
45     const [currentAuthDeviceID, setCurrentAuthDeviceID] = useState<string>('');
46     const [loadingMap, , setLoading] = useLoadingByKey();
47     const [loadingDeleteAll, withLoadingDeleteAll] = useLoading();
48     const [tmpAuthDevice, setTmpAuthDevice] = useState<AuthDeviceOutput | null>(null);
49     const [approveModal, setApproveModal, renderApproveModal] = useModalState();
50     const [rejectModal, setRejectModal, renderRejectModal] = useModalState();
51     const [removeModal, setRemoveModal, renderRemoveModal] = useModalState();
52     const [confirmDeleteAllModal, setConfirmDeleteAll, renderConfirmDeleteAll] = useModalState();
53     const errorHandler = useErrorHandler();
55     useEffect(() => {
56         const run = async () => {
57             const user = await dispatch(userThunk());
58             const currentDevice = getPersistedAuthDeviceDataByUser({ user });
59             if (currentDevice) {
60                 setCurrentAuthDeviceID(currentDevice.id);
61                 return;
62             }
63         };
64         run().catch(noop);
65     }, []);
67     const handleApproveDevice = (pendingAuthDevice: AuthDeviceOutput) => {
68         setTmpAuthDevice(pendingAuthDevice);
69         setApproveModal(true);
70     };
72     const handleConfirmRejectDevice = (pendingAuthDevice: AuthDeviceOutput) => {
73         setTmpAuthDevice(pendingAuthDevice);
74         setRejectModal(true);
75     };
77     const handleConfirmRemoveDevice = (pendingAuthDevice: AuthDeviceOutput) => {
78         setTmpAuthDevice(pendingAuthDevice);
79         setRemoveModal(true);
80     };
82     const handleRejectDevice = async (pendingAuthDevice: AuthDeviceOutput, type: 'reject' | 'delete') => {
83         try {
84             setLoading(pendingAuthDevice.ID, true);
85             await dispatch(rejectAuthDevice({ pendingAuthDevice, type }));
86             if (type === 'reject') {
87                 createNotification({ text: c('Success').t`Device rejected` });
88             } else {
89                 createNotification({ text: c('Success').t`Device deleted` });
90             }
91         } finally {
92             setLoading(pendingAuthDevice.ID, false);
93         }
94     };
96     const sortedAuthDevices = [...(authDevices || [])].sort((a, b) => {
97         return b.CreateTime - a.CreateTime;
98     });
100     const currentAuthDevice = sortedAuthDevices.find(({ ID }) => ID === currentAuthDeviceID);
102     return (
103         <>
104             {renderApproveModal && tmpAuthDevice && (
105                 <AuthDevicesModal
106                     pendingAuthDevice={tmpAuthDevice}
107                     {...approveModal}
108                     onExit={() => {
109                         approveModal.onExit();
110                         setTmpAuthDevice(null);
111                     }}
112                 />
113             )}
114             {renderRejectModal && tmpAuthDevice && (
115                 <ConfirmRejectAuthDevice
116                     pendingAuthDevice={tmpAuthDevice}
117                     onConfirm={() => {
118                         handleRejectDevice(tmpAuthDevice, 'reject').catch(errorHandler);
119                     }}
120                     {...rejectModal}
121                     onExit={() => {
122                         approveModal.onExit();
123                         setTmpAuthDevice(null);
124                     }}
125                 />
126             )}
127             {renderRemoveModal && tmpAuthDevice && (
128                 <ConfirmRemoveAuthDevice
129                     authDevice={tmpAuthDevice}
130                     onConfirm={() => {
131                         handleRejectDevice(tmpAuthDevice, 'delete').catch(errorHandler);
132                     }}
133                     {...removeModal}
134                     onExit={() => {
135                         approveModal.onExit();
136                         setTmpAuthDevice(null);
137                     }}
138                 />
139             )}
140             {renderConfirmDeleteAll && currentAuthDevice && (
141                 <Prompt
142                     {...confirmDeleteAllModal}
143                     title={c('sso').t`Remove all other devices`}
144                     buttons={[
145                         <Button
146                             color="norm"
147                             onClick={() => {
148                                 withLoadingDeleteAll(dispatch(deleteAllOtherAuthDevice({ currentAuthDevice }))).catch(
149                                     errorHandler
150                                 );
151                                 confirmDeleteAllModal.onClose();
152                             }}
153                         >
154                             {c('Action').t`Remove all other devices`}
155                         </Button>,
156                         <Button onClick={confirmDeleteAllModal.onClose}>{c('Action').t`Cancel`}</Button>,
157                     ]}
158                 >
159                     {c('sso').t`Do you want to remove all other devices than the current one?`}
160                 </Prompt>
161             )}
162             <SettingsSectionWide>
163                 {currentAuthDevice && sortedAuthDevices.length > 1 && (
164                     <div className="my-2">
165                         <Button shape="outline" onClick={() => setConfirmDeleteAll(true)} loading={loadingDeleteAll}>
166                             {c('Action').t`Remove all other devices`}
167                         </Button>
168                     </div>
169                 )}
170                 <Table hasActions responsive="cards">
171                     <TableHeader
172                         cells={[c('Title').t`Name`, c('Title').t`Created`, c('Title').t`State`, c('Title').t`Actions`]}
173                     />
174                     <TableBody loading={authDevicesLoading} colSpan={4}>
175                         {sortedAuthDevices.map((authDevice) => {
176                             const key = authDevice.ID;
177                             const isCurrentAuthDevice = authDevice.ID === currentAuthDeviceID;
178                             return (
179                                 <TableRow
180                                     key={key}
181                                     labels={[
182                                         c('Title').t`Name`,
183                                         c('Title').t`Created`,
184                                         c('Title').t`State`,
185                                         c('Title').t`Actions`,
186                                     ]}
187                                     cells={[
188                                         <>
189                                             <div className="text-bold">{authDevice.Name}</div>
190                                             <div className="color-weak">{authDevice.LocalizedClientName}</div>
191                                         </>,
192                                         <>
193                                             <Time format="PPp" key={1}>
194                                                 {authDevice.CreateTime}
195                                             </Time>
196                                         </>,
197                                         (() => {
198                                             if (isCurrentAuthDevice) {
199                                                 return (
200                                                     <Badge type="success" className="">
201                                                         {c('sso').t`Current device`}
202                                                     </Badge>
203                                                 );
204                                             }
205                                             if (authDevice.State === AuthDeviceState.Active) {
206                                                 return null;
207                                             }
208                                             return (
209                                                 <Badge
210                                                     type={(() => {
211                                                         switch (authDevice.State) {
212                                                             case AuthDeviceState.PendingAdminActivation:
213                                                             case AuthDeviceState.PendingActivation:
214                                                                 return 'warning';
215                                                             case AuthDeviceState.Rejected:
216                                                                 return 'error';
217                                                             default:
218                                                                 return 'origin';
219                                                         }
220                                                     })()}
221                                                     className=""
222                                                 >
223                                                     {getLocalizedDeviceState(authDevice.State)}
224                                                 </Badge>
225                                             );
226                                         })(),
227                                         (() => {
228                                             if (isCurrentAuthDevice) {
229                                                 return;
230                                             }
232                                             if (
233                                                 authDevice.ActivationToken &&
234                                                 (authDevice.State === AuthDeviceState.PendingActivation ||
235                                                     authDevice.State === AuthDeviceState.PendingAdminActivation)
236                                             ) {
237                                                 return (
238                                                     <ButtonGroup size="small" individualButtonColor={true}>
239                                                         <Button onClick={() => handleApproveDevice(authDevice)}>
240                                                             <div className="flex items-center flex-nowrap gap-1">
241                                                                 <Icon name="checkmark" className="shrink-0" />
242                                                                 <span className="text-ellipsis">
243                                                                     {c('sso').t`Approve`}
244                                                                 </span>
245                                                             </div>
246                                                         </Button>
247                                                         <Button
248                                                             color="danger"
249                                                             loading={loadingMap[authDevice.ID]}
250                                                             onClick={() => handleConfirmRejectDevice(authDevice)}
251                                                         >
252                                                             <div className="flex items-center flex-nowrap gap-1">
253                                                                 <Icon name="cross" className="shrink-0" />
254                                                                 <span className="text-ellipsis">
255                                                                     {c('sso').t`Reject`}
256                                                                 </span>
257                                                             </div>
258                                                         </Button>
259                                                     </ButtonGroup>
260                                                 );
261                                             }
263                                             return (
264                                                 <Button
265                                                     size="small"
266                                                     loading={loadingMap[authDevice.ID]}
267                                                     onClick={() => handleConfirmRemoveDevice(authDevice)}
268                                                 >
269                                                     {c('sso').t`Remove`}
270                                                 </Button>
271                                             );
272                                         })(),
273                                     ]}
274                                 />
275                             );
276                         })}
277                     </TableBody>
278                 </Table>
279             </SettingsSectionWide>
280         </>
281     );
284 export default AuthDevicesSettings;