diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 244bc102..75bb7fc2 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -225,6 +225,7 @@ const dict = { will_receive: "Will receive", refund_available_in: "Refund will be available in {{ blocks }} blocks", no_wallet_connected: "No wallet connected", + qr_scan_supported: "This browser doesn't support refunds via QR", }, de: { language: "Deutsch", @@ -462,6 +463,8 @@ const dict = { will_receive: "Sie erhalten", refund_available_in: "Rückerstattung möglich in {{ blocks }} Blöcken", no_wallet_connected: "Kein Wallet verbunden", + qr_scan_supported: + "Dieser Browser unterstützt keine Erstattungen über QR", }, es: { language: "Español", @@ -695,6 +698,8 @@ const dict = { will_receive: "Recibirá", refund_available_in: "Reembolso disponible en {{ blocks }} bloques", no_wallet_connected: "No hay monedero conectado", + qr_scan_supported: + "Este navegador no admite devoluciones a través de QR", }, zh: { language: "中文", @@ -902,6 +907,7 @@ const dict = { will_receive: "将收到", refund_available_in: "退款将分 {{ blocks }} 区块提供", no_wallet_connected: "未连接钱包", + qr_scan_supported: "此浏览器不支持通过 QR 退款", }, ja: { language: "日本語", @@ -1134,6 +1140,7 @@ const dict = { will_receive: "受信予定", refund_available_in: "返金は {{ blocks }} つのブロックに分かれる", no_wallet_connected: "財布はつながっていない!", + qr_scan_supported: "このブラウザはQRによる払い戻しに対応していません。", }, }; diff --git a/src/pages/Refund.tsx b/src/pages/Refund.tsx index 6f7edaa7..46ce02be 100644 --- a/src/pages/Refund.tsx +++ b/src/pages/Refund.tsx @@ -20,10 +20,16 @@ import { LogRefundData, scanLogsForPossibleRefunds, } from "../utils/contractLogs"; +import { qrScanProbe } from "../utils/qrScanProbe"; import { validateRefundFile } from "../utils/refundFile"; import { SomeSwap } from "../utils/swapCreator"; import ErrorWasm from "./ErrorWasm"; +enum RefundError { + InvalidData, + QrScanNotSupported, +} + const Refund = () => { const navigate = useNavigate(); const { getSwap, getSwaps, updateSwapStatus, wasmSupported, t } = @@ -31,7 +37,9 @@ const Refund = () => { const { signer, providers, getEtherSwap } = useWeb3Signer(); const [swapFound, setSwapFound] = createSignal(null); - const [refundInvalid, setRefundInvalid] = createSignal(false); + const [refundInvalid, setRefundInvalid] = createSignal< + RefundError | undefined + >(undefined); const [refundJson, setRefundJson] = createSignal(null); const [refundTxId, setRefundTxId] = createSignal(""); @@ -52,47 +60,47 @@ const Refund = () => { } setRefundJson(data); - setRefundInvalid(false); + setRefundInvalid(undefined); } catch (e) { log.warn("Refund json validation failed", e); - setRefundInvalid(true); + setRefundInvalid(RefundError.InvalidData); input.setCustomValidity(t("invalid_refund_file")); } }; - const uploadChange = (e: Event) => { + const uploadChange = async (e: Event) => { const input = e.currentTarget as HTMLInputElement; const inputFile = input.files[0]; input.setCustomValidity(""); setRefundJson(null); setSwapFound(null); - setRefundInvalid(false); + setRefundInvalid(undefined); if (["image/png", "image/jpg", "image/jpeg"].includes(inputFile.type)) { - QrScanner.scanImage(inputFile, { returnDetailedScanResult: true }) - .then( - async (result) => - await checkRefundJsonKeys( - input, - JSON.parse(result.data), - ), - ) - .catch((e) => { - log.error("invalid QR code upload", e); - setRefundInvalid(true); - input.setCustomValidity(t("invalid_refund_file")); + if (!(await qrScanProbe())) { + setRefundInvalid(RefundError.QrScanNotSupported); + return; + } + + try { + const res = await QrScanner.scanImage(inputFile, { + returnDetailedScanResult: true, }); + await checkRefundJsonKeys(input, JSON.parse(res.data)); + } catch (e) { + log.error("invalid QR code upload", e); + setRefundInvalid(RefundError.InvalidData); + input.setCustomValidity(t("invalid_refund_file")); + } } else { - inputFile - .text() - .then(async (result) => { - await checkRefundJsonKeys(input, JSON.parse(result)); - }) - .catch((e) => { - log.error("invalid file upload", e); - setRefundInvalid(true); - input.setCustomValidity(t("invalid_refund_file")); - }); + try { + const data = await inputFile.text(); + await checkRefundJsonKeys(input, JSON.parse(data)); + } catch (e) { + log.error("invalid file upload", e); + setRefundInvalid(RefundError.InvalidData); + input.setCustomValidity(t("invalid_refund_file")); + } } }; @@ -249,10 +257,18 @@ const Refund = () => { {t("open_swap")} - +
diff --git a/src/utils/qrScanProbe.ts b/src/utils/qrScanProbe.ts new file mode 100644 index 00000000..4bcf64da --- /dev/null +++ b/src/utils/qrScanProbe.ts @@ -0,0 +1,19 @@ +import log from "loglevel"; +import QrScanner from "qr-scanner"; + +import { formatError } from "./errors"; + +const image = + "data:image/png;base64,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"; + +export const qrScanProbe = async () => { + try { + const res = await QrScanner.scanImage(new URL(image), { + returnDetailedScanResult: true, + }); + return res.data === "21"; + } catch (e) { + log.warn(`QR code scanning probe failed: ${formatError(e)}`); + return false; + } +};