import React, { useState, useRef, useEffect } from "react"; import { Modal, Form, Input, Checkbox, Collapse, Button, Tooltip, Typography, Row, Col, message, notification, Grid } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; import { useDispatch, useSelector, shallowEqual } from "react-redux"; import { RootState } from "../../store"; import { useTranslation, Trans } from "react-i18next"; import { generatePassword } from "../../utils"; import { FakeUsernameInput } from "../../components/auth/FakeUsernameInput"; import { CopyInputButton } from "../../components/CopyInputButton"; import { getSelectWalletCategory } from "../../components/wallets/SelectWalletCategory"; import { WalletFormatName, applyWalletFormat, formatNeedsUsername } from "../../krist/wallets/formats/WalletFormat"; import { getSelectWalletFormat } from "../../components/wallets/SelectWalletFormat"; import { makeV2Address } from "../../krist/AddressAlgo"; import { addWallet, decryptWallet, editWallet, Wallet, ADDRESS_LIST_LIMIT } from "../../krist/wallets/Wallet"; const { Text } = Typography; const { useBreakpoint } = Grid; interface FormValues { label?: string; category: string; walletUsername: string; password: string; format: WalletFormatName; save: boolean; } interface Props { create?: boolean; editing?: Wallet; visible: boolean; setVisible: React.Dispatch<React.SetStateAction<boolean>>; setAddExistingVisible?: React.Dispatch<React.SetStateAction<boolean>>; } export function AddWalletModal({ create, editing, visible, setVisible, setAddExistingVisible }: Props): JSX.Element { if (editing && create) throw new Error("AddWalletModal: 'editing' and 'create' simultaneously, uh oh!"); const initialFormat = "kristwallet"; // TODO: change for edit modal // Required to encrypt new wallets const { masterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); // Required to check for existing wallets const { wallets } = useSelector((s: RootState) => s.wallets, shallowEqual); const syncNode = useSelector((s: RootState) => s.node.syncNode); const addressPrefix = useSelector((s: RootState) => s.node.currency.address_prefix); const dispatch = useDispatch(); const { t } = useTranslation(); const bps = useBreakpoint(); const [form] = Form.useForm<FormValues>(); const passwordInput = useRef<Input>(null); const [calculatedAddress, setCalculatedAddress] = useState<string | undefined>(); const [formatState, setFormatState] = useState<WalletFormatName>(editing?.format || initialFormat); function closeModal() { form.resetFields(); // Make sure to generate another password on re-open setCalculatedAddress(undefined); setVisible(false); } function addExistingWallet() { if (!setAddExistingVisible) return; setAddExistingVisible(true); closeModal(); } async function onSubmit() { if (!masterPassword) return notification.error({ message: t("addWallet.errorUnexpectedTitle"), description: t("masterPassword.errorNoPassword") }); const values = await form.validateFields(); if (!values.password) return; try { if (editing) { // Edit wallet // Double check the destination wallet exists if (!wallets[editing.id]) return notification.error({ message: t("addWallet.errorMissingWalletTitle"), description: t("addWallet.errorMissingWalletDescription") }); // If the address changed, check that a wallet doesn't already exist // with this address if (editing.address !== calculatedAddress && Object.values(wallets).find(w => w.address === calculatedAddress)) { return notification.error({ message: t("addWallet.errorDuplicateWalletTitle"), description: t("addWallet.errorDuplicateWalletDescription") }); } await editWallet(dispatch, syncNode, addressPrefix, masterPassword, editing, values, values.password); message.success(t("addWallet.messageSuccessEdit")); closeModal(); } else { // Add/create wallet // Check if we reached the wallet limit if (Object.keys(wallets).length >= ADDRESS_LIST_LIMIT) { return notification.error({ message: t("addWallet.errorWalletLimitTitle"), description: t("addWallet.errorWalletLimitDescription") }); } // Check if the wallet already exists if (Object.values(wallets).find(w => w.address === calculatedAddress)) { return notification.error({ message: t("addWallet.errorDuplicateWalletTitle"), description: t("addWallet.errorDuplicateWalletDescription") }); } await addWallet(dispatch, syncNode, addressPrefix, masterPassword, values, values.password, values.save ?? true); message.success(create ? t("addWallet.messageSuccessCreate") : t("addWallet.messageSuccessAdd")); closeModal(); } } catch (err) { console.error(err); notification.error({ message: t("addWallet.errorUnexpectedTitle"), description: editing ? t("addWallet.errorUnexpectedEditDescription") : t("addWallet.errorUnexpectedDescription") }); } } function onValuesChange(changed: Partial<FormValues>, values: Partial<FormValues>) { if (changed.format) setFormatState(changed.format); if ((changed.format || changed.password || changed.walletUsername) && values.password) updateCalculatedAddress(values.format, values.password, values.walletUsername); } /** Update the 'Wallet address' field */ async function updateCalculatedAddress(format: WalletFormatName | undefined, password: string, username?: string) { const privatekey = await applyWalletFormat(format || "kristwallet", password, username); const address = await makeV2Address(addressPrefix, privatekey); setCalculatedAddress(address); } function generateNewPassword() { if (!create || !form) return; const password = generatePassword(); form.setFieldsValue({ password }); updateCalculatedAddress("kristwallet", password); } useEffect(() => { if (visible && form && !form.getFieldValue("password")) { // Generate a password when the 'Create wallet' modal is opened if (create) generateNewPassword(); // Populate the password when the 'Edit wallet' modal is opened if (editing && masterPassword) { (async () => { const dec = await decryptWallet(masterPassword, editing); if (!dec) return notification.error({ message: t("addWallet.errorDecryptTitle"), description: t("addWallet.errorDecryptDescription") }); const password = dec.password; form.setFieldsValue({ password }); updateCalculatedAddress(form.getFieldValue("format"), password); })(); } } }, [visible, form, create, editing]); return <Modal visible={visible} title={t(editing ? "addWallet.dialogTitleEdit" : (create ? "addWallet.dialogTitleCreate" : "addWallet.dialogTitle"))} footer={[ /* Add existing wallet button */ create && bps.sm && ( <Button key="addExisting" onClick={addExistingWallet} style={{ float: "left" }}> {t("addWallet.dialogAddExisting")} </Button> ), /* Cancel button */ <Button key="cancel" onClick={closeModal}> {t("dialog.cancel")} </Button>, /* OK button */ <Button key="ok" type="primary" onClick={onSubmit}> {t(editing ? "addWallet.dialogOkEdit" : (create ? "addWallet.dialogOkCreate" : "addWallet.dialogOkAdd"))} </Button>, ]} onCancel={closeModal} destroyOnClose > <Form form={form} layout="vertical" name={editing ? "editWalletForm" : (create ? "createWalletForm" : "addWalletForm")} initialValues={{ label: editing?.label ?? undefined, category: editing?.category ?? "", username: editing?.username ?? undefined, format: editing?.format ?? initialFormat, save: true }} onValuesChange={onValuesChange} > <Row gutter={[24, 0]}> {/* Wallet label */} <Col span={12}> <Form.Item name="label" label={t("addWallet.walletLabel")} rules={[ { max: 32, message: t("addWallet.walletLabelMaxLengthError") }, { whitespace: true, message: t("addWallet.walletLabelWhitespaceError") } ]} > <Input placeholder={t("addWallet.walletLabelPlaceholder")} /> </Form.Item> </Col> {/* Wallet category */} <Col span={12}> <Form.Item name="category" label={t("addWallet.walletCategory")}> {getSelectWalletCategory({ onNewCategory: category => form.setFieldsValue({ category })})} </Form.Item> </Col> </Row> {/* Fake username input to trick browser autofill */} <FakeUsernameInput /> {/* Wallet username, if applicable */} {formatState && formatNeedsUsername(formatState) && ( <Form.Item name="walletUsername" label={t("addWallet.walletUsername")}> <Input type="text" autoComplete="off" placeholder={t("addWallet.walletUsernamePlaceholder")}/> </Form.Item> )} {/* Wallet password */} <Form.Item label={formatState === "api" ? t("addWallet.walletPrivatekey") : t("addWallet.walletPassword")} style={{ marginBottom: 0 }} > <Input.Group compact style={{ display: "flex" }}> <Form.Item name="password" style={{ flex: 1, marginBottom: 0 }} rules={[ { required: true, message: formatState === "api" ? t("addWallet.errorPrivatekeyRequired") : t("addWallet.errorPasswordRequired") } ]} > <Input ref={passwordInput} type={create ? "text" : "password"} readOnly={!!create} autoComplete="off" className={create ? "input-monospace" : ""} style={{ height: 32 }} placeholder={formatState === "api" ? t("addWallet.walletPrivatekeyPlaceholder") : t("addWallet.walletPasswordPlaceholder")} /> </Form.Item> {create && <> <CopyInputButton targetInput={passwordInput} /> <Tooltip title={t("addWallet.walletPasswordRegenerate")}> <Button icon={<ReloadOutlined />} onClick={generateNewPassword} /> </Tooltip> </>} </Input.Group> </Form.Item> {/* Password save warning */} {create && <Text className="text-small" type="danger"><Trans t={t} i18nKey="addWallet.walletPasswordWarning"> Make sure to save this somewhere <b>secure</b>! </Trans></Text>} {/* Calculated address */} <Form.Item label={t("addWallet.walletAddress")} style={{ marginTop: 24, marginBottom: 0 }}> <Input type="text" readOnly value={calculatedAddress} /> </Form.Item> {/* Advanced options */} {!create && <Collapse ghost className="flush-collapse" style={{ marginTop: 24 }}> <Collapse.Panel header={t("addWallet.advancedOptions")} key="1"> {/* Wallet format */} <Form.Item name="format" label={t("addWallet.walletFormat")}> {getSelectWalletFormat({ initialFormat })} </Form.Item> {/* Save in KristWeb checkbox */} {!editing && <Form.Item name="save" valuePropName="checked" style={{ marginBottom: 0 }}> <Checkbox>{t("addWallet.walletSave")}</Checkbox> </Form.Item>} </Collapse.Panel> </Collapse>} </Form> </Modal>; }