teamdigitale/italia-app

View on GitHub
ts/store/reducers/profile.ts

Summary

Maintainability
D
1 day
Test Coverage
/**
 * A reducer for the Profile.
 * It only manages SUCCESS actions because all UI state properties (like loading/error)
 * are managed by different global reducers.
 */
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as pot from "@pagopa/ts-commons/lib/pot";
import { createSelector } from "reselect";
import { getType } from "typesafe-actions";
import { EmailAddress } from "../../../definitions/backend/EmailAddress";
import { InitializedProfile } from "../../../definitions/backend/InitializedProfile";
import { capitalize } from "../../utils/strings";
import {
  profileLoadFailure,
  profileLoadRequest,
  profileLoadSuccess,
  profileUpsert,
  resetProfileState
} from "../actions/profile";
import { Action } from "../actions/types";
import { ServicesPreferencesModeEnum } from "../../../definitions/backend/ServicesPreferencesMode";
import { ReminderStatusEnum } from "../../../definitions/backend/ReminderStatus";
import { PushNotificationsContentTypeEnum } from "../../../definitions/backend/PushNotificationsContentType";
import { GlobalState } from "./types";
import { ProfileError } from "./profileErrorType";

export type ProfileState = pot.Pot<InitializedProfile, ProfileError>;

const INITIAL_STATE: ProfileState = pot.none;

// Selectors

export const profileSelector = (state: GlobalState): ProfileState =>
  state.profile;

export const isEmailEnabledSelector = createSelector(profileSelector, profile =>
  pot.getOrElse(
    pot.map(profile, p => p.is_email_enabled),
    false
  )
);

export const isInboxEnabledSelector = createSelector(profileSelector, profile =>
  pot.isSome(profile) && InitializedProfile.is(profile.value)
    ? profile.value.is_inbox_enabled
    : false
);

export const getProfileEmail = (
  user: InitializedProfile
): O.Option<EmailAddress> => O.fromNullable(user.email);

export const getProfileSpidEmail = (
  user: InitializedProfile
): O.Option<EmailAddress> => O.fromNullable(user.spid_email);

// return the email address (as a string) if the profile pot is some and its value is of kind InitializedProfile and it has an email
export const profileEmailSelector = createSelector(
  profileSelector,
  (profile: ProfileState): O.Option<string> =>
    pot.getOrElse(
      pot.map(profile, p => getProfileEmail(p)),
      O.none
    )
);

/**
 * Return the name of the profile if some, else undefined
 */
export const profileNameSelector = createSelector(
  profileSelector,
  (profile: ProfileState): string | undefined =>
    pot.getOrElse(
      pot.map(profile, p => capitalize(p.name)),
      undefined
    )
);

/**
 * Return the fiscal code of the profile if some, else undefined
 */
export const profileFiscalCodeSelector = createSelector(
  profileSelector,
  (profile: ProfileState): string | undefined =>
    pot.getOrElse(
      pot.map(profile, p => p.fiscal_code),
      undefined
    )
);

/**
 * The complete name + surname
 */
export const profileNameSurnameSelector = createSelector(
  profileSelector,
  (profile: ProfileState): string | undefined =>
    pot.getOrElse(
      pot.map(profile, p => capitalize(`${p.name} ${p.family_name}`)),
      undefined
    )
);

// return true if the profile has an email
export const hasProfileEmail = (user: InitializedProfile): boolean =>
  user.email !== undefined;

// return true if the profile has an email
export const hasProfileEmailSelector = createSelector(
  profileSelector,
  (profile: ProfileState): boolean =>
    pot.getOrElse(
      pot.map(profile, p => hasProfileEmail(p)),
      false
    )
);

// return the profile services preference mode
export const profileServicePreferencesModeSelector = createSelector(
  profileSelector,
  (profile: ProfileState): ServicesPreferencesModeEnum | undefined =>
    pot.getOrElse(
      pot.map(profile, p => p.service_preferences_settings.mode),
      undefined
    )
);
// return if the profile email user is already taken
export const isProfileEmailAlreadyTakenSelector = createSelector(
  profileSelector,
  (profile: ProfileState): boolean | undefined =>
    pot.getOrElse(
      pot.map(profile, p => p.is_email_already_taken),
      undefined
    )
);

// return true if the profile services preference mode is set (mode is set only when AUTO or MANUAL is the current mode)
export const isServicesPreferenceModeSet = (
  mode: ServicesPreferencesModeEnum | undefined
): boolean =>
  [ServicesPreferencesModeEnum.AUTO, ServicesPreferencesModeEnum.MANUAL].some(
    sp => sp === mode
  );

// return true if the profile has an email and it is validated
export const isProfileEmailValidated = (user: InitializedProfile): boolean =>
  user.is_email_validated !== undefined && user.is_email_validated === true;

// return true if the profile has an email and it is validated
export const isProfileEmailAlreadyTaken = (user: InitializedProfile): boolean =>
  !!user.is_email_already_taken;

// Returns true if the profile has service_preferences_settings set to Legacy.
// A profile that has completed onboarding will have this value mandatory set to auto or manual
export const isProfileFirstOnBoarding = (user: InitializedProfile): boolean =>
  user.service_preferences_settings.mode === ServicesPreferencesModeEnum.LEGACY;

// Same as above, but Selector
export const isProfileFirstOnBoardingSelector = createSelector(
  profileSelector,
  (profile: ProfileState): boolean | undefined =>
    pot.getOrElse(
      pot.map(profile, p => isProfileFirstOnBoarding(p)),
      undefined
    )
);

// return true if the profile pot is some and its field is_email_validated exists and it's true
export const isProfileEmailValidatedSelector = (state: GlobalState) =>
  pipe(
    state,
    profileSelector,
    profileStatusPot =>
      pot.map(
        profileStatusPot,
        profileStatus =>
          hasProfileEmail(profileStatus) &&
          isProfileEmailValidated(profileStatus)
      ),
    profileEmailValidatedPot => pot.getOrElse(profileEmailValidatedPot, false)
  );

export const isEmailValidatedSelector = createSelector(
  isProfileEmailValidatedSelector,
  isEmailValidated => isEmailValidated
);

export const profileHasErrorSelector = (state: GlobalState) =>
  pipe(state.profile, pot.isError);

export const profileIsUpdatingSelector = (state: GlobalState) =>
  pipe(state.profile, pot.isUpdating);

export const pushNotificationRemindersEnabledSelector = (state: GlobalState) =>
  pipe(
    state.profile,
    pot.toOption,
    O.chainNullableK(profile => profile.reminder_status),
    O.map(reminderStatus => reminderStatus === ReminderStatusEnum.ENABLED),
    O.getOrElse(() => false)
  );

export const pushNotificationPreviewEnabledSelector = (state: GlobalState) =>
  pipe(
    state.profile,
    pot.toOption,
    O.chainNullableK(profile => profile.push_notifications_content_type),
    O.map(
      pushNotificationContentType =>
        pushNotificationContentType === PushNotificationsContentTypeEnum.FULL
    ),
    O.getOrElse(() => false)
  );

// return the profile notification settings actual state or undefined if ProfileState pot is in an Error state
export const profileNotificationSettingsSelector = createSelector(
  profileSelector,
  (
    profile: ProfileState
  ):
    | { reminder: boolean | undefined; preview: boolean | undefined }
    | undefined =>
    pot.getOrElse(
      pot.map(profile, p => ({
        reminder:
          p.reminder_status === undefined
            ? undefined
            : p.reminder_status === ReminderStatusEnum.ENABLED,
        preview:
          p.push_notifications_content_type === undefined
            ? undefined
            : p.push_notifications_content_type ===
              PushNotificationsContentTypeEnum.FULL
      })),
      undefined
    )
);

// return the tos version or undefined if ProfileState pot is in an Error state
export const tosVersionSelector = createSelector(
  profileSelector,
  (profile: ProfileState): number | undefined =>
    pot.getOrElse(
      pot.map(profile, p => p.accepted_tos_version),
      undefined
    )
);
const reducer = (
  state: ProfileState = INITIAL_STATE,
  action: Action
): ProfileState => {
  switch (action.type) {
    case getType(resetProfileState):
      return pot.none;

    case getType(profileLoadRequest):
      return pot.toLoading(state);

    case getType(profileLoadSuccess):
      // Store the loaded Profile in the store
      return pot.some(action.payload);

    case getType(profileLoadFailure):
      return pot.toError(state, action.payload);

    //
    // upsert
    //

    case getType(profileUpsert.request):
      if (!pot.isSome(state)) {
        return state;
      }
      return pot.toUpdating(state, { ...state.value, ...action.payload });

    case getType(profileUpsert.success):
      if (pot.isSome(state)) {
        const currentProfile = state.value;
        const newProfile = action.payload.newValue;
        // The API profile is still absent
        if (
          !currentProfile.has_profile &&
          newProfile.has_profile &&
          isProfileFirstOnBoarding(newProfile)
        ) {
          return pot.some({
            ...currentProfile,
            has_profile: true,
            email: newProfile.email,
            is_email_enabled: newProfile.is_email_enabled === true,
            is_inbox_enabled: newProfile.is_inbox_enabled === true,
            is_email_validated: newProfile.is_email_validated === true,
            is_webhook_enabled: newProfile.is_webhook_enabled === true,
            preferred_languages: newProfile.preferred_languages,
            blocked_inbox_or_channels: newProfile.blocked_inbox_or_channels,
            accepted_tos_version: newProfile.accepted_tos_version,
            service_preferences_settings:
              newProfile.service_preferences_settings,
            reminder_status: newProfile.reminder_status,
            push_notifications_content_type:
              newProfile.push_notifications_content_type,
            version: 0
          });
        }

        // We already have a API profile
        if (
          currentProfile.has_profile &&
          newProfile.has_profile &&
          currentProfile.version < newProfile.version
        ) {
          return pot.some({
            ...currentProfile,
            email: newProfile.email,
            last_app_version: newProfile.last_app_version,
            is_inbox_enabled: newProfile.is_inbox_enabled === true,
            is_email_enabled: newProfile.is_email_enabled === true,
            is_email_validated: newProfile.is_email_validated === true,
            is_webhook_enabled: newProfile.is_webhook_enabled === true,
            preferred_languages: newProfile.preferred_languages,
            blocked_inbox_or_channels: newProfile.blocked_inbox_or_channels,
            accepted_tos_version: newProfile.accepted_tos_version,
            service_preferences_settings:
              newProfile.service_preferences_settings,
            reminder_status: newProfile.reminder_status,
            push_notifications_content_type:
              newProfile.push_notifications_content_type,
            version: newProfile.version
          });
        }

        return state;
      } else {
        // We can't merge an updated profile if we haven't loaded a full
        // profile yet
        return state;
      }

    case getType(profileUpsert.failure):
      return pot.toError(state, action.payload);

    default:
      return state;
  }
};

export default reducer;