1 import { CryptoProxy } from '@proton/crypto';
2 import type { SharedStartListening } from '@proton/redux-shared-store-types';
3 import { CacheType } from '@proton/redux-utilities';
4 import isDeepEqual from '@proton/shared/lib/helpers/isDeepEqual';
5 import type { Address, DecryptedAddressKey, DecryptedKey, Key } from '@proton/shared/lib/interfaces';
6 import noop from '@proton/utils/noop';
8 import { selectAddresses } from '../addresses';
9 import { selectUserKeys } from '../userKeys';
11 type AddressKeysState,
13 dispatchGetAllAddressesKeys,
14 getAllAddressKeysAction,
18 const addressKeyEqualityComparator = (a: Key[] | undefined = [], b: Key[] | undefined = []) => {
22 a.length === b.length &&
23 a.every((value, index) => {
24 const otherKey = b[index];
25 return isDeepEqual(value, otherKey);
30 const getAddressesWithChangedKeys = (
31 currentAddressKeys: AddressKeysState['addressKeys'],
32 currentUserKeys: DecryptedKey[],
33 nextUserKeys: DecryptedKey[],
37 const result = (() => {
38 // Any user keys changed, return all new addresses.
39 if (currentUserKeys !== nextUserKeys) {
42 const currentAddressesMap = a.reduce<{ [key: string]: Address }>((acc, address) => {
43 acc[address.ID] = address;
46 return b.filter((address) => {
47 return !addressKeyEqualityComparator(address.Keys, currentAddressesMap[address.ID]?.Keys);
50 return result.filter((address) => currentAddressKeys[address.ID]);
53 const getChanges = (currentValue: AddressKeysState['addressKeys'], nextValue: AddressKeysState['addressKeys']) => {
54 return Object.entries(currentValue).filter(([addressID, currentValue]) => {
55 return currentValue?.value !== nextValue[addressID]?.value;
59 export const addressKeysListener = (startListening: SharedStartListening<AddressKeysState>) => {
61 predicate: (action, currentState, previousState) => {
62 const currentUserKeys = selectUserKeys(currentState).value;
63 const currentAddresses = selectAddresses(currentState).value || [];
64 const previousUserKeys = selectUserKeys(previousState).value;
65 const previousAddresses = selectAddresses(previousState).value || [];
66 const previousAddressKeys = selectAddressKeys(previousState);
68 // Decrypting address keys depend on user keys, so if they change we assume it should get recomputed.
69 (currentUserKeys !== previousUserKeys ||
70 // Addresses changed and address keys have been computed.
71 currentAddresses !== previousAddresses) &&
72 Object.keys(previousAddressKeys).length > 0
75 effect: async (action, listenerApi) => {
76 const currentState = listenerApi.getOriginalState();
77 const nextState = listenerApi.getState();
78 const currentAddressKeys = selectAddressKeys(currentState);
79 const currentUserKeys = selectUserKeys(currentState)?.value || [];
80 const nextUserKeys = selectUserKeys(nextState)?.value || [];
81 const currentAddresses = selectAddresses(currentState)?.value || [];
82 const nextAddresses = selectAddresses(nextState)?.value || [];
83 const changedAddresses = getAddressesWithChangedKeys(
90 if (!changedAddresses.length) {
94 changedAddresses.map((address) =>
95 listenerApi.dispatch(addressKeysThunk({ addressID: address.ID, cache: CacheType.None }))
102 predicate: (action, currentState, previousState) => {
103 const newValue = selectAddressKeys(currentState);
104 const oldValue = selectAddressKeys(previousState);
105 const changes = getChanges(oldValue, newValue);
106 return changes.length > 0;
108 effect: (action, listenerApi) => {
109 const clear = (addressID: string, oldValue: DecryptedAddressKey[] | undefined) => {
111 (oldValue || []).map(async (cachedKey) => {
112 if (cachedKey?.privateKey) {
113 await CryptoProxy.clearKey({ key: cachedKey.privateKey }).catch(noop);
115 if (cachedKey?.publicKey) {
116 await CryptoProxy.clearKey({ key: cachedKey.publicKey }).catch(noop);
122 const oldValue = selectAddressKeys(listenerApi.getOriginalState());
123 const newValue = selectAddressKeys(listenerApi.getState());
124 const changes = getChanges(oldValue, newValue);
126 changes.map(async ([addressID, oldValue]) => {
127 return clear(addressID, oldValue?.value);
129 ).then(() => undefined);
134 actionCreator: getAllAddressKeysAction,
135 effect: async (action, listenerApi) => {
136 await dispatchGetAllAddressesKeys(listenerApi.dispatch);