Use same lock values as mobile clients
[ProtonMail-WebClient.git] / packages / shared / lib / calendar / holidaysCalendar / holidaysCalendar.ts
blob2c20fe82328a0108633676c3cec835e90dddbc7d
1 import type { SessionKey } from '@proton/crypto';
2 import { encryptPassphraseSessionKey, signPassphrase } from '@proton/shared/lib/calendar/crypto/keys/calendarKeys';
3 import type { Address } from '@proton/shared/lib/interfaces';
4 import type { CalendarNotificationSettings, HolidaysDirectoryCalendar } from '@proton/shared/lib/interfaces/calendar';
5 import unique from '@proton/utils/unique';
7 import { getPrimaryAddress } from '../../helpers/address';
8 import { base64StringToUint8Array } from '../../helpers/encoding';
9 import { getLanguageCode, getNaiveCountryCode } from '../../i18n/helper';
10 import type { GetAddressKeys } from '../../interfaces/hooks/GetAddressKeys';
12 /**
13  * Get all holidays calendars corresponding to a certain time zone
14  */
15 export const getHolidaysCalendarsFromTimeZone = (calendars: HolidaysDirectoryCalendar[], tzid: string) => {
16     return calendars.filter(({ Timezones }) => Timezones.includes(tzid));
19 /**
20  * Get all holidays calendars with the same country code and sort them by language.
21  * We might get several holidays calendars in the same country, but with different languages.
22  */
23 export const getHolidaysCalendarsFromCountryCode = (
24     holidayCalendars: HolidaysDirectoryCalendar[],
25     countryCode: string
26 ) => {
27     return holidayCalendars
28         .filter(({ CountryCode }) => CountryCode === countryCode)
29         .sort((a, b) => a.Language.localeCompare(b.Language));
32 /**
33  * Given a list of country codes, find the preferred one based on language preferences. Result can be undefined.
34  * See `getSuggestedHolidaysCalendar` for more details on the logic.
35  */
36 export const findPreferredCountryCode = (codes: string[], languageTags: string[]) => {
37     if (codes.length === 1) {
38         return codes[0];
39     }
40     for (const tag of languageTags) {
41         const languageCountryCode = getNaiveCountryCode(tag);
42         const preferredCountryCode = codes.find((code) => code === languageCountryCode);
43         if (preferredCountryCode) {
44             return preferredCountryCode;
45         }
46     }
49 /**
50  * Given a list of holidays directory calendars, find the preferred one based on language preferences. Result can be undefined.
51  * See `getSuggestedHolidaysCalendar` for more details on the logic.
52  */
53 export const findPreferredCalendarByLanguageTag = (calendars: HolidaysDirectoryCalendar[], languageTags: string[]) => {
54     if (calendars.length === 1) {
55         return calendars[0];
56     }
57     for (const tag of languageTags) {
58         const code = getLanguageCode(tag);
59         const preferredCalendar = calendars.find(({ LanguageCode }) => code === LanguageCode.toLowerCase());
60         if (preferredCalendar) {
61             return preferredCalendar;
62         }
63     }
66 /**
67  * Given a list of holidays directory calendars belonging to one country, find the preferred one based on language preferences. Result can be undefined.
68  * See `getSuggestedHolidaysCalendar` for more details on the logic.
69  */
70 export const findHolidaysCalendarByCountryCodeAndLanguageTag = (
71     calendars: HolidaysDirectoryCalendar[],
72     countryCode: string,
73     languageTags: string[]
74 ) => {
75     const calendarsFromCountry = getHolidaysCalendarsFromCountryCode(calendars, countryCode);
77     return findPreferredCalendarByLanguageTag(calendarsFromCountry, languageTags) || calendarsFromCountry[0];
80 /**
81  * Given the user time zone preference, user language preference for the Proton web-apps,
82  * and a list of language tags (RFC-5646) expressing the user language preference,
83  * we try to find a calendar that matches those in a directory of holidays calendars.
84  * The logic for matching is as follows:
85  *
86  * * First filter the calendars that are compatible with the user time zone.
87  *
88  * * Then try to match a country:
89  * * * If the filtering above returned the empty array, return undefined.
90  * * * If the filtered calendars all belong to one country, pick that country.
91  * * * If there are several countries in the filtered calendars, use the language tags to find a match. Return first match if any
92  * * * [We don't user the Proton language preference here because we assume there's more granularity in
93  * * *  the language tags passed. E.g. at the moment a user can't choose nl_BE as Proton language]
94  * * * If there's no match, return undefined.
95  *
96  * * If we got a country match, some calendars (calendar <-> language) will be associated to it:
97  * * * If the country has just one associated calendar (<-> language), pick that one.
98  * * * If the country has multiple associated calendars (<-> languages):
99  * * * * If the Proton language matches one of the languages, pick that one.
100  * * * * If no match, if any of the language tags matches one of the languages (we try in the order of preference given), pick that one.
101  * * * * If no match, pick the first language in the list.
102  */
103 export const getSuggestedHolidaysCalendar = (
104     calendars: HolidaysDirectoryCalendar[],
105     tzid: string,
106     protonLanguage: string,
107     languageTags: string[]
108 ) => {
109     // Get all calendars in the same time zone as the user
110     const calendarsFromTimeZone = getHolidaysCalendarsFromTimeZone(calendars, tzid);
112     if (!calendarsFromTimeZone.length) {
113         return;
114     }
116     const countryCodes = unique(calendarsFromTimeZone.map(({ CountryCode }) => CountryCode));
117     const countryCode = findPreferredCountryCode(countryCodes, languageTags);
119     if (!countryCode) {
120         return;
121     }
123     return findHolidaysCalendarByCountryCodeAndLanguageTag(calendarsFromTimeZone, countryCode, [
124         protonLanguage,
125         ...languageTags,
126     ]);
129 export const getJoinHolidaysCalendarData = async ({
130     holidaysCalendar,
131     addresses,
132     getAddressKeys,
133     color,
134     notifications,
135     priority,
136 }: {
137     holidaysCalendar: HolidaysDirectoryCalendar;
138     addresses: Address[];
139     getAddressKeys: GetAddressKeys;
140     color: string;
141     notifications: CalendarNotificationSettings[];
142     priority?: number;
143 }) => {
144     const {
145         CalendarID,
146         Passphrase,
147         SessionKey: { Key, Algorithm },
148     } = holidaysCalendar;
150     const primaryAddress = getPrimaryAddress(addresses);
151     if (!primaryAddress) {
152         throw new Error('No primary address');
153     }
154     const primaryAddressKey = (await getAddressKeys(primaryAddress.ID))[0];
155     if (!primaryAddressKey) {
156         throw new Error('No primary address key');
157     }
159     const { privateKey, publicKey } = primaryAddressKey;
161     const [{ encryptedSessionKey }, signature] = await Promise.all([
162         encryptPassphraseSessionKey({
163             sessionKey: { data: base64StringToUint8Array(Key), algorithm: Algorithm } as SessionKey,
164             publicKey,
165             signingKey: privateKey,
166         }),
167         signPassphrase({ passphrase: Passphrase, privateKey }),
168     ]);
170     return {
171         calendarID: CalendarID,
172         addressID: primaryAddress.ID,
173         payload: {
174             PassphraseKeyPacket: encryptedSessionKey,
175             Signature: signature,
176             Color: color,
177             DefaultFullDayNotifications: notifications,
178             Priority: priority,
179         },
180     };