[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[taler-typescript-core] branch master updated: fix #9535
From: |
Admin |
Subject: |
[taler-typescript-core] branch master updated: fix #9535 |
Date: |
Fri, 14 Feb 2025 20:51:22 +0100 |
This is an automated email from the git hooks/post-receive script.
sebasjm pushed a commit to branch master
in repository taler-typescript-core.
The following commit(s) were added to refs/heads/master by this push:
new 2bc77f355 fix #9535
2bc77f355 is described below
commit 2bc77f355f74bd98c6ba748c5db1822a243aaa36
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Fri Feb 14 16:51:05 2025 -0300
fix #9535
---
.../taler-wallet-webextension/src/utils/index.ts | 7 +-
.../src/wallet/QrReader.tsx | 360 +++++++++++++++++----
2 files changed, 309 insertions(+), 58 deletions(-)
diff --git a/packages/taler-wallet-webextension/src/utils/index.ts
b/packages/taler-wallet-webextension/src/utils/index.ts
index d83e6f472..5905b90ab 100644
--- a/packages/taler-wallet-webextension/src/utils/index.ts
+++ b/packages/taler-wallet-webextension/src/utils/index.ts
@@ -15,7 +15,7 @@
*/
import { createElement, VNode } from "preact";
-import { useCallback, useMemo } from "preact/hooks";
+import { useMemo } from "preact/hooks";
function getJsonIfOk(r: Response): Promise<any> {
if (r.ok) {
@@ -27,7 +27,8 @@ function getJsonIfOk(r: Response): Promise<any> {
}
throw new Error(
- `Try another server: (${r.status}) ${r.statusText || "internal server
error"
+ `Try another server: (${r.status}) ${
+ r.statusText || "internal server error"
}`,
);
}
@@ -104,7 +105,7 @@ export function compose<SType extends { status: string },
PType>(
// TheComponent.name = `${name}`;
return useMemo(() => {
- return TheComponent
+ return TheComponent;
}, [stateHook]);
}
diff --git a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
index 9635cd077..42abc5c69 100644
--- a/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
+++ b/packages/taler-wallet-webextension/src/wallet/QrReader.tsx
@@ -16,12 +16,21 @@
import {
assertUnreachable,
+ HttpStatusCode,
parseTalerUri,
+ TalerCoreBankHttpClient,
+ TalerError,
+ TalerExchangeHttpClient,
+ TalerMerchantInstanceHttpClient,
TalerUri,
TalerUriAction,
TranslatedString,
} from "@gnu-taler/taler-util";
-import { useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ BrowserFetchHttpLib,
+ InternationalizationAPI,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { css } from "@linaria/core";
import { styled } from "@linaria/react";
import jsQR, * as pr from "jsqr";
@@ -205,6 +214,29 @@ async function waitUntilReady(video: HTMLVideoElement):
Promise<void> {
});
}
+function debounce(func: typeof testValidUri, wait = 500): typeof testValidUri {
+ let timeout: ReturnType<typeof setTimeout>;
+ type Args = Parameters<typeof testValidUri>;
+ type Ret = ReturnType<typeof testValidUri>;
+
+ function debounced(...args: Args): Promise<Ret> {
+ clearTimeout(timeout);
+ return new Promise<Ret>((res, rej) => {
+ timeout = setTimeout(() => {
+ func(...args)
+ .then((msg) => {
+ res(Promise.resolve(msg));
+ })
+ .catch(rej);
+ }, wait);
+ });
+ }
+
+ return debounced as any;
+}
+
+const testValidUriDebounced = debounce(testValidUri);
+
export function QrReaderPage({ onDetected }: Props): VNode {
const videoRef = useRef<HTMLVideoElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
@@ -214,36 +246,55 @@ export function QrReaderPage({ onDetected }: Props):
VNode {
const { i18n } = useTranslationContext();
- function onChangeDetect(str: string) {
+ async function onChangeDetect(str: string) {
if (str) {
const uri = parseTalerUri(str.toLowerCase());
if (!uri) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
);
- } else {
- onDetected(uri);
- setError(undefined);
+ setValue(str);
+ return;
}
+ setError(i18n.str`checking...`);
+ const errorMsg = await testValidUriDebounced(uri, i18n);
+ if (errorMsg) {
+ setError(errorMsg);
+ setValue(str);
+ return;
+ }
+ onDetected(uri);
+ setError(undefined);
+ setValue(str);
} else {
setError(undefined);
+ setValue(str);
}
- setValue(str);
}
function onChange(str: string) {
if (str) {
- if (!parseTalerUri(str.toLowerCase())) {
+ const uri = parseTalerUri(str.toLowerCase());
+ if (!uri) {
setError(
i18n.str`URI is not valid. Taler URI should start with "taler://"`,
);
+ setValue(str);
} else {
- setError(undefined);
+ setError(i18n.str`checking...`);
+ setValue(str);
+ testValidUriDebounced(uri, i18n).then((errorMsg) => {
+ if (errorMsg) {
+ setError(errorMsg);
+ return;
+ }
+ setError(undefined);
+ });
}
} else {
setError(undefined);
+ setValue(str);
}
- setValue(str);
}
async function startVideo() {
@@ -264,14 +315,14 @@ export function QrReaderPage({ onDetected }: Props):
VNode {
try {
const code = await createCanvasFromVideo(video, canvasRef.current);
if (code) {
- onChangeDetect(code);
+ await onChangeDetect(code);
setShow("canvas");
}
stream.getTracks().forEach((e) => {
e.stop();
});
} catch (error) {
- setError(i18n.str`something unexpected happen: ${error}`);
+ setError(i18n.str`Unexpected error happen reading the camera: ${error}`);
}
}
@@ -284,13 +335,13 @@ export function QrReaderPage({ onDetected }: Props):
VNode {
try {
const code = await createCanvasFromFile(fileContent, canvasRef.current);
if (code) {
- onChangeDetect(code);
+ await onChangeDetect(code);
setShow("canvas");
} else {
setError(i18n.str`Could not found a QR code in the file`);
}
} catch (error) {
- setError(i18n.str`something unexpected happen: ${error}`);
+ setError(i18n.str`Unexpected error happen reading the file: ${error}`);
}
}
const uri = parseTalerUri(value.toLowerCase());
@@ -313,53 +364,61 @@ export function QrReaderPage({ onDetected }: Props):
VNode {
onChange={onChange}
/>
</div>
- {uri && (
- <Button
- disabled={!!error}
- variant="contained"
- color="success"
- onClick={async () => {
- if (uri) onDetected(uri);
- }}
- >
- {(function (talerUri: TalerUri): VNode {
- switch (talerUri.type) {
- case TalerUriAction.Pay:
- return <i18n.Translate>Pay invoice</i18n.Translate>;
- case TalerUriAction.Withdraw:
- return (
- <i18n.Translate>Withdrawal from bank</i18n.Translate>
- );
- case TalerUriAction.Refund:
- return <i18n.Translate>Claim refund</i18n.Translate>;
- case TalerUriAction.PayPull:
- return <i18n.Translate>Pay invoice</i18n.Translate>;
- case TalerUriAction.PayPush:
- return <i18n.Translate>Accept payment</i18n.Translate>;
- case TalerUriAction.PayTemplate:
- return <i18n.Translate>Complete order</i18n.Translate>;
- case TalerUriAction.Restore:
- return <i18n.Translate>Restore wallet</i18n.Translate>;
- case TalerUriAction.DevExperiment:
- return <i18n.Translate>Enable experiment</i18n.Translate>;
- case TalerUriAction.WithdrawExchange:
- return (
- <i18n.Translate>Withdraw from exchange</i18n.Translate>
- );
- case TalerUriAction.AddExchange:
- return <i18n.Translate>Add exchange</i18n.Translate>;
- default: {
- assertUnreachable(talerUri);
- }
- }
- })(uri)}
- </Button>
- )}
</div>
<Grid container justifyContent="space-around" columns={2}>
<Grid item xs={2}>
<p>{error && <Alert severity="error">{error}</Alert>}</p>
</Grid>
+ {uri && (
+ <Grid item xs={2}>
+ <p>
+ <Button
+ disabled={!!error}
+ variant="contained"
+ color="success"
+ onClick={async () => {
+ if (uri) onDetected(uri);
+ }}
+ >
+ {(function (talerUri: TalerUri): VNode {
+ switch (talerUri.type) {
+ case TalerUriAction.Pay:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.Withdraw:
+ return (
+ <i18n.Translate>Withdrawal from bank</i18n.Translate>
+ );
+ case TalerUriAction.Refund:
+ return <i18n.Translate>Claim refund</i18n.Translate>;
+ case TalerUriAction.PayPull:
+ return <i18n.Translate>Pay invoice</i18n.Translate>;
+ case TalerUriAction.PayPush:
+ return <i18n.Translate>Accept payment</i18n.Translate>;
+ case TalerUriAction.PayTemplate:
+ return <i18n.Translate>Complete order</i18n.Translate>;
+ case TalerUriAction.Restore:
+ return <i18n.Translate>Restore wallet</i18n.Translate>;
+ case TalerUriAction.DevExperiment:
+ return (
+ <i18n.Translate>Enable experiment</i18n.Translate>
+ );
+ case TalerUriAction.WithdrawExchange:
+ return (
+ <i18n.Translate>
+ Withdraw from exchange
+ </i18n.Translate>
+ );
+ case TalerUriAction.AddExchange:
+ return <i18n.Translate>Add exchange</i18n.Translate>;
+ default: {
+ assertUnreachable(talerUri);
+ }
+ }
+ })(uri)}
+ </Button>
+ </p>
+ </Grid>
+ )}
<Grid item xs={2}>
<p>
<Button variant="contained" onClick={startVideo}>
@@ -390,3 +449,194 @@ export function QrReaderPage({ onDetected }: Props):
VNode {
</Container>
);
}
+const httpFetch: any = new BrowserFetchHttpLib();
+
+async function testValidUri(
+ uri: TalerUri,
+ i18n: InternationalizationAPI,
+): Promise<TranslatedString | undefined> {
+ switch (uri.type) {
+ case TalerUriAction.Pay: {
+ const errorExchange = await checkMerchantUrl(uri.merchantBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.Withdraw: {
+ const errorExchange = await checkBankUrl(
+ uri.bankIntegrationApiBaseUrl,
+ i18n,
+ );
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.Refund: {
+ const errorExchange = await checkMerchantUrl(uri.merchantBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.PayTemplate: {
+ const errorExchange = await checkMerchantUrl(uri.merchantBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.Restore: {
+ return undefined;
+ }
+ case TalerUriAction.DevExperiment: {
+ return undefined;
+ }
+ case TalerUriAction.PayPull: {
+ const errorExchange = await checkExchangeUrl(uri.exchangeBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.PayPush: {
+ const errorExchange = await checkExchangeUrl(uri.exchangeBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.AddExchange: {
+ const errorExchange = await checkExchangeUrl(uri.exchangeBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ case TalerUriAction.WithdrawExchange: {
+ const errorExchange = await checkExchangeUrl(uri.exchangeBaseUrl, i18n);
+ if (errorExchange) {
+ return errorExchange;
+ }
+ return undefined;
+ }
+ default: {
+ assertUnreachable(uri);
+ }
+ }
+}
+
+async function checkExchangeUrl(
+ baseUrl: string,
+ i18n: InternationalizationAPI,
+): Promise<TranslatedString | undefined> {
+ let url: URL | undefined = undefined;
+ try {
+ url = new URL("./", baseUrl);
+ } catch (e) {
+ return i18n.str`The exchange URL is invalid.`;
+ }
+ if (!baseUrl.endsWith("/")) {
+ return i18n.str`The exchange URL should end with '/'`;
+ }
+ try {
+ const config = await new TalerExchangeHttpClient(
+ url.href,
+ httpFetch,
+ ).getConfig();
+ if (config.type === "ok") {
+ return undefined;
+ } else {
+ switch (config.case) {
+ case HttpStatusCode.NotFound: {
+ return i18n.str`Couldn't found an exchange in the URL specified.`;
+ }
+ default: {
+ assertUnreachable(config.case);
+ }
+ }
+ }
+ } catch (e) {
+ if (e instanceof TalerError && e.errorDetail.detail) {
+ return i18n.str`HTTP request failed to ${url.href}:
${e.errorDetail.detail}`;
+ }
+ return i18n.str`HTTP request failed to ${url.href}`;
+ }
+}
+
+async function checkMerchantUrl(
+ baseUrl: string,
+ i18n: InternationalizationAPI,
+): Promise<TranslatedString | undefined> {
+ let url: URL | undefined = undefined;
+ try {
+ url = new URL("./", baseUrl);
+ } catch (e) {
+ return i18n.str`The merchant URL is invalid.`;
+ }
+ if (!baseUrl.endsWith("/")) {
+ return i18n.str`The merchant URL should end with '/'`;
+ }
+ try {
+ const config = await new TalerMerchantInstanceHttpClient(
+ url.href,
+ httpFetch,
+ ).getConfig();
+ if (config.type === "ok") {
+ return undefined;
+ } else {
+ switch (config.case) {
+ case HttpStatusCode.NotFound: {
+ return i18n.str`Couldn't found an merchant in the URL specified.`;
+ }
+ default: {
+ assertUnreachable(config.case);
+ }
+ }
+ }
+ } catch (e) {
+ if (e instanceof TalerError && e.errorDetail.detail) {
+ return i18n.str`HTTP request failed to ${url.href}:
${e.errorDetail.detail}`;
+ }
+ return i18n.str`HTTP request failed to ${url.href}`;
+ }
+}
+
+async function checkBankUrl(
+ baseUrl: string,
+ i18n: InternationalizationAPI,
+): Promise<TranslatedString | undefined> {
+ let url: URL | undefined = undefined;
+ try {
+ url = new URL("./", baseUrl);
+ } catch (e) {
+ return i18n.str`The bank URL is invalid.`;
+ }
+ if (!baseUrl.endsWith("/")) {
+ return i18n.str`The bank URL should end with '/'`;
+ }
+ try {
+ const config = await new TalerCoreBankHttpClient(
+ url.href,
+ httpFetch,
+ ).getConfig();
+ if (config.type === "ok") {
+ return undefined;
+ } else {
+ switch (config.case) {
+ case HttpStatusCode.NotFound: {
+ return i18n.str`Couldn't found an bank in the URL specified.`;
+ }
+ default: {
+ assertUnreachable(config.case);
+ }
+ }
+ }
+ } catch (e) {
+ if (e instanceof TalerError && e.errorDetail.detail) {
+ return i18n.str`HTTP request failed to ${url.href}:
${e.errorDetail.detail}`;
+ }
+ return i18n.str`HTTP request failed to ${url.href}`;
+ }
+}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [taler-typescript-core] branch master updated: fix #9535,
Admin <=