diff options
Diffstat (limited to 'packages/taler-wallet-webextension')
9 files changed, 291 insertions, 62 deletions
diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts index 0389a17fb..01dbb6d6d 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts @@ -59,10 +59,10 @@ export namespace State { doSelectExchange: ButtonHandler; create: ButtonHandler; subject: TextFieldHandler; + expiration: TextFieldHandler; toBeReceived: AmountJson; - chosenAmount: AmountJson; + requestAmount: AmountJson; exchangeUrl: string; - invalid: boolean; error: undefined; operationError?: TalerErrorDetail; } diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts index d845e121a..27f05ce03 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts @@ -15,8 +15,9 @@ */ /* eslint-disable react-hooks/rules-of-hooks */ -import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { isFuture, parse } from "date-fns"; import { useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; @@ -49,7 +50,8 @@ export function useComponentState( const exchangeList = hook.response.exchanges; return () => { - const [subject, setSubject] = useState(""); + const [subject, setSubject] = useState<string | undefined>(); + const [timestamp, setTimestamp] = useState<string | undefined>() const [operationError, setOperationError] = useState< TalerErrorDetail | undefined @@ -67,13 +69,59 @@ export function useComponentState( const exchange = selectedExchange.selected; + const hook = useAsyncAsHook(async () => { + const resp = await api.wallet.call(WalletApiOperation.PreparePeerPullPayment, { + amount: amountStr, + exchangeBaseUrl: exchange.exchangeBaseUrl, + }) + return resp + }) + + if (!hook) { + return { + status: "loading", + error: undefined + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook + } + } + + const { amountEffective, amountRaw } = hook.response + const requestAmount = Amounts.parseOrThrow(amountRaw) + const toBeReceived = Amounts.parseOrThrow(amountEffective) + + let purse_expiration: TalerProtocolTimestamp | undefined = undefined + let timestampError: string | undefined = undefined; + + const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) + + if (t !== undefined) { + if (Number.isNaN(t.getTime())) { + timestampError = 'Should have the format "dd/MM/yyyy"' + } else { + if (!isFuture(t)) { + timestampError = 'Should be in the future' + } else { + purse_expiration = { + t_s: t.getTime() / 1000 + } + } + } + } + async function accept(): Promise<void> { + if (!subject || !purse_expiration) return; try { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPullPayment, { - amount: Amounts.stringify(amount), exchangeBaseUrl: exchange.exchangeBaseUrl, partialContractTerms: { + amount: Amounts.stringify(amount), summary: subject, + purse_expiration }, }); @@ -86,25 +134,32 @@ export function useComponentState( throw Error("error trying to accept"); } } + const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration return { status: "ready", subject: { - error: !subject ? "cant be empty" : undefined, - value: subject, + error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, + value: subject ?? "", onInput: async (e) => setSubject(e), }, + expiration: { + error: timestampError, + value: timestamp === undefined ? "" : timestamp, + onInput: async (e) => { + setTimestamp(e) + } + }, doSelectExchange: selectedExchange.doSelect, - invalid: !subject || Amounts.isZero(amount), exchangeUrl: exchange.exchangeBaseUrl, create: { - onClick: accept, + onClick: unableToCreate ? undefined : accept, }, cancel: { onClick: onClose, }, - chosenAmount: amount, - toBeReceived: amount, + requestAmount, + toBeReceived, error: undefined, operationError, }; diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx index 77885b0c1..8d4473d8f 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx @@ -27,11 +27,14 @@ export default { }; export const Ready = createExample(ReadyView, { - chosenAmount: { + requestAmount: { currency: "ARS", value: 1, fraction: 0, }, + expiration: { + value: "2/12/12", + }, cancel: {}, toBeReceived: { currency: "ARS", diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx index 4970f590f..f15482953 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { format } from "date-fns"; import { h, VNode } from "preact"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; @@ -46,18 +47,40 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { } export function ReadyView({ - invalid, exchangeUrl, subject, + expiration, cancel, operationError, create, toBeReceived, - chosenAmount, + requestAmount, doSelectExchange, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + async function oneDayExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"), + ); + } + } + + async function oneWeekExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"), + ); + } + } + async function _20DaysExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"), + ); + } + } return ( <WalletAction> <LogoHeader /> @@ -75,16 +98,6 @@ export function ReadyView({ /> )} <section style={{ textAlign: "left" }}> - <TextField - label="Subject" - variant="filled" - error={subject.error} - required - fullWidth - value={subject.value} - onChange={subject.onInput} - /> - <Part title={ <div @@ -107,6 +120,52 @@ export function ReadyView({ kind="neutral" big /> + <p> + <TextField + label="Subject" + variant="filled" + error={subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + </p> + + <p> + <TextField + label="Expiration" + variant="filled" + error={expiration.error} + required + fullWidth + value={expiration.value} + onChange={expiration.onInput} + /> + <p> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneDayExpiration} + > + 1 day + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneWeekExpiration} + > + 1 week + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={_20DaysExpiration} + > + 20 days + </Button> + </p> + </p> <Part title={<i18n.Translate>Details</i18n.Translate>} @@ -114,19 +173,14 @@ export function ReadyView({ <InvoiceDetails amount={{ effective: toBeReceived, - raw: chosenAmount, + raw: requestAmount, }} /> } /> </section> <section> - <Button - disabled={invalid} - onClick={create.onClick} - variant="contained" - color="success" - > + <Button onClick={create.onClick} variant="contained" color="success"> <i18n.Translate>Create</i18n.Translate> </Button> </section> diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts index 83293438f..8d51ff3e0 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts @@ -48,11 +48,11 @@ export namespace State { } export interface Ready extends BaseInfo { status: "ready"; - invalid: boolean; create: ButtonHandler; toBeReceived: AmountJson; - chosenAmount: AmountJson; + debitAmount: AmountJson; subject: TextFieldHandler; + expiration: TextFieldHandler; error: undefined; operationError?: TalerErrorDetail; } diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts index b229924b2..089f46047 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts @@ -14,9 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { format, isFuture, parse } from "date-fns"; import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { wxApi } from "../../wxApi.js"; import { Props, State } from "./index.js"; @@ -26,17 +28,65 @@ export function useComponentState( ): State { const amount = Amounts.parseOrThrow(amountStr); - const [subject, setSubject] = useState(""); + const [subject, setSubject] = useState<string | undefined>(); + const [timestamp, setTimestamp] = useState<string | undefined>() + const [operationError, setOperationError] = useState< TalerErrorDetail | undefined >(undefined); + + const hook = useAsyncAsHook(async () => { + const resp = await api.wallet.call(WalletApiOperation.PreparePeerPushPayment, { + amount: amountStr + }) + return resp + }) + + if (!hook) { + return { + status: "loading", + error: undefined + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook + } + } + + const { amountEffective, amountRaw } = hook.response + const debitAmount = Amounts.parseOrThrow(amountRaw) + const toBeReceived = Amounts.parseOrThrow(amountEffective) + + let purse_expiration: TalerProtocolTimestamp | undefined = undefined + let timestampError: string | undefined = undefined; + + const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) + + if (t !== undefined) { + if (Number.isNaN(t.getTime())) { + timestampError = 'Should have the format "dd/MM/yyyy"' + } else { + if (!isFuture(t)) { + timestampError = 'Should be in the future' + } else { + purse_expiration = { + t_s: t.getTime() / 1000 + } + } + } + } + async function accept(): Promise<void> { + if (!subject || !purse_expiration) return; try { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, { - amount: Amounts.stringify(amount), partialContractTerms: { summary: subject, + amount: amountStr, + purse_expiration }, }); onSuccess(resp.transactionId); @@ -48,22 +98,31 @@ export function useComponentState( throw Error("error trying to accept"); } } + + const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration + return { status: "ready", - invalid: !subject || Amounts.isZero(amount), cancel: { onClick: onClose, }, subject: { - error: !subject ? "cant be empty" : undefined, - value: subject, + error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, + value: subject ?? "", onInput: async (e) => setSubject(e), }, + expiration: { + error: timestampError, + value: timestamp === undefined ? "" : timestamp, + onInput: async (e) => { + setTimestamp(e) + } + }, create: { - onClick: accept, + onClick: unableToCreate ? undefined : accept, }, - chosenAmount: amount, - toBeReceived: amount, + debitAmount, + toBeReceived, error: undefined, operationError, }; diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx index 2746cc153..de781f008 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx @@ -27,11 +27,14 @@ export default { }; export const Ready = createExample(ReadyView, { - chosenAmount: { + debitAmount: { currency: "ARS", value: 1, fraction: 0, }, + expiration: { + value: "20/1/2022", + }, create: {}, cancel: {}, toBeReceived: { diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx index bca806c5d..7b1c208b9 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { format } from "date-fns"; import { h, VNode } from "preact"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; @@ -40,14 +41,37 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function ReadyView({ subject, + expiration, toBeReceived, - chosenAmount, + debitAmount, create, operationError, cancel, - invalid, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + + async function oneDayExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"), + ); + } + } + + async function oneWeekExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"), + ); + } + } + async function _20DaysExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"), + ); + } + } return ( <WalletAction> <LogoHeader /> @@ -65,34 +89,65 @@ export function ReadyView({ /> )} <section style={{ textAlign: "left" }}> - <TextField - label="Subject" - variant="filled" - error={subject.error} - required - fullWidth - value={subject.value} - onChange={subject.onInput} - /> + <p> + <TextField + label="Subject" + variant="filled" + error={subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + </p> + <p> + <TextField + label="Expiration" + variant="filled" + error={expiration.error} + required + fullWidth + value={expiration.value} + onChange={expiration.onInput} + /> + <p> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneDayExpiration} + > + 1 day + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneWeekExpiration} + > + 1 week + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={_20DaysExpiration} + > + 20 days + </Button> + </p> + </p> <Part title={<i18n.Translate>Details</i18n.Translate>} text={ <TransferDetails amount={{ effective: toBeReceived, - raw: chosenAmount, + raw: debitAmount, }} /> } /> </section> <section> - <Button - disabled={invalid} - onClick={create.onClick} - variant="contained" - color="success" - > + <Button onClick={create.onClick} variant="contained" color="success"> <i18n.Translate>Create</i18n.Translate> </Button> </section> diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index 0aaa5ee97..bca0d6231 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -290,7 +290,7 @@ export function Button({ return ( <ButtonBase - disabled={disabled || running} + disabled={disabled || running || !doClick} class={[ theme.typography.button, theme.shape.roundBorder, |