digitalfabrik/integreat-app

View on GitHub
native/src/components/ExportEventButton.tsx

Summary

Maintainability
B
6 hrs
Test Coverage
F
26%
import { DateTime } from 'luxon'
import React, { ReactElement, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Linking, Platform } from 'react-native'
import RNCalendarEvents, { Calendar, CalendarEventWritable, RecurrenceFrequency } from 'react-native-calendar-events'
import { PERMISSIONS, requestMultiple } from 'react-native-permissions'
import { Frequency } from 'rrule'
import styled from 'styled-components/native'

import { EventModel } from 'shared/api'

import useSnackbar from '../hooks/useSnackbar'
import { reportError } from '../utils/sentry'
import CalendarChoice from './CalendarChoiceModal'
import TextButton from './base/TextButton'

const StyledButton = styled(TextButton)`
  margin: 16px 0;
`

type ExportEventButtonType = {
  event: EventModel
}

export const formatFrequency = (frequency: Frequency): RecurrenceFrequency =>
  Frequency[frequency].toLowerCase() as RecurrenceFrequency

const ExportEventButton = ({ event }: ExportEventButtonType): ReactElement => {
  const { t } = useTranslation('events')
  const showSnackbar = useSnackbar()

  const [showCalendarChoiceModal, setShowCalendarChoiceModal] = useState<boolean>(false)
  const [calendars, setCalendars] = useState<Calendar[]>()

  const exportEventToCalendar = async (calendarId: string, exportAll: boolean): Promise<void> => {
    // Luxon ISO dates have the time zone offset but Android calendar needs them in UTC
    let startDate = event.date.startDate.toUTC().toISO()
    let endDate = event.date.endDate.toUTC().toISO()
    const allDay = event.date.allDay
    if (Platform.OS === 'android' && allDay) {
      // If allDay is set to true, Android demands that the time has a midnight boundary.
      // The endDate we receive from the CMS for allDay events is always at 23:59:00.
      startDate = event.date.startDate.toFormat("yyyy-LL-dd'T'00:00:00.000'Z'")
      endDate = event.date.endDate.plus({ minutes: 1 }).toFormat("yyyy-LL-dd'T'00:00:00.000'Z'")
    }

    const eventOptions: CalendarEventWritable = {
      startDate,
      endDate,
      allDay,
      calendarId,
      location: event.location?.fullAddress,
      description: event.excerpt, // Android
      notes: event.excerpt, // iOS
      recurrenceRule:
        exportAll && event.date.recurrenceRule
          ? {
              endDate:
                event.date.recurrenceRule.options.until?.toISOString() ??
                DateTime.now().plus({ years: 3 }).toUTC().toISO(),
              frequency: formatFrequency(event.date.recurrenceRule.options.freq),
              interval: event.date.recurrenceRule.options.interval,
              // This gets overridden by `endDate and can't be set in the CMS anyway
              occurrence: 0,
              // @ts-expect-error https://github.com/wmcmahan/react-native-calendar-events/issues/159
              duration: null,
            }
          : undefined,
    }

    try {
      await RNCalendarEvents.saveEvent(event.title, eventOptions)
      showSnackbar({
        text: t('added'),
      })
    } catch (e) {
      showSnackbar({ text: 'generalError' })
      reportError(e)
    }
  }

  const checkCalendarsAndExportEvent = async (): Promise<void> => {
    const iosPermission = [PERMISSIONS.IOS.CALENDARS]
    const androidPermissions = [PERMISSIONS.ANDROID.READ_CALENDAR, PERMISSIONS.ANDROID.WRITE_CALENDAR]
    const permission = await requestMultiple(Platform.OS === 'ios' ? iosPermission : androidPermissions)
    const permissionDenied = Object.values(permission).some(permission => ['limited', 'blocked'].includes(permission))

    if (permissionDenied) {
      showSnackbar({
        text: 'noCalendarPermission',
        positiveAction: {
          label: t('settings'),
          onPress: Linking.openSettings,
        },
      })
      return
    }
    const editableCalendars = (await RNCalendarEvents.findCalendars()).filter(cal => cal.allowsModifications)
    if (editableCalendars.length === 0) {
      showSnackbar({ text: 'noCalendarFound' })
    } else if (editableCalendars.length === 1 && 0 in editableCalendars && !event.date.recurrenceRule) {
      try {
        await exportEventToCalendar(editableCalendars[0].id, false)
      } catch (e) {
        showSnackbar({ text: 'generalError' })
        reportError(e)
      }
    } else {
      setCalendars(editableCalendars)
      setShowCalendarChoiceModal(true)
    }
  }

  const chooseCalendar = async (id: string | undefined, exportAll: boolean): Promise<void> => {
    setShowCalendarChoiceModal(false)
    if (!id) {
      showSnackbar({ text: 'generalError' })
      return
    }
    try {
      await exportEventToCalendar(id, exportAll)
    } catch (e) {
      showSnackbar({ text: 'generalError' })
      reportError(e)
    }
  }

  return (
    <>
      {calendars && calendars.length > 1 && (
        <CalendarChoice
          closeModal={() => setShowCalendarChoiceModal(false)}
          modalVisible={showCalendarChoiceModal}
          chooseCalendar={chooseCalendar}
          calendars={calendars}
          eventTitle={event.title}
          recurring={!!event.date.recurrenceRule}
        />
      )}
      <StyledButton text={t('addToCalendar')} onPress={checkCalendarsAndExportEvent} />
    </>
  )
}

export default ExportEventButton