/* This file is part of GNU Taler (C) 2022 Taler Systems S.A. GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see */ import { AbsoluteTime, Amounts, HttpStatusCode, Logger, TalerCorebankApi, TalerError, TalerErrorCode, TranslatedString, assertUnreachable, parsePaytoUri } from "@gnu-taler/taler-util"; import { Loading, LocalNotificationBanner, ShowInputErrorLabel, useLocalNotification, useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; import { ErrorLoadingWithDebug } from "../components/ErrorLoadingWithDebug.js"; import { useBankCoreApiContext } from "../context/config.js"; import { useWithdrawalDetails } from "../hooks/access.js"; import { useBackendState } from "../hooks/backend.js"; import { ChallengeInProgess, useBankState } from "../hooks/bank-state.js"; import { useConversionInfo } from "../hooks/circuit.js"; import { undefinedIfEmpty } from "../utils.js"; import { RenderAmount } from "./PaytoWireTransferForm.js"; import { OperationNotFound } from "./WithdrawalQRCode.js"; const logger = new Logger("SolveChallenge"); export function SolveChallengePage({ onContinue, }: { onContinue: () => void; }): VNode { const { api } = useBankCoreApiContext() const { i18n } = useTranslationContext(); const [bankState, updateBankState] = useBankState(); const [code, setCode] = useState(undefined); const [notification, notify, handleError] = useLocalNotification() const { state } = useBackendState(); const creds = state.status !== "loggedIn" ? undefined : state if (!bankState.currentChallenge) { return
no challenge to solve
} const ch = bankState.currentChallenge const errors = undefinedIfEmpty({ code: !code ? i18n.str`required` : undefined, }); async function startChallenge() { if (!creds) return; await handleError(async () => { const resp = await api.sendChallenge(creds, ch.id); if (resp.type === "ok") { const newCh = structuredClone(ch) newCh.sent = AbsoluteTime.now() newCh.info = resp.body updateBankState("currentChallenge", newCh) } else { switch (resp.case) { case HttpStatusCode.NotFound: return notify({ type: "error", title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case HttpStatusCode.Unauthorized: return notify({ type: "error", title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case TalerErrorCode.BANK_TAN_CHANNEL_SCRIPT_FAILED: return notify({ type: "error", title: i18n.str`Cashout not found. It may be also mean that it was already aborted.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) default: assertUnreachable(resp) } } }) } async function completeChallenge() { if (!creds || !code) return; await handleError(async () => { { const resp = await api.confirmChallenge(creds, ch.id, { tan: code }); if (resp.type === "fail") { setCode("") switch (resp.case) { case HttpStatusCode.NotFound: return notify({ type: "error", title: i18n.str`Challenge not found.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case HttpStatusCode.Unauthorized: return notify({ type: "error", title: i18n.str`This user is not authorized to complete this challenge.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case HttpStatusCode.TooManyRequests: return notify({ type: "error", title: i18n.str`Too many attemps, try another code.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case TalerErrorCode.BANK_TAN_CHALLENGE_FAILED: return notify({ type: "error", title: i18n.str`The confirmation code is wrong, try again.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) case TalerErrorCode.BANK_TAN_CHALLENGE_EXPIRED: return notify({ type: "error", title: i18n.str`The operation expired.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) default: assertUnreachable(resp) } } } { const resp = await (async (ch: ChallengeInProgess) => { switch (ch.operation) { case "delete-account": return await api.deleteAccount(creds, ch.id) case "update-account": return await api.updateAccount(creds, ch.request, ch.id) case "update-password": return await api.updatePassword(creds, ch.request, ch.id) case "create-transaction": return await api.createTransaction(creds, ch.request, ch.id) case "confirm-withdrawal": return await api.confirmWithdrawalById(creds, ch.request, ch.id) case "create-cashout": return await api.createCashout(creds, ch.request, ch.id) default: assertUnreachable(ch) } })(ch); if (resp.type === "fail") { if (resp.case !== HttpStatusCode.Accepted) { return notify({ type: "error", title: i18n.str`The operation failed.`, description: resp.detail.hint as TranslatedString, debug: resp.detail, }) } // another challenge required updateBankState("currentChallenge", { operation: ch.operation, id: String(resp.body.challenge_id), sent: AbsoluteTime.never(), request: ch.request as any, }) return notify({ type: "info", title: i18n.str`The operation needs another confirmation to complete.`, }) } updateBankState("currentChallenge", undefined) return onContinue() } }) } const subtitle = ((op): TranslatedString => { switch (op) { case "delete-account": return i18n.str`Account delete` case "update-account": return i18n.str`Account update` case "update-password": return i18n.str`Password update` case "create-transaction": return i18n.str`Wire transfer` case "confirm-withdrawal": return i18n.str`Withdrawal` case "create-cashout": return i18n.str`Cashout` } })(ch.operation) return (

Confirm the operation

{subtitle}
{ch.info &&
{ e.preventDefault() }} >
{ setCode(e.currentTarget.value) }} />
{/* */} {/* */}
}
); } function ChallengeDetails({ challenge, onStart }: { challenge: ChallengeInProgess, onStart: () => void }): VNode { const { i18n } = useTranslationContext(); const { config } = useBankCoreApiContext(); return
{challenge.info ? : }

Operation details

{((): VNode => { switch (challenge.operation) { case "delete-account": return
Account
{challenge.request}
case "create-transaction": { const payto = parsePaytoUri(challenge.request.payto_uri)! return {challenge.request.amount &&
Amount
} {payto.isKnown && payto.targetType === "iban" &&
To account
{payto.iban}
}
} case "confirm-withdrawal": return case "create-cashout": { return } case "update-account": { return {challenge.request.cashout_payto_uri !== undefined &&
Cashout account
{challenge.request.cashout_payto_uri}
} {challenge.request.contact_data?.email !== undefined &&
Email
{challenge.request.contact_data?.email}
} {challenge.request.contact_data?.phone !== undefined &&
Phone
{challenge.request.contact_data?.phone}
} {challenge.request.debit_threshold !== undefined &&
Debit threshold
} {challenge.request.is_public !== undefined &&
Is public
{challenge.request.is_public ? "enable" : "disable"}
} {challenge.request.name !== undefined &&
Name
{challenge.request.name}
} {challenge.request.tan_channel !== undefined &&
Authentication channel
{challenge.request.tan_channel}
}
} case "update-password": { return
New password
{challenge.request.new_password}
} default: assertUnreachable(challenge) } })()} {challenge.info &&

Challenge details

} {challenge.sent.t_ms !== "never" &&
Sent at
{format(challenge.sent.t_ms, "dd/MM/yyyy HH:mm:ss")}
} {challenge.info &&
{((ch: TalerCorebankApi.TanChannel): VNode => { switch (ch) { case TalerCorebankApi.TanChannel.SMS: return To phone case TalerCorebankApi.TanChannel.EMAIL: return To email default: assertUnreachable(ch) } })(challenge.info.tan_channel)}
{challenge.info.tan_info}
}
} function ShowWithdrawalDetails({ id }: { id: string }): VNode { const { i18n } = useTranslationContext(); const details = useWithdrawalDetails(id) const { config } = useBankCoreApiContext(); if (!details) { return } if (details instanceof TalerError) { return } if (details.type === "fail") { switch (details.case) { case HttpStatusCode.BadRequest: case HttpStatusCode.NotFound: return default: assertUnreachable(details) } } return
Amount
{details.body.selected_reserve_pub !== undefined &&
Withdraw id
{details.body.selected_reserve_pub.substring(0, 16)}...
} {details.body.selected_exchange_account !== undefined &&
To account
{details.body.selected_exchange_account}
}
} function ShowCashoutDetails({ request }: { request: TalerCorebankApi.CashoutRequest }): VNode { const { i18n } = useTranslationContext(); const info = useConversionInfo(); if (!info) { return } if (info instanceof TalerError) { return } return {request.subject !== undefined &&
Subject
{request.subject}
}
Debit
Credit
}