teamdigitale/italia-app

View on GitHub
ts/utils/calendar.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { IOToast } from "@pagopa/io-app-design-system";
import * as E from "fp-ts/lib/Either";
import * as O from "fp-ts/lib/Option";
import * as TE from "fp-ts/lib/TaskEither";
import { identity, pipe } from "fp-ts/lib/function";
import { Platform } from "react-native";
import RNCalendarEvents, { Calendar } from "react-native-calendar-events";
import { CreatedMessageWithContentAndAttachments } from "../../definitions/backend/CreatedMessageWithContentAndAttachments";
import { TranslationKeys } from "../../locales/locales";
import I18n from "../i18n";
import { AddCalendarEventPayload } from "../store/actions/calendarEvents";
import { CalendarEvent } from "../store/reducers/entities/calendarEvents/calendarEventsByMessageId";
import { formatDateAsReminder } from "./dates";

/**
 * Utility functions to interact with the device calendars
 */

/**
 * A type that brings info about calendar authorization
 * asked means (when true) that the authorized values comes from user choise
 * otherwise comes from a previous recorded choice
 */
type CalendarAuthorization = { authorized: boolean; asked: boolean };

/**
 * A function that checks if the user has already permission to read/write to Calendars
 * and in case of not already defined permission try to get the authorization.
 */
export async function checkAndRequestPermission(): Promise<CalendarAuthorization> {
  try {
    const status = await RNCalendarEvents.checkPermissions();
    switch (status) {
      case "authorized":
        // the app is authorized to access the service
        return { authorized: true, asked: false };
      case "restricted":
        // the app is not authorized to access the service
        // (e.g. parental control on iOS or when the user
        // denies definitely on Android)
        return { authorized: false, asked: false };
      case "denied":
        // the user explicitly denied access to the service for the app
        if (Platform.OS === "ios") {
          return { authorized: false, asked: false };
        }
        // but in Android we can ask for it again
        // (i.e. shouldShowRequestPermissionRationale returns true)
        break;
      case "undetermined":
        // the user has not yet made a choice
        break;
      default:
        // let's try to request permissions as a fallback
        // if a new unhandled case arises
        break;
    }

    // if not returned yet, we can ask for the authorization
    const newStatus = await RNCalendarEvents.requestPermissions();
    return { authorized: newStatus === "authorized", asked: true };
  } catch {
    return { authorized: false, asked: false };
  }
}

/**
 * This Type has been introduced after this story https://www.pivotaltracker.com/story/show/172079415
 * to solve the bug on some android devices (mostly the one running MIUI) naming their local calendar
 * with camel case notation this is a common situation as figured on a related reddit post
 * https://www.reddit.com/r/Xiaomi/comments/84jgdn/google_calendars_not_syncing_or_even_requesting/
 * and can be seen on MIUI's github repository
 * https://github.com/ChameleonOS/miui_framework/blob/master/java/miui/provider/ExtraCalendarContracts.java
 */

type CalendarTitleTranslation = { [key: string]: TranslationKeys };

const calendarTitleTranslations: CalendarTitleTranslation = {
  calendar_displayname_local: "profile.preferences.calendar.local_calendar",
  calendar_displayname_birthday:
    "profile.preferences.calendar.birthday_calendar"
};

export function convertLocalCalendarName(calendarTitle: string) {
  return pipe(
    calendarTitleTranslations[calendarTitle.trim().toLowerCase()],
    O.fromNullable,
    O.fold(
      () => calendarTitle,
      s => I18n.t(s)
    )
  );
}

/**
 * return a TaskEither where left is an error
 * and right is a boolean -> true === the is in calendar
 * @param eventId
 */
export const legacyIsEventInCalendar = (
  eventId: string
): TE.TaskEither<Error, boolean> => {
  const authTask = TE.tryCatch(
    () => checkAndRequestPermission(),
    message => new Error(String(message))
  );
  const findTask = TE.tryCatch(
    () => RNCalendarEvents.findEventById(eventId),
    E.toError
  );
  return pipe(
    authTask,
    TE.chain(
      TE.fromPredicate(
        auth => auth.authorized,
        () => new Error("Not authorized")
      )
    ),
    TE.chain(() => findTask),
    TE.map(ev => ev !== null)
  );
};
/**
 * Check if an event for endDate with that title already exists in the calendar.
 * Return the event id if it is found
 */
export const searchEventInCalendar = async (
  endDate: Date,
  title: string
): Promise<O.Option<string>> => {
  const startDate = new Date(endDate.getTime());
  return RNCalendarEvents.fetchAllEvents(
    formatDateAsReminder(new Date(startDate.setDate(endDate.getDate() - 1))),
    formatDateAsReminder(endDate)
  )
    .then(
      events =>
        pipe(
          events,
          O.fromNullable,
          O.chainNullableK(evs =>
            evs.find(
              e =>
                e.title === title &&
                e.endDate &&
                new Date(e.endDate).getDay() === endDate.getDay()
            )
          ),
          O.map(ev => O.some(ev.id)),
          O.getOrElseW(() => O.none)
        ),
      // handle promise rejection
      () => O.none
    )
    .catch(() => O.none);
};

export const saveCalendarEvent = (
  calendar: Calendar,
  message: CreatedMessageWithContentAndAttachments,
  dueDate: Date,
  title: string,
  onAddCalendarEvent?: (calendarEvent: AddCalendarEventPayload) => void
) => {
  const dueDateIsoString = dueDate.toISOString();
  RNCalendarEvents.saveEvent(title, {
    calendarId: calendar.id,
    startDate: dueDateIsoString,
    endDate: dueDateIsoString,
    allDay: true,
    alarms: []
  })
    .then(eventId => {
      IOToast.success(
        I18n.t("messages.cta.reminderAddSuccess", {
          title,
          calendarTitle: convertLocalCalendarName(calendar.title)
        })
      );
      const messageId = message.id;
      if (onAddCalendarEvent) {
        onAddCalendarEvent({
          messageId,
          eventId
        });
      }
    })
    .catch(_ => IOToast.error(I18n.t("messages.cta.reminderAddFailure")));
};

export const removeCalendarEventFromDeviceCalendar = (
  calendarEvent: CalendarEvent | undefined,
  onRemoveEvent?: (calendarEvent: CalendarEvent) => void
) => {
  if (calendarEvent) {
    RNCalendarEvents.removeEvent(calendarEvent.eventId)
      .then(_ => {
        IOToast.success(I18n.t("messages.cta.reminderRemoveSuccess"));
        if (onRemoveEvent) {
          onRemoveEvent(calendarEvent);
        }
      })
      .catch(_ => IOToast.error(I18n.t("messages.cta.reminderRemoveFailure")));
  } else {
    IOToast.error(I18n.t("messages.cta.reminderRemoveFailure"));
  }
};

/**
 * Check and request the permission to access the device calendar
 * @returns a boolean that is true if the permission is granted
 */
export const requestCalendarPermission = async (): Promise<boolean> => {
  const checkResult = await RNCalendarEvents.checkPermissions();
  if (checkResult === "authorized") {
    return true;
  }

  const requestStatus = await RNCalendarEvents.requestPermissions();
  return requestStatus === "authorized";
};

/**
 * Check if the event is in the device calendar
 */
export const isEventInCalendar = (eventId: string) =>
  pipe(
    TE.tryCatch(() => requestCalendarPermission(), E.toError),
    TE.chain(TE.fromPredicate(identity, () => Error("Permission not granted"))),
    TE.chain(() =>
      TE.tryCatch(() => RNCalendarEvents.findEventById(eventId), E.toError)
    ),
    TE.map(ev => ev !== null)
  );

/**
 * Add an event to the device calendar
 */
export const saveEventToDeviceCalendarTask = (
  calendarId: string,
  dueDate: Date,
  title: string
) =>
  TE.tryCatch(
    () =>
      RNCalendarEvents.saveEvent(title, {
        calendarId,
        startDate: dueDate.toISOString(),
        endDate: dueDate.toISOString(),
        allDay: true,
        alarms: []
      }),
    E.toError
  );

/**
 * Remove an event from the device calendar
 */
export const removeEventFromDeviceCalendarTask = (eventId: string) =>
  pipe(
    TE.tryCatch(() => RNCalendarEvents.removeEvent(eventId), E.toError),
    TE.map(_ => eventId)
  );

/**
 * Find the device calendars
 */
export const findDeviceCalendarsTask = TE.tryCatch(
  () => RNCalendarEvents.findCalendars(),
  E.toError
);