fix encoded uri, add pay template cta

This commit is contained in:
Sebastian 2023-03-10 01:27:31 -03:00
parent f404878063
commit 867d2ca76b
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
13 changed files with 361 additions and 123 deletions

View File

@ -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",

View File

@ -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<void>;
cancel: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;

View File

@ -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,

View File

@ -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<void>;
cancel: () => Promise<void>;
onSuccess: (tx: string) => Promise<void>;
}
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<void>;
cancel: () => Promise<void>;
goToWalletManualWithdraw: () => Promise<void>;
}
}
const viewMapping: StateViewMap<State> = {
loading: Loading,
error: ErrorAlertView,
ready: ReadyView,
"fill-template": ReadyView,
"order-ready": PaymentPage,
};
export const PaymentTemplatePage = compose(

View File

@ -14,27 +14,56 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
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<string, string> = {};
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<T extends object>(obj: T): T | undefined {
return Object.keys(obj).some((k) => (obj as any)[k] !== undefined)
? obj
: undefined;
}

View File

@ -29,6 +29,6 @@ export default {
};
export const PaymentPossible = tests.createExample(ReadyView, {
status: "ready",
status: "fill-template",
error: undefined,
});

View File

@ -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(

View File

@ -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 (
<div>
<i18n.Translate>Not yet implemented</i18n.Translate>
</div>
<Fragment>
<section style={{ textAlign: "left" }}>
{/* <Part
title={
<div
style={{
display: "flex",
alignItems: "center",
}}
>
<i18n.Translate>Merchant</i18n.Translate>
</div>
}
text={<ExchangeDetails exchange={exchangeUrl} />}
kind="neutral"
big
/> */}
{!amount ? undefined : (
<p>
<AmountField label={i18n.str`Amount`} handler={amount} />
</p>
)}
{!summary ? undefined : (
<p>
<TextField
label="Summary"
variant="filled"
required
fullWidth
error={summary.error}
value={summary.value}
onChange={summary.onInput}
/>
</p>
)}
</section>
<section>
<Button onClick={onCreate.onClick} variant="contained" color="success">
<i18n.Translate>Review order</i18n.Translate>
</Button>
</section>
</Fragment>
);
}

View File

@ -56,6 +56,11 @@ export const nullFunction = async function (): Promise<void> {
//do nothing
} as SafeHandler<void>;
//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<void>;
// error?: TalerError;

View File

@ -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:

View File

@ -31,6 +31,86 @@ export interface Props {
onDismiss: () => Promise<void>;
}
function ContentByUriType({
type,
onConfirm,
}: {
type: TalerUriType;
onConfirm: () => Promise<void>;
}) {
const { i18n } = useTranslationContext();
switch (type) {
case TalerUriType.TalerWithdraw:
return (
<div>
<p>
<i18n.Translate>This page has a withdrawal action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
<i18n.Translate>Open withdraw page</i18n.Translate>
</Button>
</div>
);
case TalerUriType.TalerPayTemplate:
case TalerUriType.TalerPay:
return (
<div>
<p>
<i18n.Translate>This page has pay action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
<i18n.Translate>Open pay page</i18n.Translate>
</Button>
</div>
);
case TalerUriType.TalerTip:
return (
<div>
<p>
<i18n.Translate>This page has a tip action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
<i18n.Translate>Open tip page</i18n.Translate>
</Button>
</div>
);
case TalerUriType.TalerRefund:
return (
<div>
<p>
<i18n.Translate>This page has a refund action.</i18n.Translate>
</p>
<Button variant="contained" color="success" onClick={onConfirm}>
<i18n.Translate>Open refund page</i18n.Translate>
</Button>
</div>
);
case TalerUriType.TalerDevExperiment:
case TalerUriType.TalerTemplate:
case TalerUriType.TalerPayPull:
case TalerUriType.TalerPayPush:
case TalerUriType.TalerRecovery:
case TalerUriType.Unknown:
return (
<div>
<p>
<i18n.Translate>
This page has a malformed taler uri.
</i18n.Translate>
</p>
</div>
);
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 {
<Title>
<i18n.Translate>Taler Action</i18n.Translate>
</Title>
{uriType === TalerUriType.TalerPay && (
<div>
<p>
<i18n.Translate>This page has pay action.</i18n.Translate>
</p>
<Button
variant="contained"
color="success"
onClick={redirectToWallet}
>
<i18n.Translate>Open pay page</i18n.Translate>
</Button>
</div>
)}
{uriType === TalerUriType.TalerWithdraw && (
<div>
<p>
<i18n.Translate>
This page has a withdrawal action.
</i18n.Translate>
</p>
<Button
variant="contained"
color="success"
onClick={redirectToWallet}
>
<i18n.Translate>Open withdraw page</i18n.Translate>
</Button>
</div>
)}
{uriType === TalerUriType.TalerTip && (
<div>
<p>
<i18n.Translate>This page has a tip action.</i18n.Translate>
</p>
<Button
variant="contained"
color="success"
onClick={redirectToWallet}
>
<i18n.Translate>Open tip page</i18n.Translate>
</Button>
</div>
)}
{uriType === TalerUriType.TalerRefund && (
<div>
<p>
<i18n.Translate>This page has a refund action.</i18n.Translate>
</p>
<Button
variant="contained"
color="success"
onClick={redirectToWallet}
>
<i18n.Translate>Open refund page</i18n.Translate>
</Button>
</div>
)}
{uriType === TalerUriType.Unknown && (
<div>
<p>
<i18n.Translate>
This page has a malformed taler uri.
</i18n.Translate>
</p>
<p>{url}</p>
</div>
)}
<ContentByUriType type={uriType} onConfirm={redirectToWallet} />
</section>
<footer>
<div />

View File

@ -289,7 +289,7 @@ export function Application(): VNode {
component={({ talerPayUri }: { talerPayUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentPage
talerPayUri={talerPayUri}
talerPayUri={decodeURIComponent(talerPayUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
@ -302,14 +302,23 @@ export function Application(): VNode {
)}
/>
<Route
path={Pages.ctaPay}
path={Pages.ctaPayTemplate}
component={({
talerTemplateUri,
talerPayTemplateUri,
}: {
talerTemplateUri: string;
talerPayTemplateUri: string;
}) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentTemplatePage talerTemplateUri={talerTemplateUri} />
<PaymentTemplatePage
talerTemplateUri={decodeURIComponent(talerPayTemplateUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
@ -318,7 +327,7 @@ export function Application(): VNode {
component={({ talerRefundUri }: { talerRefundUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash refund`}>
<RefundPage
talerRefundUri={talerRefundUri}
talerRefundUri={decodeURIComponent(talerRefundUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@ -332,7 +341,7 @@ export function Application(): VNode {
component={({ talerTipUri }: { talerTipUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash tip`}>
<TipPage
talerTipUri={talerTipUri}
talerTipUri={decodeURIComponent(talerTipUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@ -350,7 +359,7 @@ export function Application(): VNode {
}) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromURI
talerWithdrawUri={talerWithdrawUri}
talerWithdrawUri={decodeURIComponent(talerWithdrawUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@ -385,7 +394,7 @@ export function Application(): VNode {
<CallToActionTemplate title={i18n.str`Digital cash deposit`}>
<DepositPageCTA
amountStr={amount}
talerDepositUri={talerDepositUri}
talerDepositUri={decodeURIComponent(talerDepositUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@ -427,7 +436,7 @@ export function Application(): VNode {
component={({ talerPayPullUri }: { talerPayPullUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoicePayPage
talerPayPullUri={talerPayPullUri}
talerPayPullUri={decodeURIComponent(talerPayPullUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
@ -444,7 +453,7 @@ export function Application(): VNode {
component={({ talerPayPushUri }: { talerPayPushUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferPickupPage
talerPayPushUri={talerPayPushUri}
talerPayPushUri={decodeURIComponent(talerPayPushUri)}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
@ -462,7 +471,7 @@ export function Application(): VNode {
}) => (
<CallToActionTemplate title={i18n.str`Digital cash recovery`}>
<RecoveryPage
talerRecoveryUri={talerRecoveryUri}
talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={() => redirectTo(Pages.backup)}
/>

View File

@ -337,38 +337,44 @@ function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
case TalerUriType.TalerWithdraw:
return platform.redirectTabToWalletPage(
tabId,
`/cta/withdraw?talerWithdrawUri=${talerUri}`,
`/cta/withdraw?talerWithdrawUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerPay:
return platform.redirectTabToWalletPage(
tabId,
`/cta/pay?talerPayUri=${talerUri}`,
`/cta/pay?talerPayUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerTip:
return platform.redirectTabToWalletPage(
tabId,
`/cta/tip?talerTipUri=${talerUri}`,
`/cta/tip?talerTipUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerRefund:
return platform.redirectTabToWalletPage(
tabId,
`/cta/refund?talerRefundUri=${talerUri}`,
`/cta/refund?talerRefundUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerPayPull:
return platform.redirectTabToWalletPage(
tabId,
`/cta/invoice/pay?talerPayPullUri=${talerUri}`,
`/cta/invoice/pay?talerPayPullUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerPayPush:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/pickup?talerPayPushUri=${talerUri}`,
`/cta/transfer/pickup?talerPayPushUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerRecovery:
return platform.redirectTabToWalletPage(
tabId,
`/cta/transfer/recovery?talerBackupUri=${talerUri}`,
`/cta/transfer/recovery?talerBackupUri=${encodeURIComponent(talerUri)}`,
);
case TalerUriType.TalerPayTemplate:
return platform.redirectTabToWalletPage(
tabId,
`/cta/pay/template?talerPayTemplateUri=${encodeURIComponent(talerUri)}`,
);
return;
case TalerUriType.Unknown:
logger.warn(
`Response with HTTP 402 the Taler header but could not classify ${talerUri}`,
@ -379,10 +385,7 @@ function parseTalerUriAndRedirect(tabId: number, maybeTalerUri: string): void {
logger.warn("not implemented");
return;
case TalerUriType.TalerTemplate:
return platform.redirectTabToWalletPage(
tabId,
`/cta/template?talerTemplateUri=${talerUri}`,
);
logger.warn("not implemented");
return;
default: {
const error: never = uriType;