Newer
Older
CrypticOreWallet / src / krist / api / AuthFailed.tsx
@Drew Lemmy Drew Lemmy on 16 Mar 2021 3 KB fix: api.post bodies
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under GPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import { useState, useEffect, ReactElement } from "react";
import { Modal, Spin, Alert } from "antd";
import { ModalStaticFunctions } from "antd/lib/modal/confirm";

import { useTranslation } from "react-i18next";

import { Wallet, decryptWallet, useMasterPasswordOnly } from "@wallets";

import * as api from "./";

// Used to carry around information on which address failed auth
export class AuthFailedError extends api.APIError {
  constructor(message: string, public address?: string) {
    super(message);
  }
}

export type ShowAuthFailedFn = (wallet: Wallet) => void;

interface AuthFailedModalHookResponse {
  authFailedModal: Omit<ModalStaticFunctions, "warn">;
  authFailedContextHolder: ReactElement;
  showAuthFailed: ShowAuthFailedFn;
}

/**
 * Hook that returns a Modal instance and a contextHolder for it. When the
 * modal is shown, it performs an alert lookup for the given private key,
 * showing the alert if possible, and otherwise a generic 'auth failed'
 * message.
 */
export function useAuthFailedModal(): AuthFailedModalHookResponse {
  const { t } = useTranslation();
  const [modal, contextHolder] = Modal.useModal();

  // Create the auth failed modal and show it
  function showAuthFailed(wallet: Wallet) {
    modal.error({
      title: t("authFailed.title"),
      content: <ModalContents wallet={wallet} />
    });
  }

  return {
    authFailedModal: modal,
    authFailedContextHolder: contextHolder,
    showAuthFailed
  };
}

interface AlertAPIResponse {
  alert: string;
}

function ModalContents({ wallet }: { wallet: Wallet }): JSX.Element {
  const { t } = useTranslation();

  // The alert from the sync node
  const [alert, setAlert] = useState<string | null>();
  const [loading, setLoading] = useState(true);

  // Needed to decrypt the wallet, as the privatekey is required to get alert
  const masterPassword = useMasterPasswordOnly();

  // Fetch the alert from the sync node (this will usually determine if the
  // address was locked or it's just a collision)
  useEffect(() => {
    if (!masterPassword) return;

    // Errors are generally ignored here, this whole dialog is a very minor
    // edge case that only a few will see.
    (async () => {
      // Decrypt the wallet
      const decrypted = await decryptWallet(masterPassword, wallet);
      if (!decrypted) return; // This should never happen
      const { privatekey } = decrypted;

      // Perform the fetch
      api.post<AlertAPIResponse>("/addresses/alert", { privatekey })
        .then(res => setAlert(res.alert))
        .catch(console.error)
        .finally(() => setLoading(false));
    })().catch(console.error);
  }, [wallet, masterPassword]);

  return <Spin spinning={loading}>
    {alert
      // If there is a known alert from the server, this address is locked, show
      // the alert:
      ? <>
        <p>{t("authFailed.messageLocked")}</p>

        <Alert
          type="error"
          message={t("authFailed.alert")}
          description={alert}
        />
      </>
      // Otherwise, show a generic "You do not own this address." message.
      : t("authFailed.message")}
  </Spin>;
}