From 867d2ca76b2ca8903b2263a68243899749de7011 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 10 Mar 2023 01:27:31 -0300 Subject: fix encoded uri, add pay template cta --- .../src/NavigationBar.tsx | 2 +- .../src/cta/Payment/index.ts | 2 +- .../src/cta/Payment/test.ts | 2 +- .../src/cta/PaymentTemplate/index.ts | 37 ++++- .../src/cta/PaymentTemplate/state.ts | 128 ++++++++++++++++-- .../src/cta/PaymentTemplate/stories.tsx | 2 +- .../src/cta/PaymentTemplate/test.ts | 6 +- .../src/cta/PaymentTemplate/views.tsx | 57 +++++++- .../taler-wallet-webextension/src/mui/handlers.ts | 5 + .../src/platform/chrome.ts | 36 ++++- .../src/popup/TalerActionFound.tsx | 149 +++++++++++---------- .../src/wallet/Application.tsx | 33 +++-- .../taler-wallet-webextension/src/wxBackend.ts | 25 ++-- 13 files changed, 361 insertions(+), 123 deletions(-) (limited to 'packages/taler-wallet-webextension') diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx index fb6f280c3..e8ee4f475 100644 --- a/packages/taler-wallet-webextension/src/NavigationBar.tsx +++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx @@ -117,7 +117,7 @@ export const Pages = { cta: pageDefinition<{ action: string }>("/cta/:action"), ctaPay: "/cta/pay", - ctaPayTemplate: "/cta/payTemplate", + ctaPayTemplate: "/cta/pay/template", ctaRecovery: "/cta/recovery", ctaRefund: "/cta/refund", ctaTips: "/cta/tip", diff --git a/packages/taler-wallet-webextension/src/cta/Payment/index.ts b/packages/taler-wallet-webextension/src/cta/Payment/index.ts index e844c1706..c9bead89c 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/index.ts +++ b/packages/taler-wallet-webextension/src/cta/Payment/index.ts @@ -30,7 +30,7 @@ import { useComponentState } from "./state.js"; import { BaseView } from "./views.js"; export interface Props { - talerPayUri?: string; + talerPayUri: string; goToWalletManualWithdraw: (amount?: string) => Promise; cancel: () => Promise; onSuccess: (tx: string) => Promise; diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts index e92eb78c0..f4b63955d 100644 --- a/packages/taler-wallet-webextension/src/cta/Payment/test.ts +++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts @@ -41,7 +41,7 @@ describe("Payment CTA states", () => { it("should tell the user that the URI is missing", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { - talerPayUri: undefined, + talerPayUri: "", cancel: nullFunction, goToWalletManualWithdraw: nullFunction, onSuccess: nullFunction, diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts index 2cdc8d2e1..f5a8c8814 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/index.ts @@ -20,12 +20,25 @@ import { ErrorAlert } from "../../context/alert.js"; import { compose, StateViewMap } from "../../utils/index.js"; import { useComponentState } from "./state.js"; import { ReadyView } from "./views.js"; +import { PaymentPage } from "../Payment/index.js"; +import { + AmountFieldHandler, + ButtonHandler, + TextFieldHandler, +} from "../../mui/handlers.js"; export interface Props { - talerTemplateUri?: string; + talerTemplateUri: string; + goToWalletManualWithdraw: (amount?: string) => Promise; + cancel: () => Promise; + onSuccess: (tx: string) => Promise; } -export type State = State.Loading | State.LoadingUriError | State.Ready; +export type State = + | State.Loading + | State.LoadingUriError + | State.OrderReady + | State.FillTemplate; export namespace State { export interface Loading { @@ -37,16 +50,30 @@ export namespace State { error: ErrorAlert; } - export interface Ready { - status: "ready"; + export interface FillTemplate { + status: "fill-template"; error: undefined; + currency: string; + amount?: AmountFieldHandler; + summary?: TextFieldHandler; + onCreate: ButtonHandler; + } + + export interface OrderReady { + status: "order-ready"; + error: undefined; + talerPayUri: string; + onSuccess: (tx: string) => Promise; + cancel: () => Promise; + goToWalletManualWithdraw: () => Promise; } } const viewMapping: StateViewMap = { loading: Loading, error: ErrorAlertView, - ready: ReadyView, + "fill-template": ReadyView, + "order-ready": PaymentPage, }; export const PaymentTemplatePage = compose( diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts index f5e6dee61..abcf040b7 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/state.ts @@ -14,27 +14,56 @@ GNU Taler; see the file COPYING. If not, see */ +import { Amounts } from "@gnu-taler/taler-util"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; -import { alertFromError } from "../../context/alert.js"; +import { useState } from "preact/hooks"; +import { alertFromError, useAlertContext } from "../../context/alert.js"; import { useBackendContext } from "../../context/backend.js"; import { useTranslationContext } from "../../context/translation.js"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; +import { AmountFieldHandler, TextFieldHandler } from "../../mui/handlers.js"; import { Props, State } from "./index.js"; -export function useComponentState({ talerTemplateUri }: Props): State { - // const { pushAlertOnError } = useAlertContext(); +export function useComponentState({ + talerTemplateUri, + cancel, + goToWalletManualWithdraw, + onSuccess, +}: Props): State { const api = useBackendContext(); const { i18n } = useTranslationContext(); + const { safely } = useAlertContext(); + + const url = talerTemplateUri ? new URL(talerTemplateUri) : undefined; + + const amountParam = !url + ? undefined + : url.searchParams.get("amount") ?? undefined; + const summaryParam = !url + ? undefined + : url.searchParams.get("summary") ?? undefined; + + const parsedAmount = !amountParam ? undefined : Amounts.parse(amountParam); + const currency = parsedAmount ? parsedAmount.currency : amountParam; + + const initialAmount = + parsedAmount ?? (currency ? Amounts.zeroOfCurrency(currency) : undefined); + const [amount, setAmount] = useState(initialAmount); + const [summary, setSummary] = useState(summaryParam); + const [newOrder, setNewOrder] = useState(""); const hook = useAsyncAsHook(async () => { if (!talerTemplateUri) throw Error("ERROR_NO-URI-FOR-PAYMENT-TEMPLATE"); - const payStatus = await api.wallet.call( - WalletApiOperation.PreparePayForTemplate, - { - talerPayTemplateUri: talerTemplateUri, - templateParams: {}, - }, - ); + let payStatus; + if (!amountParam && !summaryParam) { + payStatus = await api.wallet.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: talerTemplateUri, + templateParams: {}, + }, + ); + } const balance = await api.wallet.call(WalletApiOperation.GetBalances, {}); return { payStatus, balance, uri: talerTemplateUri }; }, []); @@ -56,8 +85,85 @@ export function useComponentState({ talerTemplateUri }: Props): State { }; } + if (hook.response.payStatus) { + return { + status: "order-ready", + error: undefined, + cancel, + goToWalletManualWithdraw, + onSuccess, + talerPayUri: hook.response.payStatus.talerUri!, + }; + } + + if (newOrder) { + return { + status: "order-ready", + error: undefined, + cancel, + goToWalletManualWithdraw, + onSuccess, + talerPayUri: newOrder, + }; + } + + async function createOrder() { + try { + const templateParams: Record = {}; + if (amount) { + templateParams["amount"] = Amounts.stringify(amount); + } + if (summary) { + templateParams["summary"] = summary; + } + const payStatus = await api.wallet.call( + WalletApiOperation.PreparePayForTemplate, + { + talerPayTemplateUri: talerTemplateUri, + templateParams, + }, + ); + setNewOrder(payStatus.talerUri!); + } catch (e) {} + } + const errors = undefinedIfEmpty({ + amount: amount && Amounts.isZero(amount) ? i18n.str`required` : undefined, + summary: !summary ? i18n.str`required` : undefined, + }); return { - status: "ready", + status: "fill-template", error: undefined, + currency: currency!, //currency is always not null + amount: + amount !== undefined + ? ({ + onInput: (a) => { + setAmount(a); + }, + value: amount, + error: errors?.amount, + } as AmountFieldHandler) + : undefined, + summary: + summary !== undefined + ? ({ + onInput: (t) => { + setSummary(t); + }, + value: summary, + error: errors?.summary, + } as TextFieldHandler) + : undefined, + onCreate: { + onClick: errors + ? undefined + : safely(createOrder, i18n.str`Could not create order`), + }, }; } + +function undefinedIfEmpty(obj: T): T | undefined { + return Object.keys(obj).some((k) => (obj as any)[k] !== undefined) + ? obj + : undefined; +} diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx index 32a080959..93421eaa3 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/stories.tsx @@ -29,6 +29,6 @@ export default { }; export const PaymentPossible = tests.createExample(ReadyView, { - status: "ready", + status: "fill-template", error: undefined, }); diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts index d4c65e008..72fbb6853 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/test.ts @@ -21,6 +21,7 @@ import { expect } from "chai"; import { tests } from "../../../../web-util/src/index.browser.js"; +import { nullFunction } from "../../mui/handlers.js"; import { createWalletApiMock } from "../../test-utils.js"; import { useComponentState } from "./state.js"; @@ -28,7 +29,10 @@ describe("Order template CTA states", () => { it("should tell the user that the URI is missing", async () => { const { handler, TestingContext } = createWalletApiMock(); const props = { - talerTemplateUri: undefined, + talerTemplateUri: "", + cancel: nullFunction, + goToWalletManualWithdraw: nullFunction, + onSuccess: nullFunction, }; const hookBehavior = await tests.hookBehaveLikeThis( diff --git a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx index d3f893c7e..9f4c0f28c 100644 --- a/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/PaymentTemplate/views.tsx @@ -15,15 +15,64 @@ */ import { Fragment, h, VNode } from "preact"; +import { AmountField } from "../../components/AmountField.js"; +import { Part } from "../../components/Part.js"; import { useTranslationContext } from "../../context/translation.js"; +import { Button } from "../../mui/Button.js"; +import { TextField } from "../../mui/TextField.js"; import { State } from "./index.js"; -export function ReadyView({ status }: State.Ready): VNode { +export function ReadyView({ + currency, + amount, + summary, + onCreate, +}: State.FillTemplate): VNode { const { i18n } = useTranslationContext(); + console.log("is summary", !!summary); return ( -
- Not yet implemented -
+ +
+ {/* + Merchant + + } + text={} + kind="neutral" + big + /> */} + {!amount ? undefined : ( +

+ +

+ )} + {!summary ? undefined : ( +

+ +

+ )} +
+
+ +
+
); } diff --git a/packages/taler-wallet-webextension/src/mui/handlers.ts b/packages/taler-wallet-webextension/src/mui/handlers.ts index 61786742f..0bc00ca45 100644 --- a/packages/taler-wallet-webextension/src/mui/handlers.ts +++ b/packages/taler-wallet-webextension/src/mui/handlers.ts @@ -56,6 +56,11 @@ export const nullFunction = async function (): Promise { //do nothing } as SafeHandler; +//FIXME: UI button should required SafeHandler but +//useStateComponent should not be required to create SafeHandlers +//so this need to be splitted in two: +// * ButtonHandlerUI => with i18n +// * ButtonHandlerLogic => without i18n export interface ButtonHandler { onClick?: SafeHandler; // error?: TalerError; diff --git a/packages/taler-wallet-webextension/src/platform/chrome.ts b/packages/taler-wallet-webextension/src/platform/chrome.ts index beb65b2d0..4b0bdbfb7 100644 --- a/packages/taler-wallet-webextension/src/platform/chrome.ts +++ b/packages/taler-wallet-webextension/src/platform/chrome.ts @@ -241,41 +241,63 @@ function openWalletURIFromPopup(maybeTalerUri: string): void { : maybeTalerUri; const uriType = classifyTalerUri(talerUri); + encodeURIComponent; let url: string | undefined = undefined; switch (uriType) { case TalerUriType.TalerWithdraw: url = chrome.runtime.getURL( - `static/wallet.html#/cta/withdraw?talerWithdrawUri=${talerUri}`, + `static/wallet.html#/cta/withdraw?talerWithdrawUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerRecovery: url = chrome.runtime.getURL( - `static/wallet.html#/cta/recovery?talerRecoveryUri=${talerUri}`, + `static/wallet.html#/cta/recovery?talerRecoveryUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerPay: url = chrome.runtime.getURL( - `static/wallet.html#/cta/pay?talerPayUri=${talerUri}`, + `static/wallet.html#/cta/pay?talerPayUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerTip: url = chrome.runtime.getURL( - `static/wallet.html#/cta/tip?talerTipUri=${talerUri}`, + `static/wallet.html#/cta/tip?talerTipUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerRefund: url = chrome.runtime.getURL( - `static/wallet.html#/cta/refund?talerRefundUri=${talerUri}`, + `static/wallet.html#/cta/refund?talerRefundUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerPayPull: url = chrome.runtime.getURL( - `static/wallet.html#/cta/invoice/pay?talerPayPullUri=${talerUri}`, + `static/wallet.html#/cta/invoice/pay?talerPayPullUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.TalerPayPush: url = chrome.runtime.getURL( - `static/wallet.html#/cta/transfer/pickup?talerPayPushUri=${talerUri}`, + `static/wallet.html#/cta/transfer/pickup?talerPayPushUri=${encodeURIComponent( + talerUri, + )}`, + ); + break; + case TalerUriType.TalerPayTemplate: + url = chrome.runtime.getURL( + `static/wallet.html#/cta/pay/template?talerPayTemplateUri=${encodeURIComponent( + talerUri, + )}`, ); break; case TalerUriType.Unknown: diff --git a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx index 5c435a9a5..205e42d20 100644 --- a/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx +++ b/packages/taler-wallet-webextension/src/popup/TalerActionFound.tsx @@ -31,6 +31,86 @@ export interface Props { onDismiss: () => Promise; } +function ContentByUriType({ + type, + onConfirm, +}: { + type: TalerUriType; + onConfirm: () => Promise; +}) { + const { i18n } = useTranslationContext(); + switch (type) { + case TalerUriType.TalerWithdraw: + return ( +
+

+ This page has a withdrawal action. +

+ +
+ ); + + case TalerUriType.TalerPayTemplate: + case TalerUriType.TalerPay: + return ( +
+

+ This page has pay action. +

+ +
+ ); + case TalerUriType.TalerTip: + return ( +
+

+ This page has a tip action. +

+ +
+ ); + + case TalerUriType.TalerRefund: + return ( +
+

+ This page has a refund action. +

+ +
+ ); + + case TalerUriType.TalerDevExperiment: + case TalerUriType.TalerTemplate: + case TalerUriType.TalerPayPull: + case TalerUriType.TalerPayPush: + case TalerUriType.TalerRecovery: + case TalerUriType.Unknown: + return ( +
+

+ + This page has a malformed taler uri. + +

+
+ ); + + default: { + const error: never = type; + return null; + } + } +} + export function TalerActionFound({ url, onDismiss }: Props): VNode { const uriType = classifyTalerUri(url); const { i18n } = useTranslationContext(); @@ -43,74 +123,7 @@ export function TalerActionFound({ url, onDismiss }: Props): VNode { <i18n.Translate>Taler Action</i18n.Translate> - {uriType === TalerUriType.TalerPay && ( -
-

- This page has pay action. -

- -
- )} - {uriType === TalerUriType.TalerWithdraw && ( -
-

- - This page has a withdrawal action. - -

- -
- )} - {uriType === TalerUriType.TalerTip && ( -
-

- This page has a tip action. -

- -
- )} - {uriType === TalerUriType.TalerRefund && ( -
-

- This page has a refund action. -

- -
- )} - {uriType === TalerUriType.Unknown && ( -
-

- - This page has a malformed taler uri. - -

-

{url}

-
- )} +