<commit_before>import { createExportIcs } from '@proton/shared/lib/calendar/export/createExportIcs';
import { getProdIdFromNameAndVersion } from '@proton/shared/lib/calendar/vcalConfig';
import { getUniqueVtimezones } from '@proton/shared/lib/calendar/vtimezoneHelper';
import {
    Calendar,
    EXPORT_ERRORS,
    EXPORT_STEPS,
    ExportCalendarModel,
    ExportError,
    VcalVeventComponent,
} from '@proton/shared/lib/interfaces/calendar';
import { getWeekStartsOn } from '@proton/shared/lib/settings/helper';
import { useState } from 'react';
import { c } from 'ttag';

import downloadFile from '@proton/shared/lib/helpers/downloadFile';
import { format } from 'date-fns';
import { getAppHref, getClientID } from '@proton/shared/lib/apps/helper';
import { APPS } from '@proton/shared/lib/constants';
import { Button, BasicModal } from '../../../components';
import { useGetVtimezonesMap } from '../../../hooks/useGetVtimezonesMap';
import ExportingModalContent from './ExportingModalContent';
import ExportSummaryModalContent from './ExportSummaryModalContent';
import { useGetCalendarUserSettings, useUserSettings } from '../../../hooks';

interface Props {
    calendar: Calendar;
    onClose?: () => void;
    onExit?: () => void;
    isOpen?: boolean;
}

export const ExportModal = ({ calendar, onClose, onExit, isOpen = false }: Props) => {
    const getVTimezonesMap = useGetVtimezonesMap();
    const getCalendarUserSettings = useGetCalendarUserSettings();
    const [userSettings] = useUserSettings();
    const weekStartsOn = getWeekStartsOn(userSettings);

    const [model, setModel] = useState<ExportCalendarModel>({
        step: EXPORT_STEPS.EXPORTING,
        totalFetched: 0,
        totalProcessed: 0,
        exportErrors: [],
        totalToProcess: 0,
        weekStartsOn,
        calendar,
    });
    const updateModel = (changes: Partial<ExportCalendarModel>) =>
        setModel((currentModel: ExportCalendarModel) => ({ ...currentModel, ...changes }));

    const [calendarBlob, setCalendarBlob] = useState<Blob>();

    const { content, onSubmit } = (() => {
        if (model.step === EXPORT_STEPS.EXPORTING) {
            const handleFinish = async (
                exportedEvents: VcalVeventComponent[],
                exportErrors: ExportError[],
                keepError?: boolean
            ) => {
                // we don't catch errors here as they're caught into a NETWORK error on ExportingModalContent
                const { PrimaryTimezone: defaultTzid } = await getCalendarUserSettings();
                const uniqueTimezonesPromise = getUniqueVtimezones({
                    vevents: exportedEvents,
                    tzids: [defaultTzid],
                    getVTimezonesMap,
                }).catch(() => {
                    return [];
                });
                const appVersionPromise = fetch(getAppHref('/assets/version.json', APPS.PROTONCALENDAR))
                    .then((result) => result.json())
                    .then((json) => json.version)
                    .catch(() => {
                        // TODO: remove when atlas CSP issues are resolved
                        return '4.1.11';
                    });

                const [uniqueTimezones, appVersion] = await Promise.all([uniqueTimezonesPromise, appVersionPromise]);
                const clientId = getClientID(APPS.PROTONCALENDAR);

                const ics = createExportIcs({
                    calendar,
                    prodId: getProdIdFromNameAndVersion(clientId, appVersion),
                    eventsWithSummary: exportedEvents,
                    defaultTzid,
                    vtimezones: uniqueTimezones,
                });
                updateModel({
                    step: EXPORT_STEPS.FINISHED,
                    exportErrors,
                    ...(!keepError && { error: undefined }),
                });
                setCalendarBlob(new Blob([ics], { type: 'text/plain;charset=utf-8' }));
            };

            return {
                content: <ExportingModalContent model={model} setModel={setModel} onFinish={handleFinish} />,
                onSubmit: null,
            };
        }

        return {
            content: <ExportSummaryModalContent model={model} />,
            onSubmit: () => {
                if (model.error === EXPORT_ERRORS.NETWORK_ERROR) {
                    updateModel({ step: EXPORT_STEPS.EXPORTING });
                    return;
                }

                downloadFile(calendarBlob, `${calendar.Name}-${format(Date.now(), 'yyyy-MM-dd')}.ics`);
                onClose?.();
            },
        };
    })();

    return (
        <BasicModal
            title={c('Title').t`Export calendar`}
            footer={
                <>
                    <Button onClick={onClose}>{c('Action').t`Cancel`}</Button>
                    {!!onSubmit && (
                        <Button color="norm" onClick={onSubmit} type="submit">
                            {model.error === EXPORT_ERRORS.NETWORK_ERROR
                                ? c('Action').t`Try again`
                                : c('Action').t`Save ICS file`}
                        </Button>
                    )}
                </>
            }
            isOpen={isOpen}
            size="large"
            fullscreenOnMobile
            onClose={onClose}
            onExit={onExit}
        >
            {content}
        </BasicModal>
    );
};

export default ExportModal;
<commit_msg>Improve the PRODID of the exported ICS<commit_after>import { createExportIcs } from '@proton/shared/lib/calendar/export/createExportIcs';
import { getProdIdFromNameAndVersion } from '@proton/shared/lib/calendar/vcalConfig';
import { getUniqueVtimezones } from '@proton/shared/lib/calendar/vtimezoneHelper';
import {
    Calendar,
    EXPORT_ERRORS,
    EXPORT_STEPS,
    ExportCalendarModel,
    ExportError,
    VcalVeventComponent,
} from '@proton/shared/lib/interfaces/calendar';
import { getWeekStartsOn } from '@proton/shared/lib/settings/helper';
import { useState } from 'react';
import { c } from 'ttag';

import downloadFile from '@proton/shared/lib/helpers/downloadFile';
import { format } from 'date-fns';
import { Button, BasicModal } from '../../../components';
import { useGetVtimezonesMap } from '../../../hooks/useGetVtimezonesMap';
import ExportingModalContent from './ExportingModalContent';
import ExportSummaryModalContent from './ExportSummaryModalContent';
import { useConfig, useGetCalendarUserSettings, useUserSettings } from '../../../hooks';
import { getAppVersion } from '../../..';

interface Props {
    calendar: Calendar;
    onClose?: () => void;
    onExit?: () => void;
    isOpen?: boolean;
}

export const ExportModal = ({ calendar, onClose, onExit, isOpen = false }: Props) => {
    const getVTimezonesMap = useGetVtimezonesMap();
    const getCalendarUserSettings = useGetCalendarUserSettings();
    const [userSettings] = useUserSettings();
    const weekStartsOn = getWeekStartsOn(userSettings);
    const { APP_VERSION } = useConfig();
    const appVersion = getAppVersion(APP_VERSION);

    const [model, setModel] = useState<ExportCalendarModel>({
        step: EXPORT_STEPS.EXPORTING,
        totalFetched: 0,
        totalProcessed: 0,
        exportErrors: [],
        totalToProcess: 0,
        weekStartsOn,
        calendar,
    });
    const updateModel = (changes: Partial<ExportCalendarModel>) =>
        setModel((currentModel: ExportCalendarModel) => ({ ...currentModel, ...changes }));

    const [calendarBlob, setCalendarBlob] = useState<Blob>();

    const { content, onSubmit } = (() => {
        if (model.step === EXPORT_STEPS.EXPORTING) {
            const handleFinish = async (
                exportedEvents: VcalVeventComponent[],
                exportErrors: ExportError[],
                keepError?: boolean
            ) => {
                // we don't catch errors here as they're caught into a NETWORK error on ExportingModalContent
                const { PrimaryTimezone: defaultTzid } = await getCalendarUserSettings();
                const uniqueTimezones = await getUniqueVtimezones({
                    vevents: exportedEvents,
                    tzids: [defaultTzid],
                    getVTimezonesMap,
                }).catch(() => {
                    return [];
                });

                const ics = createExportIcs({
                    calendar,
                    // We use the ProtonAccount version here,
                    // but we do not want to display 'Web Account' in this public PRODID
                    // As a compromise between a "marketing display" and traceability,
                    // we add a '.a' suffix to indicate that the version refers to Proton Account
                    prodId: getProdIdFromNameAndVersion('WebCalendar', `${appVersion}.a`),
                    eventsWithSummary: exportedEvents,
                    defaultTzid,
                    vtimezones: uniqueTimezones,
                });
                updateModel({
                    step: EXPORT_STEPS.FINISHED,
                    exportErrors,
                    ...(!keepError && { error: undefined }),
                });
                setCalendarBlob(new Blob([ics], { type: 'text/plain;charset=utf-8' }));
            };

            return {
                content: <ExportingModalContent model={model} setModel={setModel} onFinish={handleFinish} />,
                onSubmit: null,
            };
        }

        return {
            content: <ExportSummaryModalContent model={model} />,
            onSubmit: () => {
                if (model.error === EXPORT_ERRORS.NETWORK_ERROR) {
                    updateModel({ step: EXPORT_STEPS.EXPORTING });
                    return;
                }

                downloadFile(calendarBlob, `${calendar.Name}-${format(Date.now(), 'yyyy-MM-dd')}.ics`);
                onClose?.();
            },
        };
    })();

    return (
        <BasicModal
            title={c('Title').t`Export calendar`}
            footer={
                <>
                    <Button onClick={onClose}>{c('Action').t`Cancel`}</Button>
                    {!!onSubmit && (
                        <Button color="norm" onClick={onSubmit} type="submit">
                            {model.error === EXPORT_ERRORS.NETWORK_ERROR
                                ? c('Action').t`Try again`
                                : c('Action').t`Save ICS file`}
                        </Button>
                    )}
                </>
            }
            isOpen={isOpen}
            size="large"
            fullscreenOnMobile
            onClose={onClose}
            onExit={onExit}
        >
            {content}
        </BasicModal>
    );
};

export default ExportModal;

{'fraction_non_alphanumeric': 0.11864093804819499, 'fraction_numerical': 0.0011079309389714707, 'mean_word_length': 1.8505263157894736, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 50, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2404832', 'n_tokens_mistral': 2914, 'n_tokens_neox': 2770, 'n_words': 580}