commit 57ab7532fc2d8320f69331563dd4a2e6568ebf72
parent 9207a48350d404537748a8f2b4c33619effc3b89
Author: Sebastian <sebasjm@gmail.com>
Date: Wed, 1 Oct 2025 18:49:44 -0300
fix #9758
Diffstat:
5 files changed, 138 insertions(+), 55 deletions(-)
diff --git a/packages/taler-util/src/operation.ts b/packages/taler-util/src/operation.ts
@@ -263,6 +263,13 @@ export function succeedOrThrow<R>(resp: OperationResult<R, unknown>): R {
}
throw TalerError.fromException(resp);
}
+export function succeedOrValue<R,V>(resp: OperationResult<R, unknown>, v:V): R | V {
+ if (isOperationOk(resp)) {
+ return resp.body;
+ }
+
+ return v;
+}
/**
* The operation is expected to fail with a body.
diff --git a/packages/taler-util/src/taleruri.ts b/packages/taler-util/src/taleruri.ts
@@ -511,7 +511,7 @@ export namespace TalerUris {
return opKnownFailureWithBody(
TalerUriParseError.INVALID_TARGET_PATH,
{
- pos: cs.length - 1,
+ pos: 1 as const,
uriType,
},
);
@@ -706,7 +706,7 @@ export namespace TalerUris {
return opKnownFailureWithBody(
TalerUriParseError.INVALID_TARGET_PATH,
{
- pos: cs.length - 1,
+ pos: 1 as const,
uriType,
},
);
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -177,7 +177,7 @@ export const Pages = {
scope: CrockEncodedString;
amount?: string;
}>("/cta/scope-withdraw/:scope/:amount?"),
- ctaWithdrawTransferResult: pageDefinition("/cta/transfer-result"),
+ ctaWithdrawTransferResult: "/cta/transfer-result",
ctaWithdrawManual: pageDefinition<{amount?: string;}>("/cta/manual-withdraw/:amount?"),
paytoQrs: pageDefinition<{ payto: CrockEncodedString }>("/payto/qrs/:payto?"),
paytoBanks: pageDefinition<{ payto: CrockEncodedString }>(
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx
@@ -26,4 +26,6 @@ export default {
title: "qr reader",
};
-export const Reading = tests.createExample(QrReaderPage, {});
+export const Reading = tests.createExample(QrReaderPage, {
+
+});
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -17,14 +17,18 @@
import {
assertUnreachable,
HttpStatusCode,
- parseTalerUri,
+ OperationOk,
+ succeedOrThrow,
+ succeedOrValue,
TalerCoreBankHttpClient,
TalerError,
TalerExchangeHttpClient,
TalerMerchantInstanceHttpClient,
TalerUri,
TalerUriAction,
- TranslatedString,
+ TalerUriParseError,
+ TalerUris,
+ TranslatedString
} from "@gnu-taler/taler-util";
import {
BrowserFetchHttpLib,
@@ -221,6 +225,7 @@ function debounce(func: typeof testValidUri, wait = 500): typeof testValidUri {
function debounced(...args: Args): Promise<Ret> {
clearTimeout(timeout);
+ // FIXME: this should race
return new Promise<Ret>((res, rej) => {
timeout = setTimeout(() => {
func(...args)
@@ -237,6 +242,96 @@ function debounce(func: typeof testValidUri, wait = 500): typeof testValidUri {
const testValidUriDebounced = debounce(testValidUri);
+type FailCasesOf<T extends (...args: any) => any> = Exclude<
+ ReturnType<T>,
+ OperationOk<any>
+>;
+
+function translateTalerUriError(
+ result: FailCasesOf<typeof TalerUris.fromString>,
+ i18n: InternationalizationAPI,
+): TranslatedString {
+ switch (result.case) {
+ case TalerUriParseError.WRONG_PREFIX:
+ return i18n.str`URI is not valid. Taler URI should start with "taler://"`;
+ case TalerUriParseError.INCOMPLETE:
+ return i18n.str`After the URI type it should follow a '/' with more information.`;
+ case TalerUriParseError.UNSUPPORTED:
+ return i18n.str`This URI type is not supported`;
+ case TalerUriParseError.COMPONENTS_LENGTH: {
+ switch (result.body.uriType) {
+ case TalerUriAction.Withdraw:
+ return i18n.str`This URI requires the bank host and operation id separated by /`;
+ case TalerUriAction.Pay:
+ return i18n.str`This URI requires the merchant host, order id and session id separated by /`;
+ case TalerUriAction.Refund:
+ return i18n.str`This URI requires the merchant host and order id separated by /`;
+ case TalerUriAction.PayPush:
+ return i18n.str`This URI requires the exchange host and key id separated by /`;
+ case TalerUriAction.PayPull:
+ return i18n.str`This URI requires the exchange host and key id separated by /`;
+ case TalerUriAction.PayTemplate:
+ return i18n.str`This URI requires the merchant host and template id separated by /`;
+ case TalerUriAction.Restore:
+ return i18n.str`This URI requires the host key and provider list separated by /`;
+ case TalerUriAction.DevExperiment:
+ return i18n.str`This URI requires the experiment id`;
+ case TalerUriAction.AddExchange:
+ return i18n.str`This URI requires the exchange host`;
+ case TalerUriAction.WithdrawExchange:
+ return i18n.str`This URI requires the exchange host`;
+ case TalerUriAction.WithdrawalTransferResult:
+ return i18n.str`This URI requires string after the first /`;
+ }
+ }
+ case TalerUriParseError.INVALID_TARGET_PATH: {
+ switch (result.body.uriType) {
+ case TalerUriAction.Withdraw:
+ return i18n.str`The bank host is invalid`;
+ case TalerUriAction.Pay:
+ return i18n.str`The merchant host is invalid`;
+ case TalerUriAction.Refund: {
+ switch (result.body.pos) {
+ case 0:
+ return i18n.str`The merchant host is invalid`;
+ case 1:
+ return i18n.str`The URI should end with /`;
+ }
+ }
+ case TalerUriAction.PayPush:
+ return i18n.str`The exchange host is invalid`;
+ case TalerUriAction.PayPull:
+ return i18n.str`The exchange host is invalid`;
+ case TalerUriAction.PayTemplate:
+ return i18n.str`The merchant host is invalid`;
+ case TalerUriAction.AddExchange:
+ return i18n.str`The exchange host is invalid`;
+ case TalerUriAction.WithdrawExchange:
+ switch (result.body.pos) {
+ case 0:
+ return i18n.str`The exchange host is invalid`;
+ case 1:
+ return i18n.str`The URI should end with /`;
+ }
+ }
+ }
+ case TalerUriParseError.INVALID_PARAMETER: {
+ switch (result.body.uriType) {
+ case TalerUriAction.WithdrawExchange:
+ return i18n.str`The amount is invalid`;
+ case TalerUriAction.Withdraw:
+ case TalerUriAction.Pay:
+ case TalerUriAction.Refund:
+ case TalerUriAction.PayPush:
+ case TalerUriAction.PayPull:
+ case TalerUriAction.PayTemplate:
+ case TalerUriAction.AddExchange:
+ return i18n.str`A parameter is invalid`;
+ }
+ }
+ }
+}
+
export function QrReaderPage({ onDetected }: Props): VNode {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -248,27 +343,15 @@ export function QrReaderPage({ onDetected }: Props): VNode {
async function onChangeDetect(str: string) {
if (str) {
- const uri = parseTalerUri(str.toLowerCase());
- if (!uri) {
- const lstr = str.toLowerCase();
- if (lstr.startsWith("taler://")) {
- if (lstr.length > 8) {
- const withoutTaler = lstr.substring(8);
- const idx = withoutTaler.indexOf("/");
- const action =
- idx === -1 ? withoutTaler : withoutTaler.substring(0, idx);
- setError(
- i18n.str`URI is not valid. Unsupported Taler-action "${action}"`,
- );
- }
- } else {
- setError(
- i18n.str`URI is not valid. Taler URI should start with "taler://"`,
- );
- }
+ const lstr = str.toLowerCase();
+ const uriResp = TalerUris.fromString(lstr);
+
+ if (uriResp.type === "fail") {
+ setError(translateTalerUriError(uriResp, i18n));
setValue(str);
return;
}
+ const { body: uri } = uriResp;
setError(i18n.str`checking...`);
const errorMsg = await testValidUriDebounced(uri, i18n);
if (errorMsg) {
@@ -287,36 +370,25 @@ export function QrReaderPage({ onDetected }: Props): VNode {
function onChange(str: string) {
if (str) {
- const uri = parseTalerUri(str);
- if (!uri) {
- const lstr = str.toLowerCase();
- if (lstr.startsWith("taler://")) {
- if (lstr.length > 8) {
- const withoutTaler = lstr.substring(8);
- const idx = withoutTaler.indexOf("/");
- const action =
- idx === -1 ? withoutTaler : withoutTaler.substring(0, idx);
- setError(
- i18n.str`URI is not valid. Unsupported Taler-action "${action}"`,
- );
- }
- } else {
- setError(
- i18n.str`URI is not valid. Taler URI should start with "taler://"`,
- );
- }
- setValue(str);
- } else {
- setError(i18n.str`checking...`);
+ const lstr = str.toLowerCase();
+ const uriResp = TalerUris.fromString(lstr);
+
+ if (uriResp.type === "fail") {
+ setError(translateTalerUriError(uriResp, i18n));
setValue(str);
- testValidUriDebounced(uri, i18n).then((errorMsg) => {
- if (errorMsg) {
- setError(errorMsg);
- return;
- }
- setError(undefined);
- });
+ return;
}
+ const { body: uri } = uriResp;
+
+ setError(i18n.str`checking...`);
+ setValue(str);
+ testValidUriDebounced(uri, i18n).then((errorMsg) => {
+ if (errorMsg) {
+ setError(errorMsg);
+ return;
+ }
+ setError(undefined);
+ });
} else {
setError(undefined);
setValue(str);
@@ -370,7 +442,7 @@ export function QrReaderPage({ onDetected }: Props): VNode {
setError(i18n.str`Unexpected error happen reading the file: ${error}`);
}
}
- const uri = parseTalerUri(value.toLowerCase());
+ const uri = succeedOrValue(TalerUris.fromString(value.toLowerCase()), undefined);
return (
<Container>
@@ -437,7 +509,9 @@ export function QrReaderPage({ onDetected }: Props): VNode {
case TalerUriAction.AddExchange:
return <i18n.Translate>Add exchange</i18n.Translate>;
case TalerUriAction.WithdrawalTransferResult:
- return <i18n.Translate>Notify transaction</i18n.Translate>;
+ return (
+ <i18n.Translate>Notify transaction</i18n.Translate>
+ );
default: {
assertUnreachable(talerUri);
}
@@ -485,7 +559,7 @@ async function testValidUri(
): Promise<TranslatedString | undefined> {
switch (uri.type) {
case TalerUriAction.Restore:
- case TalerUriAction.DevExperiment:
+ case TalerUriAction.DevExperiment:
case TalerUriAction.WithdrawalTransferResult: {
return undefined;
}