DeFiCh/wallet

View on GitHub
mobile-app/app/screens/AppNavigator/screens/Portfolio/components/Announcements.tsx

Summary

Maintainability
C
7 hrs
Test Coverage
import {
  ThemedIcon,
  ThemedProps,
  ThemedText,
  ThemedTextV2,
  ThemedTouchableOpacityV2,
  ThemedView,
  ThemedViewV2,
} from "@components/themed";
import { tailwind } from "@tailwind";
import { useGetAnnouncementsQuery } from "@waveshq/walletkit-ui/dist/store";
import { AnnouncementData } from "@waveshq/walletkit-core";
import { satisfies } from "semver";
import { useLanguageContext } from "@shared-contexts/LanguageProvider";
import { openURL } from "@api/linking";
import { Platform, StyleProp, TouchableOpacity, ViewStyle } from "react-native";
import { nativeApplicationVersion } from "expo-application";
import { translate } from "@translations";
import { Text } from "@components";
import { MaterialIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import { useApiStatus } from "@hooks/useApiStatus";
import { IconProps } from "@expo/vector-icons/build/createIconSet";
import {
  useServiceProviderContext,
  useThemeContext,
} from "@waveshq/walletkit-ui";
import {
  blockChainIsDownContent,
  useDeFiChainStatus,
} from "../hooks/DeFiChainStatus";
import { useDisplayAnnouncement } from "../hooks/DisplayAnnouncement";

export function Announcements(): JSX.Element {
  const { data: announcements, isSuccess } = useGetAnnouncementsQuery({});

  const { language } = useLanguageContext();
  const { hiddenAnnouncements, hideAnnouncement } = useDisplayAnnouncement();

  const { blockchainStatusAnnouncement, oceanStatusAnnouncement } =
    useDeFiChainStatus();

  const { isCustomUrl } = useServiceProviderContext();

  const { isBlockchainDown } = useApiStatus();

  const customServiceProviderIssue: AnnouncementData[] = [
    {
      lang: {
        en: "We have detected issues with your custom endpoint that is affecting your connection. You are advised to check on the status of your custom endpoint provider",
        de: "Wir haben Probleme mit deinem benutzerdefinierten Endpunkt festgestellt, die deine Verbindung beeinträchtigen. Wir empfehlen, den Status deines benutzerdefinierten Endpunktanbieters zu überprüfen.",
        "zh-Hans":
          "我们侦测到您目前使用的自定义终端点会影响到您的连接问题。建议您与供应者检查其连接状态。 ",
        "zh-Hant":
          "我們偵測到您目前使用的自定義終端點會影響到您的連接問題。 建議您與供應者檢查其連接狀態。",
        fr: "Nous avons détecté des problèmes avec votre point de terminaison personnalisé qui affectent votre connexion. Nous vous conseillons de vérifier le statut de votre fournisseur de point d'accès personnalisé.",
        es: "We have detected issues with your custom endpoint that is affecting your connection. You are advised to check on the status of your custom endpoint provider",
        it: "We have detected issues with your custom endpoint that is affecting your connection. You are advised to check on the status of your custom endpoint provider",
      },
      version: "0.0.0",
      type: "EMERGENCY",
    },
  ];

  const [emergencyMsgContent, setEmergencyMsgContent] = useState<
    AnnouncementData[] | undefined
  >();

  const emergencyAnnouncement = findDisplayedAnnouncementForVersion(
    "0.0.0",
    language,
    hiddenAnnouncements,
    emergencyMsgContent
  );
  const blockchainIsDownAnnouncement = findDisplayedAnnouncementForVersion(
    "0.0.0",
    language,
    hiddenAnnouncements,
    blockchainStatusAnnouncement
  );
  const oceanIsDownAnnouncement = findDisplayedAnnouncementForVersion(
    "0.0.0",
    language,
    hiddenAnnouncements,
    oceanStatusAnnouncement
  );
  const announcement = findDisplayedAnnouncementForVersion(
    nativeApplicationVersion ?? "0.0.0",
    language,
    hiddenAnnouncements,
    announcements
  );

  /*
    Display priority:
    1. Emergencies - Custom Provider/Blockchain Issue
    2. Outages - Blockchain API
    3. Outages - Ocean API
    4. Other announcements
  */
  const announcementToDisplay: Announcement | undefined =
    emergencyAnnouncement ??
    blockchainIsDownAnnouncement ??
    oceanIsDownAnnouncement ??
    announcement;

  useEffect(() => {
    // To display warning message in Announcement banner when blockchain is down for > 45 mins
    if (isBlockchainDown && !isCustomUrl) {
      return setEmergencyMsgContent(blockChainIsDownContent);
    } else if (isBlockchainDown && isCustomUrl) {
      return setEmergencyMsgContent(customServiceProviderIssue);
    } else {
      return setEmergencyMsgContent(undefined);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isBlockchainDown]);

  if (!isSuccess || announcementToDisplay === undefined) {
    return <></>;
  }

  return (
    <AnnouncementBannerV2
      announcement={announcementToDisplay}
      hideAnnouncement={hideAnnouncement}
      testID="announcements_banner"
      containerStyle={{ style: tailwind("mt-9 mx-5") }}
    />
  );
}

interface AnnouncementBannerProps {
  hideAnnouncement?: (id: string) => void;
  announcement: Announcement;
  testID: string;
  // eslint-disable-next-line react/no-unused-prop-types
  containerStyle?: ThemedProps & { style?: ThemedProps & StyleProp<ViewStyle> };
}

export function AnnouncementBanner({
  hideAnnouncement,
  announcement,
  testID,
}: AnnouncementBannerProps): JSX.Element {
  const { isLight } = useThemeContext();
  const icons: { [key in AnnouncementData["type"]]: IconProps<any>["name"] } = {
    EMERGENCY: "warning",
    OTHER_ANNOUNCEMENT: "campaign",
    OUTAGE: "warning",
    SCAN: "campaign",
  };
  const isOtherAnnouncement =
    announcement.type === undefined ||
    announcement.type === "OTHER_ANNOUNCEMENT";

  return (
    <ThemedView
      testID={testID}
      style={tailwind("px-4 py-3 flex-row items-center")}
      light={tailwind({
        "bg-primary-700": isOtherAnnouncement,
        "bg-warning-100": !isOtherAnnouncement,
      })}
      dark={tailwind({
        "bg-darkprimary-700": isOtherAnnouncement,
        "bg-darkwarning-100": !isOtherAnnouncement,
      })}
    >
      {announcement.id !== undefined && (
        <MaterialIcons
          style={tailwind([
            "mr-1",
            {
              "text-white": !isLight || isOtherAnnouncement,
              "text-gray-900": !(!isLight || isOtherAnnouncement),
            },
          ])}
          iconType="MaterialIcons"
          name="close"
          size={18}
          onPress={() => {
            if (announcement.id === undefined) {
              return;
            }
            if (hideAnnouncement !== undefined) {
              hideAnnouncement(announcement.id);
            }
          }}
          testID="close_announcement"
        />
      )}

      <MaterialIcons
        style={tailwind([
          "mr-2.5",
          {
            "text-white": isOtherAnnouncement,
            "text-warning-600": !isOtherAnnouncement && isLight,
            "text-darkwarning-600": !isOtherAnnouncement && !isLight,
          },
        ])}
        iconType="MaterialIcons"
        name={icons[announcement.type ?? "OTHER_ANNOUNCEMENT"]}
        size={
          icons[announcement.type ?? "OTHER_ANNOUNCEMENT"] === "warning"
            ? 24
            : 28
        }
      />
      <Text
        style={tailwind([
          "text-xs flex-auto",
          {
            "text-white": !isLight || (isLight && isOtherAnnouncement),
            "text-gray-900": !isOtherAnnouncement && isLight,
          },
        ])}
        testID="announcements_text"
      >
        {`${announcement.content} `}
      </Text>
      {announcement.url !== undefined && announcement.url.length !== 0 && (
        <TouchableOpacity
          onPress={async () => await openURL(announcement.url)}
          style={tailwind("ml-2 py-1")}
        >
          <ThemedText
            style={tailwind("text-sm font-medium")}
            light={tailwind({
              "text-white": isOtherAnnouncement,
              "text-warning-600": !isOtherAnnouncement,
            })}
            dark={tailwind({
              "text-white": isOtherAnnouncement,
              "text-darkwarning-600": !isOtherAnnouncement,
            })}
          >
            {translate("components/Announcements", "DETAILS")}
          </ThemedText>
        </TouchableOpacity>
      )}
    </ThemedView>
  );
}

export function AnnouncementBannerV2({
  hideAnnouncement,
  announcement,
  testID,
  containerStyle,
}: AnnouncementBannerProps): JSX.Element {
  const { isLight } = useThemeContext();
  const isOtherAnnouncement =
    announcement.type === undefined ||
    announcement.type === "OTHER_ANNOUNCEMENT";
  return (
    <ThemedViewV2
      testID={testID}
      style={[
        tailwind(
          "relative px-5 py-3 flex flex-row items-center border-0.5 rounded-xl-v2",
          {
            "border-mono-light-v2-900": isOtherAnnouncement && isLight,
            "border-mono-dark-v2-900": isOtherAnnouncement && !isLight,
            "border-orange-v2": announcement.type === "OUTAGE",
            "border-red-v2": announcement.type === "EMERGENCY",
          }
        ),
        containerStyle?.style,
      ]}
      light={containerStyle?.light}
      dark={containerStyle?.dark}
    >
      <ThemedTextV2
        light={tailwind({ "text-mono-light-v2-900": isOtherAnnouncement })}
        dark={tailwind({ "text-mono-dark-v2-900": isOtherAnnouncement })}
        style={tailwind([
          "text-xs flex-auto font-normal-v2",
          {
            "text-orange-v2": announcement.type === "OUTAGE",
            "text-red-v2": announcement.type === "EMERGENCY",
          },
        ])}
        testID="announcements_text"
      >
        {`${announcement.content} `}
      </ThemedTextV2>
      {announcement.url !== undefined && announcement.url.length !== 0 && (
        <ThemedTouchableOpacityV2
          onPress={async () => await openURL(announcement.url)}
          style={tailwind("ml-3.5 pl-1 py-1")}
          testID={`${testID}_url_button`}
        >
          <ThemedIcon
            iconType="Feather"
            name="external-link"
            size={20}
            light={tailwind({ "text-mono-light-v2-900": isOtherAnnouncement })}
            dark={tailwind({ "text-mono-dark-v2-900": isOtherAnnouncement })}
            style={tailwind({
              "text-orange-v2": announcement.type === "OUTAGE",
              "text-red-v2": announcement.type === "EMERGENCY",
            })}
          />
        </ThemedTouchableOpacityV2>
      )}
      {announcement.icon !== undefined && announcement.icon.length !== 0 && (
        <ThemedIcon
          size={20}
          name={announcement.icon}
          iconType="Feather"
          light={tailwind({ "text-mono-light-v2-900": isOtherAnnouncement })}
          dark={tailwind({ "text-mono-dark-v2-900": isOtherAnnouncement })}
          style={tailwind("pl-4", {
            "text-orange-v2": announcement.type === "OUTAGE",
            "text-red-v2": announcement.type === "EMERGENCY",
          })}
        />
      )}
      {announcement.id !== undefined && (
        <ThemedViewV2 style={tailwind("absolute -top-2 -right-2 rounded-full")}>
          <ThemedTouchableOpacityV2
            onPress={() => {
              if (announcement.id === undefined) {
                return;
              }
              if (hideAnnouncement !== undefined) {
                hideAnnouncement(announcement.id);
              }
            }}
            style={tailwind("border-0")}
          >
            <ThemedIcon
              testID="close_announcement"
              iconType="MaterialIcons"
              name="cancel"
              size={20}
              light={tailwind({
                "text-mono-light-v2-900": isOtherAnnouncement,
              })}
              dark={tailwind({ "text-mono-dark-v2-900 ": isOtherAnnouncement })}
              style={tailwind("", {
                "text-orange-v2": announcement.type === "OUTAGE",
                "text-red-v2": announcement.type === "EMERGENCY",
              })}
            />
          </ThemedTouchableOpacityV2>
        </ThemedViewV2>
      )}
    </ThemedViewV2>
  );
}

export interface Announcement {
  content: string;
  url: string;
  id?: string;
  type: AnnouncementData["type"];
  icon?: string;
}

export function findDisplayedAnnouncementForVersion(
  version: string,
  language: string,
  hiddenAnnouncements: string[],
  announcements?: AnnouncementData[]
): Announcement | undefined {
  if (announcements === undefined || announcements.length === 0) {
    return;
  }

  for (const announcement of announcements) {
    const lang: any = announcement.lang;
    const platformUrl: any = announcement.url;

    if (
      ((Platform.OS !== "ios" &&
        Platform.OS !== "android" &&
        announcement.type !== "SCAN") ||
        satisfies(version, announcement.version)) &&
      getDisplayAnnouncement(hiddenAnnouncements, announcement)
    ) {
      return {
        content: lang[language] ?? lang.en,
        url: platformUrl !== undefined ? platformUrl[Platform.OS] : undefined,
        id: announcement.id,
        type: announcement.type,
      };
    }
  }
}

function getDisplayAnnouncement(
  hiddenAnnouncements: string[],
  announcement: AnnouncementData
): boolean {
  if (announcement === undefined) {
    return false;
  }

  if (hiddenAnnouncements.length > 0 && announcement.id !== undefined) {
    return !hiddenAnnouncements.includes(announcement.id);
  }

  return true;
}