Newer
Older
CrypticOreWallet / src / pages / names / mgmt / NamePurchaseModal.tsx
@Drew Lemmy Drew Lemmy on 28 Mar 2021 7 KB feat: mobile tx list part 1.5/2
// Copyright (c) 2020-2021 Drew Lemmy
// This file is part of KristWeb 2 under AGPL-3.0.
// Full details: https://github.com/tmpim/KristWeb2/blob/master/LICENSE.txt
import { useState, useEffect, useMemo, Dispatch, SetStateAction } from "react";
import { Modal, Form, Input, Typography, Button, notification } from "antd";

import { Trans } from "react-i18next";
import { useTFns } from "@utils/i18n";

import { useSelector } from "react-redux";
import { RootState } from "@store";
import { store } from "@app";

import { Link } from "react-router-dom";

import { useWallets, Wallet } from "@wallets";
import {
  useNameSuffix, BARE_NAME_REGEX, MAX_NAME_LENGTH, isValidName
} from "@utils/krist";

import { checkName } from "./checkName";
import { handlePurchaseError } from "./handleErrors";
import { purchaseName } from "@api/names";
import { useAuthFailedModal } from "@api/AuthFailed";

import { KristValue } from "@comp/krist/KristValue";
import { AddressPicker } from "@comp/addresses/picker/AddressPicker";

import awaitTo from "await-to-js";
import { throttle } from "lodash-es";

const { Text } = Typography;

interface FormValues {
  address: string;
  name: string;
}

interface Props {
  visible: boolean;
  setVisible: Dispatch<SetStateAction<boolean>>;
}

const CHECK_THROTTLE = 300;

export function NamePurchaseModal({
  visible,
  setVisible
}: Props): JSX.Element {
  const tFns = useTFns("namePurchase.");
  const { t, tStr, tKey, tErr } = tFns;

  const [form] = Form.useForm();

  // Used to perform extra validation
  const [address, setAddress] = useState("");
  const [name, setName] = useState("");

  const [submitting, setSubmitting] = useState(false);
  const [nameAvailable, setNameAvailable] = useState<boolean | undefined>();
  const checkNameAvailable =
    useMemo(() => throttle(checkName, CHECK_THROTTLE, { leading: true }), []);

  // Modal used when auth fails
  const { showAuthFailed, authFailedContextHolder } = useAuthFailedModal();
  // Context for translation in the success notification
  const [notif, notifContextHolder] = notification.useNotification();

  // Used to format the form and determine whether or not the name can be
  // afforded by the chosen wallet
  const nameSuffix = useNameSuffix();
  const nameCost = useSelector((s: RootState) => s.node.constants.name_cost);

  const { walletAddressMap } = useWallets();

  // Used to show the validate message if the name can't be afforded
  const canAffordName = (walletAddressMap[address]?.balance || 0) >= nameCost;

  // Look up name availability when the input changes
  useEffect(() => {
    setNameAvailable(undefined);

    if (name && isValidName(name))
      checkNameAvailable(name, setNameAvailable);
  }, [name, checkNameAvailable]);

  // Take the form values and known wallet and purchase the name
  async function handleSubmit(wallet: Wallet, name: string): Promise<void> {
    const masterPassword = store.getState().masterPassword.masterPassword;
    if (!masterPassword) throw tErr("errorWalletDecrypt");

    // Verify the name can be afforded once again
    if ((wallet.balance || 0) < nameCost)
      throw tErr("errorInsufficientFunds");

    // Perform the purchase
    await purchaseName(masterPassword, wallet, name);

    // Success! Show notification and close modal
    notif.success({
      message: t(tKey("successMessage")),
      btn: <NotifSuccessButton name={name} />
    });

    setSubmitting(false);
    closeModal();
  }

  async function onSubmit() {
    setSubmitting(true);

    // Get the form values
    const [err, values] = await awaitTo(form.validateFields());
    if (err || !values) {
      // Validation errors are handled by the form
      setSubmitting(false);
      return;
    }
    const { address, name } = values;

    // Fetch the wallet from the address field and verify it actually exists
    const currentWallet = walletAddressMap[address];
    if (!currentWallet) throw tErr("errorWalletGone");

    // Begin the purchase
    handleSubmit(currentWallet, name)
      .catch(err =>
        handlePurchaseError(tFns, showAuthFailed, currentWallet, err))
      .finally(() => setSubmitting(false));
  }

  function onValuesChange(values: Partial<FormValues>) {
    setAddress(values.address || "");
    setName(values.name || "");
  }

  function closeModal() {
    form.resetFields();
    setVisible(false);
    setAddress("");
    setName("");
    setSubmitting(false);
    setNameAvailable(undefined);
  }

  const modal = <Modal
    visible={visible}

    title={tStr("modalTitle")}

    onOk={onSubmit}
    okText={<Trans t={t} i18nKey={tKey("buttonSubmit")}>
      Purchase (<KristValue value={nameCost} />)
    </Trans>}
    okButtonProps={submitting ? { loading: true } : undefined}

    onCancel={closeModal}
    cancelText={t("dialog.cancel")}
    destroyOnClose
  >
    <Form
      form={form}
      layout="vertical"

      name="namePurchase"

      onValuesChange={onValuesChange}
      onFinish={onSubmit}
    >
      {/* Name cost */}
      <div className="name-purchase-cost" style={{ marginBottom: 24 }}>
        <Trans t={t} i18nKey={tKey("nameCost")}>
          Cost to purchase: <KristValue long value={nameCost} />
        </Trans>
      </div>

      {/* Wallet/address */}
      <AddressPicker
        name="address"
        label={tStr("labelWallet")}

        // Show a message if the name can't be afforded
        validateStatus={address && !canAffordName
          ? "error" : undefined}
        help={address && !canAffordName
          ? tStr("errorInsufficientFunds") : undefined}

        value={address}
        walletsOnly={true}
      />

      {/* Name */}
      <Form.Item
        label={tStr("labelName")}
        required
      >
        <Input.Group compact style={{ display: "flex" }}>
          <Form.Item
            name="name"
            style={{ flex: 1, marginBottom: 0}}

            // Show feedback for name validity
            validateStatus={nameAvailable === false
              ? "error"
              : (nameAvailable ? "success" : undefined)}
            help={nameAvailable === false
              // Name taken
              ? <Text type="danger">{tStr("errorNameTaken")}</Text>
              : (nameAvailable
                // Name available
                ? <span className="text-green">
                  {tStr("nameAvailable")}
                </span>
                : undefined)}

            validateFirst
            rules={[
              { required: true, message: tStr("errorNameRequired") },
              { max: MAX_NAME_LENGTH, message: tStr("errorNameTooLong") },
              { pattern: BARE_NAME_REGEX, message: tStr("errorInvalidName") },
            ]}
          >
            <Input
              placeholder={tStr("placeholderName")}
              maxLength={MAX_NAME_LENGTH}
            />
          </Form.Item>

          <span className="ant-input-group-addon kw-fake-addon name-suffix">
            .{nameSuffix}
          </span>
        </Input.Group>
      </Form.Item>
    </Form>
  </Modal>;

  return <>
    {modal}

    {authFailedContextHolder}
    {notifContextHolder}
  </>;
}

export function NotifSuccessButton({ name }: { name: string }): JSX.Element {
  const { tStr } = useTFns("namePurchase.");

  return <Link to={"/network/names/" + encodeURIComponent(name)}>
    <Button type="primary">
      {tStr("successNotificationButton")}
    </Button>
  </Link>;
}