taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 57ab7532fc2d8320f69331563dd4a2e6568ebf72
parent 9207a48350d404537748a8f2b4c33619effc3b89
Author: Sebastian <sebasjm@gmail.com>
Date:   Wed,  1 Oct 2025 18:49:44 -0300

fix #9758

Diffstat:
Mpackages/taler-util/src/operation.ts | 7+++++++
Mpackages/taler-util/src/taleruri.ts | 4++--
Mpackages/taler-wallet-webextension/src/NavigationBar.tsx | 2+-
Mpackages/taler-wallet-webextension/src/wallet/QrReader.stories.tsx | 4+++-
Mpackages/taler-wallet-webextension/src/wallet/QrReader.tsx | 176++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
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; }