diff --git a/public/locales/en.json b/public/locales/en.json index 204ee61..0199ef5 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1017,7 +1017,9 @@ "errorNotificationTitle": "Transaction failed", "successNotificationTitle": "Transaction successful", "successNotificationContent": "You sent <1 /> from <3 /> to <5 />.", - "successNotificationButton": "View transaction" + "successNotificationButton": "View transaction", + + "errorInvalidQuery": "The query parameters were invalid, they were ignored." }, "authFailed": { diff --git a/src/pages/transactions/send/QueryParamsHook.tsx b/src/pages/transactions/send/QueryParamsHook.tsx new file mode 100644 index 0000000..99bb708 --- /dev/null +++ b/src/pages/transactions/send/QueryParamsHook.tsx @@ -0,0 +1,62 @@ +// 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 { useMemo } from "react"; +import { message } from "antd"; + +import { useTFns } from "@utils/i18n"; + +import { useLocation } from "react-router-dom"; + +import { + useAddressPrefix, useNameSuffix, isValidAddress, getNameParts +} from "@utils/krist"; + +interface Res { + to?: string; + amount?: number; + metadata?: string; +} + +export function useSendTxQuery(): Res { + const { tStr } = useTFns("sendTransaction."); + const search = useLocation().search; + + const addressPrefix = useAddressPrefix(); + const nameSuffix = useNameSuffix(); + + // Memoise the query parsing, as notifications are triggered directly here. To + // avoid spamming them, this should only run once per query string. + return useMemo(() => { + const query = new URLSearchParams(search); + + // Fetch the form parameters from the query string + const rawTo = query.get("to")?.trim(); + const rawAmount = query.get("amount")?.trim(); + const rawMetadata = query.get("metadata"); + + // Validate the parameters + const toValid = rawTo && (isValidAddress(addressPrefix, rawTo) + || !!getNameParts(nameSuffix, rawTo)); + + const parsedAmount = rawAmount ? parseInt(rawAmount) : undefined; + const amountValid = rawAmount && !isNaN(parsedAmount!) && parsedAmount! > 0; + + const metadataValid = rawMetadata && rawMetadata.length < 255; + + // Show a notification if any parameter is invalid + if ((rawTo && !toValid) + || (rawAmount && !amountValid) + || (rawMetadata && !metadataValid)) { + message.error(tStr("errorInvalidQuery")); + return {}; + } + + // The parameters were valid (or non-existent), return them + return { + to: rawTo || undefined, + amount: parsedAmount, + metadata: rawMetadata || undefined + }; + }, [tStr, search, addressPrefix, nameSuffix]); +} diff --git a/src/pages/transactions/send/SendTransactionForm.tsx b/src/pages/transactions/send/SendTransactionForm.tsx index be8673a..e96dc22 100644 --- a/src/pages/transactions/send/SendTransactionForm.tsx +++ b/src/pages/transactions/send/SendTransactionForm.tsx @@ -18,9 +18,10 @@ import { sha256 } from "@utils/crypto"; import { useBooleanSetting, useIntegerSetting } from "@utils/settings"; -import { APIError, useSyncNode } from "@api"; +import { useSyncNode } from "@api"; import { KristTransaction } from "@api/types"; import { makeTransaction } from "@api/transactions"; +import { handleTransactionError } from "./handleErrors"; import { useAuthFailedModal } from "@api/AuthFailed"; import { AddressPicker } from "@comp/addresses/picker/AddressPicker"; @@ -46,6 +47,8 @@ interface Props { from?: Wallet | string; to?: string; + amount?: number; + metadata?: string; form: FormInstance; triggerSubmit: () => Promise; } @@ -53,6 +56,8 @@ function SendTransactionForm({ from: rawInitialFrom, to: initialTo, + amount: initialAmount, + metadata: initialMetadata, form, triggerSubmit }: Props): JSX.Element { @@ -106,21 +111,21 @@ const initialValues = useMemo(() => ({ from: initialFrom, to: initialTo, - amount: 1, - metadata: "" + amount: initialAmount || 1, + metadata: initialMetadata || "" // eslint-disable-next-line react-hooks/exhaustive-deps }), [ rawInitialFrom, initialFrom, - initialTo + initialTo, + initialAmount, + initialMetadata ]); - // If the to/from change, refresh the form + // If the initial values change, refresh the form useEffect(() => { - if (!form || (!rawInitialFrom && !initialTo)) return; - form.setFieldsValue(initialValues); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [form, rawInitialFrom, to]); + form?.setFieldsValue(initialValues); + }, [form, initialValues]); return
void; onSuccess?: (transaction: KristTransaction) => void; allowClearOnSend?: boolean; @@ -210,6 +217,8 @@ export function useTransactionForm({ from: initialFrom, to: initialTo, + amount: initialAmount, + metadata: initialMetadata, onError, onSuccess, allowClearOnSend @@ -276,33 +285,8 @@ } // Convert API errors to friendlier errors - function handleError(err: Error, from?: Wallet): void { - // Construct a TranslatedError pre-keyed to sendTransaction - const tErr = (key: string) => new TranslatedError("sendTransaction." + key); - - switch (err.message) { - case "missing_parameter": - case "invalid_parameter": - switch ((err as APIError).parameter) { - case "to": - return onError?.(tErr("errorParameterTo")); - case "amount": - return onError?.(tErr("errorParameterAmount")); - case "metadata": - return onError?.(tErr("errorParameterMetadata")); - } - break; - case "insufficient_funds": - return onError?.(tErr("errorInsufficientFunds")); - case "name_not_found": - return onError?.(tErr("errorNameNotFound")); - case "auth_failed": - return showAuthFailed(from!); - } - - // Pass through any other unknown errors - onError?.(err); - } + const handleError = handleTransactionError.bind(handleTransactionError, + onError, showAuthFailed); async function onSubmit() { setIsSubmitting(true); @@ -374,6 +358,8 @@ diff --git a/src/pages/transactions/send/SendTransactionPage.tsx b/src/pages/transactions/send/SendTransactionPage.tsx index 0120bdb..13610ea 100644 --- a/src/pages/transactions/send/SendTransactionPage.tsx +++ b/src/pages/transactions/send/SendTransactionPage.tsx @@ -18,6 +18,7 @@ import { useTransactionForm } from "./SendTransactionForm"; import { NotifSuccessContents, NotifSuccessButton } from "./Success"; +import { useSendTxQuery } from "./QueryParamsHook"; import "./SendTransactionPage.less"; @@ -28,8 +29,12 @@ // The success or error alert const [alert, setAlert] = useState(null); + // Get any pre-filled values from the query parameters + const { to, amount, metadata } = useSendTxQuery(); + // Create the transaction form const { isSubmitting, triggerSubmit, txForm } = useTransactionForm({ + to, amount, metadata, onSuccess: tx => setAlert(), onError: err => setAlert() }); diff --git a/src/pages/transactions/send/handleErrors.ts b/src/pages/transactions/send/handleErrors.ts new file mode 100644 index 0000000..9eb0e54 --- /dev/null +++ b/src/pages/transactions/send/handleErrors.ts @@ -0,0 +1,42 @@ +// 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 { TranslatedError } from "@utils/i18n"; + +import { APIError } from "@api"; +import { ShowAuthFailedFn } from "@api/AuthFailed"; + +import { Wallet } from "@wallets"; + +export function handleTransactionError( + onError: ((error: Error) => void) | undefined, + showAuthFailed: ShowAuthFailedFn, + err: Error, + from?: Wallet +): void { + // Construct a TranslatedError pre-keyed to sendTransaction + const tErr = (key: string) => new TranslatedError("sendTransaction." + key); + + switch (err.message) { + case "missing_parameter": + case "invalid_parameter": + switch ((err as APIError).parameter) { + case "to": + return onError?.(tErr("errorParameterTo")); + case "amount": + return onError?.(tErr("errorParameterAmount")); + case "metadata": + return onError?.(tErr("errorParameterMetadata")); + } + break; + case "insufficient_funds": + return onError?.(tErr("errorInsufficientFunds")); + case "name_not_found": + return onError?.(tErr("errorNameNotFound")); + case "auth_failed": + return showAuthFailed(from!); + } + + // Pass through any other unknown errors + onError?.(err); +}