diff --git a/public/locales/en.json b/public/locales/en.json index 0199ef5..a4215dc 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -1022,6 +1022,19 @@ "errorInvalidQuery": "The query parameters were invalid, they were ignored." }, + "request": { + "title": "Request Krist", + "siteTitle": "Request Krist", + + "labelTo": "Request recipient", + "labelAmount": "Request amount", + "labelMetadata": "Request metadata", + "placeholderMetadata": "Metadata", + + "generatedLink": "Generated link", + "generatedLinkHint": "Send this link to somebody to request a payment from them." + }, + "authFailed": { "title": "Auth failed", "message": "You do not own this address.", diff --git a/src/components/transactions/AmountInput.tsx b/src/components/transactions/AmountInput.tsx index 5f5e3f4..806f1da 100644 --- a/src/components/transactions/AmountInput.tsx +++ b/src/components/transactions/AmountInput.tsx @@ -1,6 +1,7 @@ // 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 { ReactNode } from "react"; import { Form, Input, InputNumber, Button } from "antd"; import { useTranslation } from "react-i18next"; @@ -13,12 +14,22 @@ interface Props { from?: string; setAmount: (amount: number) => void; + + label?: ReactNode; + required?: boolean; + disabled?: boolean; + tabIndex?: number; } export function AmountInput({ from, setAmount, + + label, + required, + disabled, + tabIndex, ...props }: Props): JSX.Element { @@ -36,9 +47,11 @@ setAmount(currentWallet?.balance || 0); } + const amountRequired = required === undefined || !!required; + return @@ -57,13 +70,19 @@ validateFirst rules={[ - { required: true, message: t("sendTransaction.errorAmountRequired") }, - { type: "number", message: t("sendTransaction.errorAmountNumber") }, + { required: amountRequired, + message: t("sendTransaction.errorAmountRequired") }, + { type: "number", + message: t("sendTransaction.errorAmountNumber") }, // Validate that the number isn't higher than the selected wallet's - // balance + // balance, if it is present { async validator(_, value): Promise { + // If the field isn't required, don't complain if it's empty + if (!required && typeof value !== "number") + return; + if (value < 1) throw t("sendTransaction.errorAmountTooLow"); @@ -83,6 +102,7 @@ min={1} style={{ width: "100%", height: 32 }} tabIndex={tabIndex} + disabled={disabled} /> @@ -92,7 +112,7 @@ {/* Max value button */} - {from && } diff --git a/src/global/AppRouter.tsx b/src/global/AppRouter.tsx index c6a840f..ea1bf31 100644 --- a/src/global/AppRouter.tsx +++ b/src/global/AppRouter.tsx @@ -9,6 +9,7 @@ import { ContactsPage } from "@pages/contacts/ContactsPage"; import { SendTransactionPage } from "@pages/transactions/send/SendTransactionPage"; +import { RequestPage } from "@pages/transactions/request/RequestPage"; import { AddressPage } from "@pages/addresses/AddressPage"; import { BlocksPage } from "@pages/blocks/BlocksPage"; @@ -47,6 +48,7 @@ // Payments { path: "/send", name: "sendTransaction", component: }, + { path: "/request", name: "request", component: }, // Network explorer { path: "/network/addresses/:address", name: "address", component: }, diff --git a/src/layout/nav/AppHeader.tsx b/src/layout/nav/AppHeader.tsx index f9f4897..26902d4 100644 --- a/src/layout/nav/AppHeader.tsx +++ b/src/layout/nav/AppHeader.tsx @@ -50,7 +50,7 @@ {/* Request Krist */} - }> + }> {t("nav.request")} diff --git a/src/pages/transactions/request/RequestForm.tsx b/src/pages/transactions/request/RequestForm.tsx new file mode 100644 index 0000000..92e6aca --- /dev/null +++ b/src/pages/transactions/request/RequestForm.tsx @@ -0,0 +1,151 @@ +// 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 { useCallback, useState } from "react"; +import { Form, Input, Checkbox } from "antd"; + +import { useTFns } from "@utils/i18n"; + +import { AddressPicker } from "@comp/addresses/picker/AddressPicker"; +import { AmountInput } from "@comp/transactions/AmountInput"; +import { SmallCopyable } from "@comp/SmallCopyable"; + +import { METADATA_REGEXP } from "../send/SendTransactionForm"; + +interface FormValues { + to: string; + + hasAmount: boolean; + amount?: number; + + hasMetadata: boolean; + metadata?: string; +} + +export function RequestForm(): JSX.Element { + const { t, tStr } = useTFns("request."); + + const [form] = Form.useForm(); + + const [to, setTo] = useState(""); + const [hasAmount, setHasAmount] = useState(false); + const [hasMetadata, setHasMetadata] = useState(false); + + const [generatedLink, setGeneratedLink] = useState(); + + const generateLink = useCallback((values: Partial) => { + const [scheme,, href] = window.location.href.split("/"); + const baseURL = scheme + "//" + href; + + if (!values || !values.to) { + setGeneratedLink(undefined); + return; + } + + const query = new URLSearchParams(); + query.set("to", encodeURIComponent(values.to)); + + // Add the amount if requested + if (values.hasAmount && values.amount) + query.set("amount", encodeURIComponent(values.amount.toString())); + + // Add the metadata if requested + if (values.hasMetadata && values.metadata) + query.set("metadata", encodeURIComponent(values.metadata)); + + setGeneratedLink(baseURL + "/send?" + query.toString()); + }, []); + + async function onValuesChange(_: unknown, values: Partial) { + setTo(values.to || ""); + setHasAmount(!!values.hasAmount); + setHasMetadata(!!values.hasMetadata); + + generateLink(values); + } + + return
+ {/* Request recipient */} + + + {/* Request amount */} + form.setFieldsValue({ amount })} + + label={<> + {/* Has amount checkbox */} + + + + + {tStr("labelAmount")} + } + + disabled={!hasAmount} + required={hasAmount} + + tabIndex={2} + /> + + {/* Request metadata */} + + {/* Has metadata checkbox */} + + + + + {tStr("labelMetadata")} + } + + rules={[ + { max: 255, message: t("sendTransaction.errorMetadataTooLong") }, + { pattern: METADATA_REGEXP, + message: t("sendTransaction.errorMetadataInvalid") }, + ]} + > + + + + {/* Generated link */} + + {tStr("generatedLink")} + + {/* Show a copy button if there's a link */} + {generatedLink && } + } + + style={{ marginBottom: 0 }} + > + + + ; +} diff --git a/src/pages/transactions/request/RequestPage.less b/src/pages/transactions/request/RequestPage.less new file mode 100644 index 0000000..4aa7f50 --- /dev/null +++ b/src/pages/transactions/request/RequestPage.less @@ -0,0 +1,24 @@ +// 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 (reference) "../../../App.less"; + +.request-page { + .request-container { + flex-direction: column; + + margin: 0 auto; + width: 100%; + max-width: 768px; + + background: @kw-light; + border-radius: @kw-big-card-border-radius; + + padding: @padding-lg; + + .cb { + margin-bottom: 0; + margin-right: @margin-sm; + } + } +} diff --git a/src/pages/transactions/request/RequestPage.tsx b/src/pages/transactions/request/RequestPage.tsx new file mode 100644 index 0000000..c5481e2 --- /dev/null +++ b/src/pages/transactions/request/RequestPage.tsx @@ -0,0 +1,24 @@ +// 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 { useTFns } from "@utils/i18n"; + +import { PageLayout } from "@layout/PageLayout"; + +import { RequestForm } from "./RequestForm"; + +import "./RequestPage.less"; + +export function RequestPage(): JSX.Element { + const { tKey } = useTFns("request."); + + return +
+ +
+
; +} diff --git a/src/pages/transactions/request/RequestTransactionForm.tsx b/src/pages/transactions/request/RequestTransactionForm.tsx deleted file mode 100644 index 380923c..0000000 --- a/src/pages/transactions/request/RequestTransactionForm.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// 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 { Form, Input } from "antd"; - -interface FormValues { - to: string; - - hasAmount: boolean; - amount?: number; - - hasMetadata: boolean; - metadata?: string; -} - -export function RequestTransactionForm(): JSX.Element { - return ; -} diff --git a/src/pages/transactions/send/SendTransactionForm.tsx b/src/pages/transactions/send/SendTransactionForm.tsx index 1436b01..7fe5be6 100644 --- a/src/pages/transactions/send/SendTransactionForm.tsx +++ b/src/pages/transactions/send/SendTransactionForm.tsx @@ -25,7 +25,7 @@ import { useAuthFailedModal } from "@api/AuthFailed"; import { AddressPicker } from "@comp/addresses/picker/AddressPicker"; -import { AmountInput } from "../../../components/transactions/AmountInput"; +import { AmountInput } from "@comp/transactions/AmountInput"; import { SendTransactionConfirmModalContents } from "./SendTransactionConfirmModal"; import awaitTo from "await-to-js"; @@ -35,7 +35,7 @@ // This is from https://github.com/tmpim/Krist/blob/a924f3f/src/controllers/transactions.js#L102 // except `+` is changed to `*`. -const METADATA_REGEXP = /^[\x20-\x7F\n]*$/i; +export const METADATA_REGEXP = /^[\x20-\x7F\n]*$/i; export interface FormValues { from: string; diff --git a/src/pages/transactions/send/SendTransactionPage.tsx b/src/pages/transactions/send/SendTransactionPage.tsx index 13610ea..7ce175f 100644 --- a/src/pages/transactions/send/SendTransactionPage.tsx +++ b/src/pages/transactions/send/SendTransactionPage.tsx @@ -23,7 +23,6 @@ import "./SendTransactionPage.less"; export function SendTransactionPage(): JSX.Element { - // TODO: use this page for pre-filled transaction links? const { t } = useTranslation(); // The success or error alert diff --git a/src/style/theme.less b/src/style/theme.less index fd826d3..deb1573 100644 --- a/src/style/theme.less +++ b/src/style/theme.less @@ -85,6 +85,7 @@ @input-addon-bg: @kw-slighter; +@input-disabled-bg: @kw-slighter; @kw-input-readonly-bg: @kw-slighter; @btn-default-color: @text-color;