// 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, useRef, Dispatch, SetStateAction } from "react"; import { Modal, notification } from "antd"; import { useTFns } from "@utils/i18n"; import { useWallets, useMasterPasswordOnly, decryptAddresses, DecryptErrorGone, DecryptErrorFailed, ValidDecryptedAddresses } from "@wallets"; import { useNameSuffix } from "@utils/krist"; import { transferNames, updateNames } from "@api/names"; import { useAuthFailedModal } from "@api/AuthFailed"; import { NameOption, fetchNames, buildLUT } from "./lookupNames"; import { handleEditError } from "./handleErrors"; import { lockNameTable, NameTableLock } from "../tableLock"; import { useNameEditForm } from "./NameEditForm"; import { useEditProgress } from "./EditProgress"; import { showConfirmModal } from "./ConfirmModal"; import { SuccessNotifContent } from "./SuccessNotifContent"; import awaitTo from "await-to-js"; export type Mode = "transfer" | "update"; interface Props { visible: boolean; setVisible: Dispatch<SetStateAction<boolean>>; name?: string; aRecord?: string | null; mode: Mode; } export function NameEditModal({ visible, setVisible, name, aRecord, mode }: Props): JSX.Element { // Return translated strings with the correct prefix depending on the mode const tFns = useTFns(mode === "transfer" ? "nameTransfer." : "nameUpdate."); const { t, tKey, tStr, tErr } = tFns; const [submitting, setSubmitting] = useState(false); // Pause updates of the name table if it's visible when submitting const tableLock = useRef<NameTableLock>(); // Confirmation modal used for when transferring multiple names. // This is created here to provide a translation context for the modal. const [confirmModal, contextHolder] = Modal.useModal(); // Modal used when auth fails const { showAuthFailed, authFailedContextHolder } = useAuthFailedModal(); // Context for translation in the success notification const [notif, notifContextHolder] = notification.useNotification(); // Used to fetch the list of available names const { walletAddressMap } = useWallets(); const nameSuffix = useNameSuffix(); // Used to decrypt the wallets for transfer/update const masterPassword = useMasterPasswordOnly(); // Create the form. This is usually not rendered during submission. const { form, formInstance, resetFields } = useNameEditForm({ name, aRecord, mode, submitting, onSubmit, tFns }); // Progress bar for bulk edits const { progressBar, onProgress, initProgress, resetProgress } = useEditProgress(tFns); // Wrap the handleError function const onError = handleEditError.bind( handleEditError, tFns, showAuthFailed, walletAddressMap ); // Actually perform the bulk name edit async function handleSubmit( names: NameOption[], recipient?: string, aRecord?: string ) { if (!masterPassword) return; // Attempt to decrypt each wallet. Group the names by wallet to create a // LUT of decrypted privatekeys. const nameOwners = names.map(n => n.owner); const decryptResults = await decryptAddresses( masterPassword, walletAddressMap, nameOwners ); // Check if there were any decryption errors if (Object.values(decryptResults).includes(DecryptErrorGone)) throw tErr("errorWalletGone"); const decryptFailed = Object.entries(decryptResults) .find(([_, r]) => r === DecryptErrorFailed); if (decryptFailed) { throw new Error(t( tKey("errorWalletDecrypt"), { address: decryptFailed[0] } )); } // We've already validated the names, so these can be cast const finalAddresses = decryptResults as ValidDecryptedAddresses; const finalNames = names.map(n => ({ name: n.key, owner: n.owner })); // Lock the name table if present tableLock?.current?.release(); tableLock.current = lockNameTable(); if (mode === "transfer") { // Transfer the names await transferNames(finalAddresses, finalNames, recipient!, onProgress); } else if (mode === "update") { // Update the names await updateNames(finalAddresses, finalNames, aRecord!, onProgress); } // Success! Show notification and close modal const count = names.length; notif.success({ message: t(tKey("successMessage"), { count }), description: <SuccessNotifContent count={count} recipient={recipient} mode={mode} /> }); setSubmitting(false); tableLock?.current?.release(); closeModal(); } // Validate the form and consolidate all the data before submission async function onSubmit() { setSubmitting(true); // Get the form values const [err, values] = await awaitTo(formInstance.validateFields()); if (err || !values) { // Validation errors are handled by the form setSubmitting(false); return; } const { names, recipient, aRecord } = values; // Lookup the names list one last time, to associate the name owners // to the wallets for decryption, and to show the correct confirmation // modal. const nameGroups = await fetchNames(t, nameSuffix, walletAddressMap); if (!nameGroups) { // This shouldn't happen, but if the owner suddenly has no names anymore, // show an error. notification.error({ message: tStr("errorNotifTitle"), description: tStr("errorNameRequired") }); setSubmitting(false); return; } // Filter out names owned by the recipient, just in case. const filteredNameGroups = nameGroups .filter(g => g.wallet.address !== recipient); // The names from filteredNameGroups that we actually want to edit const namesLUT = buildLUT(names); const allNames = filteredNameGroups.flatMap(g => g.names); const filteredNames = allNames.filter(n => !!namesLUT[n.key]); // All the names owned by the wallets, used for the confirmation modal. const allNamesCount = allNames.length; const count = filteredNames.length; // Don't return this promise, so the confirm modal closes immediately const triggerSubmit = () => { initProgress(count); handleSubmit(filteredNames, recipient, aRecord) .catch(onError) .finally(() => { setSubmitting(false); tableLock?.current?.release(); }); }; if (mode === "transfer" && count > 1) { // If transferring multiple names, prompt for confirmation showConfirmModal( t, confirmModal, count, allNamesCount, recipient!, triggerSubmit, setSubmitting ); } else { // Otherwise, submit straight away triggerSubmit(); } } function closeModal() { // Don't allow closing the modal while submitting if (submitting) return; setVisible(false); resetFields(); resetProgress(); tableLock?.current?.release(); } return <> <Modal visible={visible} title={tStr("modalTitle")} onOk={onSubmit} okText={tStr("buttonSubmit")} okButtonProps={submitting ? { loading: true } : undefined} onCancel={closeModal} cancelText={t("dialog.cancel")} destroyOnClose > {/* Only render the form if not submitting */} {!submitting && form} {submitting && progressBar} </Modal> {/* Give the modals somewhere to find the context from. This is done * outside of the modal so that they don't get immediately destroyed when * the modal closes. */} {contextHolder} {authFailedContextHolder} {notifContextHolder} </>; }