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';
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';
26 type AuthDeviceOutput,
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();
56 const run = async () => {
57 const user = await dispatch(userThunk());
58 const currentDevice = getPersistedAuthDeviceDataByUser({ user });
60 setCurrentAuthDeviceID(currentDevice.id);
67 const handleApproveDevice = (pendingAuthDevice: AuthDeviceOutput) => {
68 setTmpAuthDevice(pendingAuthDevice);
69 setApproveModal(true);
72 const handleConfirmRejectDevice = (pendingAuthDevice: AuthDeviceOutput) => {
73 setTmpAuthDevice(pendingAuthDevice);
77 const handleConfirmRemoveDevice = (pendingAuthDevice: AuthDeviceOutput) => {
78 setTmpAuthDevice(pendingAuthDevice);
82 const handleRejectDevice = async (pendingAuthDevice: AuthDeviceOutput, type: 'reject' | 'delete') => {
84 setLoading(pendingAuthDevice.ID, true);
85 await dispatch(rejectAuthDevice({ pendingAuthDevice, type }));
86 if (type === 'reject') {
87 createNotification({ text: c('Success').t`Device rejected` });
89 createNotification({ text: c('Success').t`Device deleted` });
92 setLoading(pendingAuthDevice.ID, false);
96 const sortedAuthDevices = [...(authDevices || [])].sort((a, b) => {
97 return b.CreateTime - a.CreateTime;
100 const currentAuthDevice = sortedAuthDevices.find(({ ID }) => ID === currentAuthDeviceID);
104 {renderApproveModal && tmpAuthDevice && (
106 pendingAuthDevice={tmpAuthDevice}
109 approveModal.onExit();
110 setTmpAuthDevice(null);
114 {renderRejectModal && tmpAuthDevice && (
115 <ConfirmRejectAuthDevice
116 pendingAuthDevice={tmpAuthDevice}
118 handleRejectDevice(tmpAuthDevice, 'reject').catch(errorHandler);
122 approveModal.onExit();
123 setTmpAuthDevice(null);
127 {renderRemoveModal && tmpAuthDevice && (
128 <ConfirmRemoveAuthDevice
129 authDevice={tmpAuthDevice}
131 handleRejectDevice(tmpAuthDevice, 'delete').catch(errorHandler);
135 approveModal.onExit();
136 setTmpAuthDevice(null);
140 {renderConfirmDeleteAll && currentAuthDevice && (
142 {...confirmDeleteAllModal}
143 title={c('sso').t`Remove all other devices`}
148 withLoadingDeleteAll(dispatch(deleteAllOtherAuthDevice({ currentAuthDevice }))).catch(
151 confirmDeleteAllModal.onClose();
154 {c('Action').t`Remove all other devices`}
156 <Button onClick={confirmDeleteAllModal.onClose}>{c('Action').t`Cancel`}</Button>,
159 {c('sso').t`Do you want to remove all other devices than the current one?`}
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`}
170 <Table hasActions responsive="cards">
172 cells={[c('Title').t`Name`, c('Title').t`Created`, c('Title').t`State`, c('Title').t`Actions`]}
174 <TableBody loading={authDevicesLoading} colSpan={4}>
175 {sortedAuthDevices.map((authDevice) => {
176 const key = authDevice.ID;
177 const isCurrentAuthDevice = authDevice.ID === currentAuthDeviceID;
183 c('Title').t`Created`,
185 c('Title').t`Actions`,
189 <div className="text-bold">{authDevice.Name}</div>
190 <div className="color-weak">{authDevice.LocalizedClientName}</div>
193 <Time format="PPp" key={1}>
194 {authDevice.CreateTime}
198 if (isCurrentAuthDevice) {
200 <Badge type="success" className="">
201 {c('sso').t`Current device`}
205 if (authDevice.State === AuthDeviceState.Active) {
211 switch (authDevice.State) {
212 case AuthDeviceState.PendingAdminActivation:
213 case AuthDeviceState.PendingActivation:
215 case AuthDeviceState.Rejected:
223 {getLocalizedDeviceState(authDevice.State)}
228 if (isCurrentAuthDevice) {
233 authDevice.ActivationToken &&
234 (authDevice.State === AuthDeviceState.PendingActivation ||
235 authDevice.State === AuthDeviceState.PendingAdminActivation)
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`}
249 loading={loadingMap[authDevice.ID]}
250 onClick={() => handleConfirmRejectDevice(authDevice)}
252 <div className="flex items-center flex-nowrap gap-1">
253 <Icon name="cross" className="shrink-0" />
254 <span className="text-ellipsis">
266 loading={loadingMap[authDevice.ID]}
267 onClick={() => handleConfirmRemoveDevice(authDevice)}
279 </SettingsSectionWide>
284 export default AuthDevicesSettings;