This commit is contained in:
Sebastian 2023-09-20 16:10:32 -03:00
parent e39d5c488e
commit 7d4c5a71aa
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
6 changed files with 225 additions and 150 deletions

View File

@ -85,18 +85,26 @@ export function getInitialBackendBaseURL(): string {
typeof localStorage !== "undefined"
? localStorage.getItem("bank-base-url")
: undefined;
let result: string;
if (!overrideUrl) {
//normal path
if (!bankUiSettings.backendBaseURL) {
console.error(
"ERROR: backendBaseURL was overridden by a setting file and missing. Setting value to 'window.origin'",
);
return canonicalizeBaseUrl(window.origin);
}
return canonicalizeBaseUrl(bankUiSettings.backendBaseURL);
result = window.origin
}
result = bankUiSettings.backendBaseURL;
} else {
// testing/development path
return canonicalizeBaseUrl(overrideUrl);
result = overrideUrl
}
try {
return canonicalizeBaseUrl(result)
} catch (e) {
//fall back
return canonicalizeBaseUrl(window.origin)
}
}
export const defaultState: BackendState = {

View File

@ -15,8 +15,12 @@
*/
import {
AmountString,
Codec,
buildCodecForObject,
codecForAmountString,
codecForBoolean,
codecForNumber,
codecForString,
codecOptional,
} from "@gnu-taler/taler-util";
@ -24,15 +28,21 @@ import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
interface Settings {
currentWithdrawalOperationId: string | undefined;
showWithdrawalSuccess: boolean;
maxWithdrawalAmount: number;
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("currentWithdrawalOperationId", codecOptional(codecForString()))
.property("showWithdrawalSuccess", (codecForBoolean()))
.property("maxWithdrawalAmount", codecForNumber())
.build("Settings");
const defaultSettings: Settings = {
currentWithdrawalOperationId: undefined,
showWithdrawalSuccess: true,
maxWithdrawalAmount: 25
};
const DEMOBANK_SETTINGS_KEY = buildStorageKey(

View File

@ -183,8 +183,28 @@ export function BankFrame({
{/* <span class="ml-auto w-9 min-w-max whitespace-nowrap rounded-full bg-gray-50 px-2.5 py-0.5 text-center text-xs font-medium leading-5 text-gray-600 ring-1 ring-inset ring-gray-200" aria-hidden="true">5</span> */}
</a>
</li>
<li>
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
<i18n.Translate>Show withdrawal confirmation</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
onClick={() => {
console.log(settings.showWithdrawalSuccess)
updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
}}>
<span aria-hidden="true" data-enabled={settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</li>
</ul>
</li>
<li class="sm:hidden">
<div class="text-xs font-semibold leading-6 text-gray-400">
<i18n.Translate>Sites</i18n.Translate>

View File

@ -95,8 +95,9 @@ export function WithdrawalOperationPage({
}): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response
const baseUrl = getInitialBackendBaseURL()
const uri = stringifyWithdrawUri({
bankIntegrationApiBaseUrl: `${getInitialBackendBaseURL()}/integration-api`,
bankIntegrationApiBaseUrl: `${baseUrl}/integration-api`,
withdrawalOperationId: operationId,
});
const parsedUri = parseWithdrawUri(uri);
@ -164,7 +165,7 @@ export function handleNotOkResult(
}
case ErrorType.UNREADABLE: {
notify({
type:"error",
type: "error",
title: i18n.str`Unexpected error.`,
description: i18n.str`Response from ${result.info?.url} is unreadable, http status: ${result.status}`,
debug: JSON.stringify(result),
@ -173,7 +174,7 @@ export function handleNotOkResult(
}
case ErrorType.UNEXPECTED: {
notify({
type:"error",
type: "error",
title: i18n.str`Unexpected error.`,
description: i18n.str`Diagnostic from ${result.info?.url} is "${result.message}"`,
debug: JSON.stringify(result),

View File

@ -20,24 +20,23 @@ import {
HttpStatusCode,
Logger,
PaytoUri,
PaytoUriGeneric,
PaytoUriIBAN,
PaytoUriTalerBank,
TranslatedString,
WithdrawUriResult,
WithdrawUriResult
} from "@gnu-taler/taler-util";
import {
RequestError,
notify,
notifyError,
notifyInfo,
useTranslationContext,
} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { useMemo, useState } from "preact/hooks";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { useAccessAnonAPI } from "../hooks/access.js";
import { buildRequestErrorMessage, undefinedIfEmpty } from "../utils.js";
import { ShowInputErrorLabel } from "../components/ShowInputErrorLabel.js";
import { Amount } from "./PaytoWireTransferForm.js";
const logger = new Logger("WithdrawalConfirmationQuestion");
@ -71,6 +70,7 @@ export function WithdrawalConfirmationQuestion({
const { confirmWithdrawal, abortWithdrawal } = useAccessAnonAPI();
const [captchaAnswer, setCaptchaAnswer] = useState<string | undefined>();
const answer = parseInt(captchaAnswer ?? "", 10);
const [busy, setBusy] = useState<Record<string, undefined>>()
const errors = undefinedIfEmpty({
answer: !captchaAnswer
? i18n.str`Answer the question before continue`
@ -79,13 +79,15 @@ export function WithdrawalConfirmationQuestion({
: answer !== captchaNumbers.a + captchaNumbers.b
? i18n.str`The answer "${answer}" to "${captchaNumbers.a} + ${captchaNumbers.b}" is wrong.`
: undefined,
});
}) ?? busy;
async function doTransfer() {
try {
setBusy({})
await confirmWithdrawal(
withdrawUri.withdrawalOperationId,
);
notifyInfo(i18n.str`Wire transfer completed!`)
} catch (error) {
if (error instanceof RequestError) {
notify(
@ -107,10 +109,12 @@ export function WithdrawalConfirmationQuestion({
)
}
}
setBusy(undefined)
}
async function doCancel() {
try {
setBusy({})
await abortWithdrawal(withdrawUri.withdrawalOperationId);
onAborted();
} catch (error) {
@ -132,6 +136,7 @@ export function WithdrawalConfirmationQuestion({
)
}
}
setBusy(undefined)
}
return (
@ -142,68 +147,6 @@ export function WithdrawalConfirmationQuestion({
<i18n.Translate>Confirm the withdrawal operation</i18n.Translate>
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<div class="px-4 mt-4 ">
<div class="w-full">
<div class="px-4 sm:px-0">
<p><i18n.Translate>Wire transfer details</i18n.Translate></p>
</div>
<div class="mt-6 border-t border-gray-100">
<dl class="divide-y divide-gray-100">
{((): VNode => {
switch (details.account.targetType) {
case "iban": {
const p = details.account as PaytoUriIBAN
const name = p.params["receiver-name"]
return <Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
</div>
{name &&
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
</div>
}
</Fragment>
}
case "x-taler-bank": {
const p = details.account as PaytoUriTalerBank
const name = p.params["receiver-name"]
return <Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
</div>
{name &&
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
</div>
}
</Fragment>
}
default:
return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
</div>
}
})()}
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.reserve}</dd>
</div>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd>
</div>
</dl>
</div>
</div>
</div>
<div class="px-4 mt-4 grid grid-cols-1 gap-y-6 sm:grid-cols-3 sm:gap-x-3">
<label class={"relative flex cursor-pointer rounded-lg border bg-white p-4 shadow-sm focus:outline-noneborder-indigo-600 ring-2 ring-indigo-600"}>
@ -285,36 +228,32 @@ export function WithdrawalConfirmationQuestion({
aria-describedby="answer"
autoFocus
class="block w-full rounded-md border-0 py-1.5 text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
// value={username ?? ""}
value={captchaAnswer ?? ""}
required
name="answer"
id="answer"
autocomplete="off"
// value={value ?? ""}
// disabled={!onChange}
// onInput={(e): void => {
// if (onChange) {
// onChange(e.currentTarget.value);
// }
// }}
onChange={(e): void => {
setCaptchaAnswer(e.currentTarget.value)
}}
/>
</div>
<ShowInputErrorLabel message={errors?.answer} isDirty={false} />
<ShowInputErrorLabel message={errors?.answer} isDirty={captchaAnswer !== undefined} />
</div>
</div>
<div class="flex items-center justify-between gap-x-6 border-t border-gray-900/10 px-4 py-4 sm:px-8">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900"
// onClick={onCancel}
onClick={doCancel}
>
<i18n.Translate>Cancel</i18n.Translate></button>
<button type="submit"
class="disabled:opacity-50 disabled:cursor-default cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
// disabled={isRawPayto ? !!errorsPayto : !!errorsWire}
// onClick={(e) => {
// e.preventDefault()
// doStart()
// }}
disabled={!!errors}
onClick={(e) => {
e.preventDefault()
doTransfer()
}}
>
<i18n.Translate>Transfer</i18n.Translate>
</button>
@ -323,6 +262,68 @@ export function WithdrawalConfirmationQuestion({
</form>
</div>
</div>
<div class="px-4 mt-4 ">
<div class="w-full">
<div class="px-4 sm:px-0">
<p><i18n.Translate>Wire transfer details</i18n.Translate></p>
</div>
<div class="mt-6 border-t border-gray-100">
<dl class="divide-y divide-gray-100">
{((): VNode => {
switch (details.account.targetType) {
case "iban": {
const p = details.account as PaytoUriIBAN
const name = p.params["receiver-name"]
return <Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.iban}</dd>
</div>
{name &&
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
</div>
}
</Fragment>
}
case "x-taler-bank": {
const p = details.account as PaytoUriTalerBank
const name = p.params["receiver-name"]
return <Fragment>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.account}</dd>
</div>
{name &&
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange name</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{p.params["receiver-name"]}</dd>
</div>
}
</Fragment>
}
default:
return <div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Exchange account</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.account.targetPath}</dd>
</div>
}
})()}
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Withdrawal identification</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{details.reserve}</dd>
</div>
<div class="px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
<dt class="text-sm font-medium leading-6 text-gray-900">Amount</dt>
<dd class="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">{Amounts.stringifyValue(details.amount)}</dd>
</div>
</dl>
</div>
</div>
</div>
</div>
</div>

View File

@ -62,9 +62,10 @@ export function WithdrawalQRCode({
result.type === ErrorType.CLIENT &&
result.status === HttpStatusCode.NotFound
) {
clearCurrentWithdrawal()
return <div>operation not found</div>;
}
onLoadNotOk();
// onLoadNotOk();
return handleNotOkResult(i18n)(result);
}
const { data } = result;
@ -85,7 +86,7 @@ export function WithdrawalQRCode({
</i18n.Translate>
</p>
<a class="pure-button pure-button-primary"
style={{float:"right"}}
style={{ float: "right" }}
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
@ -99,35 +100,82 @@ export function WithdrawalQRCode({
}
if (data.confirmation_done) {
return <section id="main" class="content">
<h1 class="nav">{i18n.str`Operation completed`}</h1>
<section id="assets" style={{maxWidth: 400, marginLeft: "auto", marginRight:"auto"}}>
<p>
if (!settings.showWithdrawalSuccess) {
clearCurrentWithdrawal()
onContinue()
}
return <div class="relative ml-auto mr-auto transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6">
<div>
<div class="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
<svg class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-5">
<h3 class="text-base font-semibold leading-6 text-gray-900" id="modal-title">
<i18n.Translate>Withdrawal OK</i18n.Translate>
</h3>
<div class="mt-2">
<p class="text-sm text-gray-500">
<i18n.Translate>
The wire transfer to the GNU Taler Exchange bank's account is completed, now the
The wire transfer to the Taler exchange bank's account is completed, now the
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate>
</p>
<p>
</div>
<div class="mt-2">
<p >
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate>
</p>
<div style={{textAlign:"center"}}>
</div>
</div>
</div>
<div class="mt-4">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span class="text-sm text-black font-medium leading-6 " id="availability-label">
<i18n.Translate>Do not show this again</i18n.Translate>
</span>
</span>
<button type="button" data-enabled={!settings.showWithdrawalSuccess} class="bg-indigo-600 data-[enabled=false]:bg-gray-200 relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-indigo-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description"
<a class="pure-button pure-button-primary"
onClick={() => {
updateSettings("showWithdrawalSuccess", !settings.showWithdrawalSuccess);
}}>
<span aria-hidden="true" data-enabled={!settings.showWithdrawalSuccess} class="translate-x-5 data-[enabled=false]:translate-x-0 pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</div>
<div class="mt-5 sm:mt-6">
<button type="button"
class="inline-flex w-full justify-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
onContinue()
}}>
{i18n.str`Continue`}
</a>
<i18n.Translate>Continue</i18n.Translate>
</button>
</div>
</section>
</section>
</div>
}
if (!data.selection_done) {
return (
<QrCodeSection
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal()
onContinue()
}}
/>
);
}
if (!data.selected_reserve_pub) {
return <div>
the exchange is selcted but no reserve pub
@ -142,19 +190,6 @@ export function WithdrawalQRCode({
</div>
}
if (!data.selection_done) {
return (
<QrCodeSection
withdrawUri={withdrawUri}
onAborted={() => {
notifyInfo(i18n.str`Operation canceled`);
clearCurrentWithdrawal()
onContinue()
}}
/>
);
}
return (
<WithdrawalConfirmationQuestion
withdrawUri={withdrawUri}