Search code examples
javascriptazureoutlookazure-active-directoryoffice365

Outlook calendar don't render with FullCalendar


I am developing an app to see the events on my Outlook calendar using fullcalendar, the problem is that I log in to Microsoft, in principle everything is correct, I return to my website but the calendar is not generated, what's more, if I click again The login button displays a message that the login is still in progress (errorCode === "interaction_in_progress") and i get the console log 'im inside the else (accounts.length)' .

This is my code, I can't find what is causing the calendar and events to not load.

<div id="calendar"></div>

<script>
    let msalInstance;
    const loginRequest = {
        scopes: ["User.Read", "Calendars.Read", "Calendars.Read.Shared"]
    };

    async function mostrarCalendarioOutlook() {
        console.log('I am inside of mostrarCalendarioOutlook');
        try {
            if (!msalInstance) {
                throw new Error("msalInstance is not initialized");
            }
            const accounts = msalInstance.getAllAccounts();
            if (accounts.length > 0) {
                console.log('Dentro del if (accounts.length > 0)');
                const account = accounts[0];
                const tokenResponse = await msalInstance.acquireTokenSilent({
                    account,
                    scopes: ["User.Read", "Calendars.Read", "Calendars.Read.Shared"]
                });
                const events = await getCalendarEvents(tokenResponse.accessToken);
                console.log('initializing render calendar function');
                renderCalendar(events);
            } else {                
                console.log('im inside the else (accounts.length)');
                await msalInstance.loginRedirect(loginRequest);
            }
        } catch (error) {                        
            handleError(error);            
        }
    }

    function handleError(error) {
        if (error.errorCode === "interaction_in_progress") {
            Swal.fire({
                    icon: 'info',
                    title: 'Process in progress',
                    text: 'The login process is in progress. Please wait for it to complete.',
                    allowOutsideClick: false
                });
        } else if (error.errorCode === "consent_required" || error.errorCode === "interaction_required") {
            Swal.fire({
                icon: 'error',
                title: 'Necessary permissions',
                text: 'The administrator must grant the necessary permissions to access the calendars. Contact the administrator.'
            });
        } else if (error.errorCode === "access_denied") {
            Swal.fire({
                icon: 'error',
                title: 'Access denied',
                text: 'The requested data could not be accessed. Check permissions and try again.'
            });
        } else {
            Swal.fire({
                icon: 'error',
                title: 'Authentication error',
                text: 'An error occurred during authentication. Please try again later.'
            });
            console.error('Error during authentication:', error);
        }
    }    

    document.addEventListener('DOMContentLoaded', async function() {                
        try {
            const msalConfig = {
                auth: {
                    clientId: "Client_ID", 
                    authority: "https://login.microsoftonline.com/common",                    
                    redirectUri: "redirect_Uri",
                    popUp: true
                }
            };

            msalInstance = new msal.PublicClientApplication(msalConfig);            

            //EVENTS
            async function getCalendarEvents(accessToken) {
                console.log('im inside of getCalendarEvents');
                const response = await fetch('https://graph.microsoft.com/v1.0/me/events', {
                    headers: {
                        'Authorization': `Bearer ${accessToken}`
                    }
                });

                if (!response.ok) {
                    const errorText = await response.text();
                    console.error(`Error getting calendar events: ${response.status} ${response.statusText}`, errorText);
                    throw new Error('Error getting calendar events');
                }

                const data = await response.json();
                const events = [];

                function adjustToLocalTime(dateTime) {
                    const date = new Date(dateTime);
                    const timeZoneOffset = date.getTimezoneOffset() * 60000;
                    return new Date(date.getTime() - timeZoneOffset).toISOString();
                }

                const recurringEvents = data.value.filter(event => event.recurrence && event.recurrence.pattern);

                recurringEvents.forEach(event => {
                    const startDate = new Date(event.start.dateTime);
                    const endDate = new Date(event.end.dateTime);
                    const recurrence = event.recurrence.pattern;
                    let instanceDate = new Date(startDate);
                    const endRecurrenceDate = event.recurrence.range && event.recurrence.range.endDate ? new Date(event.recurrence.range.endDate) : null;

                    while (!endRecurrenceDate || instanceDate <= endRecurrenceDate) {
                        if (!recurrence.daysOfWeek) {
                            const adjustedStartDate = new Date(instanceDate);
                            adjustedStartDate.setHours(startDate.getHours());
                            adjustedStartDate.setMinutes(startDate.getMinutes());

                            const adjustedEndDate = new Date(instanceDate);
                            adjustedEndDate.setHours(endDate.getHours());
                            adjustedEndDate.setMinutes(endDate.getMinutes());

                            if (adjustedStartDate <= endRecurrenceDate || !endRecurrenceDate) {
                                events.push({
                                    title: event.subject,
                                    start: adjustToLocalTime(adjustedStartDate.toISOString()),
                                    end: adjustToLocalTime(adjustedEndDate.toISOString()),
                                    allDay: event.isAllDay
                                });
                            }

                            switch (recurrence.type) {
                                case "daily":
                                    instanceDate.setDate(instanceDate.getDate() + recurrence.interval);
                                    break;
                                case "absoluteMonthly":
                                    instanceDate.setMonth(instanceDate.getMonth() + recurrence.interval);
                                    break;
                                case "absoluteYearly":
                                    instanceDate.setFullYear(instanceDate.getFullYear() + recurrence.interval);
                                    break;
                            }

                            continue;
                        }

                        const daysOfWeekIndices = (recurrence.daysOfWeek || []).map(day => {
                            switch (day.toLowerCase()) {
                                case "monday":
                                    return 1;
                                case "tuesday":
                                    return 2;
                                case "wednesday":
                                    return 3;
                                case "thursday":
                                    return 4;
                                case "friday":
                                    return 5;
                                case "saturday":
                                    return 6;
                                case "sunday":
                                    return 0;
                            }
                        });

                        daysOfWeekIndices.forEach(dayIndex => {
                            let tempDate = new Date(instanceDate);
                            while (tempDate.getDay() !== dayIndex) {
                                tempDate.setDate(tempDate.getDate() + 1);
                            }

                            if (tempDate >= startDate && (!endRecurrenceDate || tempDate <= endRecurrenceDate)) {
                                const adjustedStartDate = new Date(tempDate);
                                adjustedStartDate.setHours(startDate.getHours());
                                adjustedStartDate.setMinutes(startDate.getMinutes());

                                const adjustedEndDate = new Date(tempDate);
                                adjustedEndDate.setHours(endDate.getHours());
                                adjustedEndDate.setMinutes(endDate.getMinutes());

                                if (adjustedStartDate <= endRecurrenceDate || !endRecurrenceDate) {
                                    events.push({
                                        title: event.subject,
                                        start: adjustToLocalTime(adjustedStartDate.toISOString()),
                                        end: adjustToLocalTime(adjustedEndDate.toISOString()),
                                        allDay: event.isAllDay
                                    });
                                }
                            }
                        });

                        instanceDate.setDate(instanceDate.getDate() + 7 * recurrence.interval);
                    }

                    if (endRecurrenceDate && recurrence.daysOfWeek) {
                        const tempDate = new Date(endRecurrenceDate);
                        const endRecurrenceDay = tempDate.toLocaleString('en-US', {
                            weekday: 'long'
                        }).toLowerCase();
                        if (recurrence.daysOfWeek.includes(endRecurrenceDay)) {
                            const adjustedStartDate = new Date(tempDate);
                            adjustedStartDate.setHours(startDate.getHours());
                            adjustedStartDate.setMinutes(startDate.getMinutes());

                            const adjustedEndDate = new Date(tempDate);
                            adjustedEndDate.setHours(endDate.getHours());
                            adjustedEndDate.setMinutes(endDate.getMinutes());

                            if (adjustedStartDate.getTime() !== endRecurrenceDate.getTime()) {
                                events.push({
                                    title: event.subject,
                                    start: adjustToLocalTime(adjustedStartDate.toISOString()),
                                    end: adjustToLocalTime(adjustedEndDate.toISOString()),
                                    allDay: event.isAllDay
                                });
                            }
                        }
                    }
                });

                const singleEvents = data.value.filter(event => !event.recurrence);
                singleEvents.forEach(event => {
                    events.push({
                        title: event.subject,
                        start: adjustToLocalTime(event.start.dateTime),
                        end: adjustToLocalTime(event.end.dateTime),
                        allDay: event.isAllDay
                    });
                });               

                return events;
            }

            async function handleLogoutClick() {
                console.log('Signing out Microsoft...');
                Swal.fire({
                    title: 'Signing out...',
                    allowOutsideClick: false,
                    didOpen: () => {
                        Swal.showLoading();
                    },
                });

                try {
                    const accounts = await msalInstance.getAllAccounts();
                    if (accounts.length === 0) {
                        Swal.close();
                        return;
                    }

                    const respuesta = await msalInstance.logoutPopup({
                        account: accounts[0],
                    });

                    if (respuesta) {
                        localStorage.setItem('logoutCompleted', 'true');
                        console.log('Microsoft session closed successfully');
                    } else {
                        localStorage.setItem('logoutCompleted', 'false');
                        console.log('The session was not logged out');
                    }

                    Swal.close();

                } catch (error) {
                    Swal.fire({
                        icon: 'error',
                        title: 'Error logout',
                        text: 'An error occurred while trying to log out. Please try again later.'
                    });
                    console.log('Error signing out Microsoft', error);
                } finally {
                    Swal.close();
                }
            }

            function renderCalendar(events) {
                console.log('I enter the renderCalendar function');
                const calendarEl = document.getElementById('calendar');
                const calendar = new FullCalendar.Calendar(calendarEl, {
                    customButtons: {
                        myCustomButton: {
                            text: 'Refrescar',
                            click: async function() {
                                console.log('Updating Calendar...');
                                Swal.fire({
                                    title: 'Updating Calendar...',
                                    allowOutsideClick: false,
                                    didOpen: () => {
                                        Swal.showLoading();
                                    }
                                });

                                try {
                                    const accounts = await msalInstance.getAllAccounts();
                                    if (accounts.length === 0) {
                                        await msalInstance.loginRedirect(loginRequest);
                                        return;
                                    }

                                    const response = await msalInstance.acquireTokenSilent({
                                        account: accounts[0],
                                        scopes: ["User.Read", "Calendars.Read", "Calendars.Read.Shared"]
                                    });

                                    if (response !== null) {
                                        const accessToken = response.accessToken;
                                        const events = await getCalendarEvents(accessToken);
                                        renderCalendar(events);
                                        Swal.close();
                                        console.log('Calendar refreshed successfully');
                                    } else {
                                        console.error('Could not get access token.');
                                        console.log('Calendar NOT refreshed');
                                        Swal.close();
                                    }
                                } catch (error) {
                                    console.log('Error when refreshing calendar');
                                    handleError(error);
                                }
                            }
                        },
                        logout: {
                            text: 'Cerrar Sesión',
                            click: function() {
                                handleLogoutClick();
                            }
                        }
                    },
                    headerToolbar: {
                        left: 'prev,next today myCustomButton logout',
                        center: 'title',
                        right: 'dayGridMonth,timeGridWeek,timeGridDay'
                    },
                    buttonText: {
                        today: 'Hoy',
                        month: 'Mes',
                        week: 'Semana',
                        day: 'Día',
                        list: 'Lista'
                    },
                    initialView: 'dayGridMonth',
                    locale: <?php echo '"' . session('language') . '"'; ?>,
                    firstDay: 1,
                    events: events,
                    dayMaxEvents: true,
                    eventClick: function(info) {
                        const event = info.event;
                        const title = event.title;
                        const start = event.start;
                        const end = event.end;
                        const allDay = event.allDay;
                        const location = event.extendedProps.location;

                        const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;

                        const startFormatted = start.toLocaleString('es-ES', {
                            timeZone: timeZone
                        });
                        const endFormatted = end ? end.toLocaleString('es-ES', {
                            timeZone: timeZone
                        }) : 'Evento de todo el día';

                        let content = `<h2>${title}</h2>`;
                        if (!allDay) {
                            content += `<p><strong>Inicio:</strong> ${startFormatted}</p>`;
                            content += `<p><strong>Fin:</strong> ${endFormatted}</p>`;
                        } else {
                            content += `<p><strong>Evento de todo el día</strong></p>`;
                        }
                        if (location) {
                            content += `<p><strong>Ubicación:</strong> ${location}</p>`;
                        }

                        Swal.fire({
                            title: 'Información del Evento',
                            html: content,
                            icon: 'info'
                        });

                        console.log('Infomacion:' + info.event);
                        console.log('Titulo:' + title);
                        console.log('Inicio:' + start);
                        console.log('Fin:' + end);
                        console.log('Evento Diario:' + allDay);
                    }
                });
                calendar.render();
            }
        } catch (error) {
            console.error('Error during initialization:', error);
            Swal.fire({
                icon: 'error',
                title: 'Initialization error',
                text: 'An error occurred while loading the configuration. Please try again later.'
            });
        }
    });
</script>

Thank you very much!!


Solution

  • The error (errorCode === "interaction_in_progress") may occur due to Multiple Login Attempts or Page Navigation.

    Posting the answer to help the community:

    In your scenario, the issue is because of having the login button and the calendar on the same URI.

    • When the login process redirects back to the application, it causes a page to reload, which interrupts the flow of the application.

    To resolve the issue, check the below:

    • Configure Separate Redirect URL: Configure a distinct redirect URI in the Azure portal for handling the Microsoft login and ensure it does not interfere with the main application URI.
    • Create Isolated Login Button: Move the login button to a navigation bar, that will make a separate page to the login process.
    https://graph.microsoft.com/v1.0/me/events
    

    enter image description here

    Hence, make sure that the login process does not interfere with the state of your application by doing this the authentication flow will be intact, and you will be able to successfully render the calendar with the fetched events.