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';
13 * Get all holidays calendars corresponding to a certain time zone
15 export const getHolidaysCalendarsFromTimeZone = (calendars: HolidaysDirectoryCalendar[], tzid: string) => {
16 return calendars.filter(({ Timezones }) => Timezones.includes(tzid));
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.
23 export const getHolidaysCalendarsFromCountryCode = (
24 holidayCalendars: HolidaysDirectoryCalendar[],
27 return holidayCalendars
28 .filter(({ CountryCode }) => CountryCode === countryCode)
29 .sort((a, b) => a.Language.localeCompare(b.Language));
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.
36 export const findPreferredCountryCode = (codes: string[], languageTags: string[]) => {
37 if (codes.length === 1) {
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;
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.
53 export const findPreferredCalendarByLanguageTag = (calendars: HolidaysDirectoryCalendar[], languageTags: string[]) => {
54 if (calendars.length === 1) {
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;
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.
70 export const findHolidaysCalendarByCountryCodeAndLanguageTag = (
71 calendars: HolidaysDirectoryCalendar[],
73 languageTags: string[]
75 const calendarsFromCountry = getHolidaysCalendarsFromCountryCode(calendars, countryCode);
77 return findPreferredCalendarByLanguageTag(calendarsFromCountry, languageTags) || calendarsFromCountry[0];
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:
86 * * First filter the calendars that are compatible with the user time zone.
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.
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.
103 export const getSuggestedHolidaysCalendar = (
104 calendars: HolidaysDirectoryCalendar[],
106 protonLanguage: string,
107 languageTags: string[]
109 // Get all calendars in the same time zone as the user
110 const calendarsFromTimeZone = getHolidaysCalendarsFromTimeZone(calendars, tzid);
112 if (!calendarsFromTimeZone.length) {
116 const countryCodes = unique(calendarsFromTimeZone.map(({ CountryCode }) => CountryCode));
117 const countryCode = findPreferredCountryCode(countryCodes, languageTags);
123 return findHolidaysCalendarByCountryCodeAndLanguageTag(calendarsFromTimeZone, countryCode, [
129 export const getJoinHolidaysCalendarData = async ({
137 holidaysCalendar: HolidaysDirectoryCalendar;
138 addresses: Address[];
139 getAddressKeys: GetAddressKeys;
141 notifications: CalendarNotificationSettings[];
147 SessionKey: { Key, Algorithm },
148 } = holidaysCalendar;
150 const primaryAddress = getPrimaryAddress(addresses);
151 if (!primaryAddress) {
152 throw new Error('No primary address');
154 const primaryAddressKey = (await getAddressKeys(primaryAddress.ID))[0];
155 if (!primaryAddressKey) {
156 throw new Error('No primary address key');
159 const { privateKey, publicKey } = primaryAddressKey;
161 const [{ encryptedSessionKey }, signature] = await Promise.all([
162 encryptPassphraseSessionKey({
163 sessionKey: { data: base64StringToUint8Array(Key), algorithm: Algorithm } as SessionKey,
165 signingKey: privateKey,
167 signPassphrase({ passphrase: Passphrase, privateKey }),
171 calendarID: CalendarID,
172 addressID: primaryAddress.ID,
174 PassphraseKeyPacket: encryptedSessionKey,
175 Signature: signature,
177 DefaultFullDayNotifications: notifications,