Merge branch 'master' into age-withdraw

This commit is contained in:
Özgür Kesim 2023-08-15 13:47:29 +02:00
commit 819949d7f2
Signed by: oec
GPG Key ID: 3D76A56D79EDD9D7
48 changed files with 3030 additions and 432 deletions

View File

@ -61,17 +61,20 @@ export function buildQuerySignature(key: SigningKey): string {
return encodeCrock(eddsaSign(sigBlob, key)); return encodeCrock(eddsaSign(sigBlob, key));
} }
export function buildDecisionSignature( export function buildDecisionSignature(
key: SigningKey, key: SigningKey,
decision: AmlExchangeBackend.AmlDecision, decision: AmlExchangeBackend.AmlDecision,
): string { ): string {
const zero = new Uint8Array(new ArrayBuffer(64))
const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION) const sigBlob = buildSigPS(TalerSignaturePurpose.TALER_SIGNATURE_AML_DECISION)
.put(hash(stringToBytes(decision.justification))) //TODO: new need the null terminator, also in the exchange
// .put(timestampRoundedToBuffer(decision.decision_time)) .put(hash(stringToBytes(decision.justification)))//check null
.put(timestampRoundedToBuffer(decision.decision_time))
.put(amountToBuffer(decision.new_threshold)) .put(amountToBuffer(decision.new_threshold))
.put(decodeCrock(decision.h_payto)) .put(decodeCrock(decision.h_payto))
// .put(hash(stringToBytes(decision.kyc_requirements))) .put(zero) //kyc_requirement
.put(bufferForUint32(decision.new_state)) .put(bufferForUint32(decision.new_state))
.build(); .build();

View File

@ -85,7 +85,6 @@ export function useCases(
const records = !afterData const records = !afterData
? [] ? []
: ((afterData ?? lastAfter).data ?? { records: [] }).records; : ((afterData ?? lastAfter).data ?? { records: [] }).records;
console.log("afterdata", afterData, lastAfter, records)
if (loadingAfter) return { loading: true, data: { records } }; if (loadingAfter) return { loading: true, data: { records } };
if (afterData) { if (afterData) {
return { ok: true, data: { records }, ...pagination }; return { ok: true, data: { records }, ...pagination };

View File

@ -38,7 +38,7 @@ export function NewFormEntry({
fullName: "loggedIn_user_fullname", fullName: "loggedIn_user_fullname",
when: AbsoluteTime.now(), when: AbsoluteTime.now(),
state: AmlExchangeBackend.AmlState.pending, state: AmlExchangeBackend.AmlState.pending,
threshold: Amounts.parseOrThrow("ARS:1000"), threshold: Amounts.parseOrThrow("KUDOS:1000"),
}; };
const api = useAmlCasesAPI() const api = useAmlCasesAPI()

View File

@ -1,20 +1,78 @@
import { clk } from "@gnu-taler/taler-util/clk"; import { clk } from "@gnu-taler/taler-util/clk";
import { import {
discoverPolicies,
getBackupStartState, getBackupStartState,
getRecoveryStartState, getRecoveryStartState,
reduceAction, reduceAction,
} from "@gnu-taler/anastasis-core"; } from "@gnu-taler/anastasis-core";
import fs from "fs"; import fs from "fs";
import { j2s } from "@gnu-taler/taler-util";
export const reducerCli = clk export const reducerCli = clk.program("anastasis-cli", {
.program("reducer", { help: "Command line interface for Anastasis.",
help: "Command line interface for Anastasis.", });
reducerCli
.subcommand("reducer", "reduce", {
help: "Run the anastasis reducer",
}) })
.flag("initBackup", ["-b", "--backup"]) .flag("initBackup", ["-b", "--backup"])
.flag("initRecovery", ["-r", "--restore"]) .flag("initRecovery", ["-r", "--restore"])
.maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING) .maybeOption("argumentsJson", ["-a", "--arguments"], clk.STRING)
.maybeArgument("action", clk.STRING) .maybeArgument("action", clk.STRING)
.maybeArgument("stateFile", clk.STRING); .maybeArgument("stateFile", clk.STRING)
.action(async (x) => {
if (x.reducer.initBackup) {
console.log(JSON.stringify(await getBackupStartState()));
return;
} else if (x.reducer.initRecovery) {
console.log(JSON.stringify(await getRecoveryStartState()));
return;
}
const action = x.reducer.action;
if (!action) {
console.log("action required");
return;
}
let lastState: any;
if (x.reducer.stateFile) {
const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
let args: any;
if (x.reducer.argumentsJson) {
args = JSON.parse(x.reducer.argumentsJson);
} else {
args = {};
}
const nextState = await reduceAction(lastState, action, args);
console.log(JSON.stringify(nextState));
});
reducerCli
.subcommand("discover", "discover", {
help: "Run the anastasis reducer",
})
.maybeArgument("stateFile", clk.STRING)
.action(async (args) => {
let lastState: any;
if (args.discover.stateFile) {
const s = fs.readFileSync(args.discover.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
const res = await discoverPolicies(lastState);
console.log(j2s(res));
});
async function read(stream: NodeJS.ReadStream): Promise<string> { async function read(stream: NodeJS.ReadStream): Promise<string> {
const chunks = []; const chunks = [];
@ -24,41 +82,6 @@ async function read(stream: NodeJS.ReadStream): Promise<string> {
return Buffer.concat(chunks).toString("utf8"); return Buffer.concat(chunks).toString("utf8");
} }
reducerCli.action(async (x) => {
if (x.reducer.initBackup) {
console.log(JSON.stringify(await getBackupStartState()));
return;
} else if (x.reducer.initRecovery) {
console.log(JSON.stringify(await getRecoveryStartState()));
return;
}
const action = x.reducer.action;
if (!action) {
console.log("action required");
return;
}
let lastState: any;
if (x.reducer.stateFile) {
const s = fs.readFileSync(x.reducer.stateFile, { encoding: "utf-8" });
lastState = JSON.parse(s);
} else {
const s = await read(process.stdin);
lastState = JSON.parse(s);
}
let args: any;
if (x.reducer.argumentsJson) {
args = JSON.parse(x.reducer.argumentsJson);
} else {
args = {};
}
const nextState = await reduceAction(lastState, action, args);
console.log(JSON.stringify(nextState));
});
export function reducerCliMain() { export function reducerCliMain() {
reducerCli.run(); reducerCli.run();
} }

View File

@ -151,7 +151,11 @@ export async function decryptPolicyMetadata(
userId: UserIdentifier, userId: UserIdentifier,
metadataEnc: OpaqueData, metadataEnc: OpaqueData,
): Promise<PolicyMetadata> { ): Promise<PolicyMetadata> {
// @ts-ignore
console.log("metadataEnc", metadataEnc);
const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd"); const plain = await anastasisDecrypt(asOpaque(userId), metadataEnc, "rmd");
// @ts-ignore
console.log("plain:", plain);
const metadataBytes = decodeCrock(plain); const metadataBytes = decodeCrock(plain);
const policyHash = encodeCrock(metadataBytes.slice(0, 64)); const policyHash = encodeCrock(metadataBytes.slice(0, 64));
const secretName = bytesToString(metadataBytes.slice(64)); const secretName = bytesToString(metadataBytes.slice(64));

View File

@ -21,9 +21,11 @@ export function Loading(): VNode {
<div <div
class="columns is-centered is-vcentered" class="columns is-centered is-vcentered"
style={{ style={{
height: "calc(100% - 3rem)",
position: "absolute",
width: "100%", width: "100%",
height: "200px",
display: "flex",
margin: "auto",
justifyContent: "center",
}} }}
> >
<Spinner /> <Spinner />
@ -33,7 +35,7 @@ export function Loading(): VNode {
export function Spinner(): VNode { export function Spinner(): VNode {
return ( return (
<div class="lds-ring"> <div class="lds-ring" style={{margin:"auto"}}>
<div /> <div />
<div /> <div />
<div /> <div />

View File

@ -14,8 +14,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2022-12-26 23:30+0000\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"taler-bank-spa/it/>\n" "taler-bank-spa/it/>\n"
"Language: it\n" "Language: it\n"
@ -199,9 +199,9 @@ msgid "Amount to withdraw:"
msgstr "Somma da ritirare" msgstr "Somma da ritirare"
#: src/pages/home/WalletWithdrawForm.tsx:84 #: src/pages/home/WalletWithdrawForm.tsx:84
#, fuzzy, c-format #, c-format
msgid "Withdraw" msgid "Withdraw"
msgstr "Conferma il ritiro" msgstr "Prelevare"
#: src/pages/home/WalletWithdrawForm.tsx:128 #: src/pages/home/WalletWithdrawForm.tsx:128
#, fuzzy, c-format #, fuzzy, c-format
@ -231,12 +231,12 @@ msgstr "Trasferisci fondi a un altro conto di questa banca:"
#: src/pages/home/Transactions.tsx:69 #: src/pages/home/Transactions.tsx:69
#, c-format #, c-format
msgid "Date" msgid "Date"
msgstr "" msgstr "Data"
#: src/pages/home/Transactions.tsx:70 #: src/pages/home/Transactions.tsx:70
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "Somma" msgstr "Importo"
#: src/pages/home/Transactions.tsx:71 #: src/pages/home/Transactions.tsx:71
#, c-format #, c-format
@ -246,7 +246,7 @@ msgstr "Controparte"
#: src/pages/home/Transactions.tsx:72 #: src/pages/home/Transactions.tsx:72
#, c-format #, c-format
msgid "Subject" msgid "Subject"
msgstr "Causale" msgstr "Soggetto"
#: src/pages/home/QrCodeSection.tsx:41 #: src/pages/home/QrCodeSection.tsx:41
#, fuzzy, c-format #, fuzzy, c-format

View File

@ -84,11 +84,11 @@ export function HomePage({
export function WithdrawalOperationPage({ export function WithdrawalOperationPage({
operationId, operationId,
onLoadNotOk, onLoadNotOk,
onAbort, onContinue,
}: { }: {
operationId: string; operationId: string;
onLoadNotOk: () => void; onLoadNotOk: () => void;
onAbort: () => void; onContinue: () => void;
}): VNode { }): VNode {
//FIXME: libeufin sandbox should return show to create the integration api endpoint //FIXME: libeufin sandbox should return show to create the integration api endpoint
//or return withdrawal uri from response //or return withdrawal uri from response
@ -99,12 +99,6 @@ export function WithdrawalOperationPage({
const parsedUri = parseWithdrawUri(uri); const parsedUri = parseWithdrawUri(uri);
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const [settings, updateSettings] = useSettings();
function clearCurrentWithdrawal(): void {
updateSettings("currentWithdrawalOperationId", undefined);
onAbort();
}
if (!parsedUri) { if (!parsedUri) {
notifyError({ notifyError({
title: i18n.str`The Withdrawal URI is not valid: "${uri}"`, title: i18n.str`The Withdrawal URI is not valid: "${uri}"`,
@ -115,10 +109,7 @@ export function WithdrawalOperationPage({
return ( return (
<WithdrawalQRCode <WithdrawalQRCode
withdrawUri={parsedUri} withdrawUri={parsedUri}
onConfirmed={() => { onContinue={onContinue}
notifyInfo(i18n.str`Withdrawal confirmed!`);
}}
onAborted={clearCurrentWithdrawal}
onLoadNotOk={onLoadNotOk} onLoadNotOk={onLoadNotOk}
/> />
); );

View File

@ -40,7 +40,7 @@ export function Routing(): VNode {
component={({ wopid }: { wopid: string }) => ( component={({ wopid }: { wopid: string }) => (
<WithdrawalOperationPage <WithdrawalOperationPage
operationId={wopid} operationId={wopid}
onAbort={() => { onContinue={() => {
route("/account"); route("/account");
}} }}
onLoadNotOk={() => { onLoadNotOk={() => {

View File

@ -33,7 +33,6 @@ import { ShowInputErrorLabel } from "./ShowInputErrorLabel.js";
const logger = new Logger("WithdrawalConfirmationQuestion"); const logger = new Logger("WithdrawalConfirmationQuestion");
interface Props { interface Props {
onConfirmed: () => void;
onAborted: () => void; onAborted: () => void;
withdrawUri: WithdrawUriResult; withdrawUri: WithdrawUriResult;
} }
@ -42,7 +41,6 @@ interface Props {
* Not providing a back button, only abort. * Not providing a back button, only abort.
*/ */
export function WithdrawalConfirmationQuestion({ export function WithdrawalConfirmationQuestion({
onConfirmed,
onAborted, onAborted,
withdrawUri, withdrawUri,
}: Props): VNode { }: Props): VNode {
@ -119,7 +117,6 @@ export function WithdrawalConfirmationQuestion({
await confirmWithdrawal( await confirmWithdrawal(
withdrawUri.withdrawalOperationId, withdrawUri.withdrawalOperationId,
); );
onConfirmed();
} catch (error) { } catch (error) {
if (error instanceof RequestError) { if (error instanceof RequestError) {
notifyError( notifyError(

View File

@ -24,6 +24,7 @@ import { Fragment, VNode, h } from "preact";
import { Loading } from "../components/Loading.js"; import { Loading } from "../components/Loading.js";
import { useWithdrawalDetails } from "../hooks/access.js"; import { useWithdrawalDetails } from "../hooks/access.js";
import { notifyInfo } from "../hooks/notification.js"; import { notifyInfo } from "../hooks/notification.js";
import { useSettings } from "../hooks/settings.js";
import { handleNotOkResult } from "./HomePage.js"; import { handleNotOkResult } from "./HomePage.js";
import { QrCodeSection } from "./QrCodeSection.js"; import { QrCodeSection } from "./QrCodeSection.js";
import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js"; import { WithdrawalConfirmationQuestion } from "./WithdrawalConfirmationQuestion.js";
@ -32,8 +33,7 @@ const logger = new Logger("WithdrawalQRCode");
interface Props { interface Props {
withdrawUri: WithdrawUriResult; withdrawUri: WithdrawUriResult;
onAborted: () => void; onContinue: () => void;
onConfirmed: () => void;
onLoadNotOk: () => void; onLoadNotOk: () => void;
} }
/** /**
@ -43,10 +43,14 @@ interface Props {
*/ */
export function WithdrawalQRCode({ export function WithdrawalQRCode({
withdrawUri, withdrawUri,
onConfirmed, onContinue,
onAborted,
onLoadNotOk, onLoadNotOk,
}: Props): VNode { }: Props): VNode {
const [settings, updateSettings] = useSettings();
function clearCurrentWithdrawal(): void {
updateSettings("currentWithdrawalOperationId", undefined);
onContinue();
}
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId); const result = useWithdrawalDetails(withdrawUri.withdrawalOperationId);
if (!result.ok) { if (!result.ok) {
@ -64,13 +68,64 @@ export function WithdrawalQRCode({
} }
const { data } = result; const { data } = result;
logger.trace("withdrawal status", data); if (data.aborted) {
if (data.aborted || data.confirmation_done) { return <section id="main" class="content">
// signal that this withdrawal is aborted <h1 class="nav">{i18n.str`Operation aborted`}</h1>
// will redirect to account info <section>
notifyInfo(i18n.str`Operation completed`); <p>
onAborted(); <i18n.Translate>
return <Loading />; The wire transfer to the GNU Taler Exchange bank's account was aborted, your balance
was not affected.
</i18n.Translate>
</p>
<p>
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate>
</p>
<a class="pure-button pure-button-primary"
style={{float:"right"}}
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
onContinue()
}}>
{i18n.str`Continue`}
</a>
</section>
</section>
}
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>
<i18n.Translate>
The wire transfer to the GNU Taler Exchange bank's account is completed, now the
exchange will send the requested amount into your GNU Taler wallet.
</i18n.Translate>
</p>
<p>
<i18n.Translate>
You can close this page now or continue to the account page.
</i18n.Translate>
</p>
<div style={{textAlign:"center"}}>
<a class="pure-button pure-button-primary"
onClick={async (e) => {
e.preventDefault();
clearCurrentWithdrawal()
onContinue()
}}>
{i18n.str`Continue`}
</a>
</div>
</section>
</section>
} }
if (!data.selection_done) { if (!data.selection_done) {
@ -79,25 +134,21 @@ export function WithdrawalQRCode({
withdrawUri={withdrawUri} withdrawUri={withdrawUri}
onAborted={() => { onAborted={() => {
notifyInfo(i18n.str`Operation canceled`); notifyInfo(i18n.str`Operation canceled`);
onAborted(); clearCurrentWithdrawal()
}} onContinue()
}}
/> />
); );
} }
// Wallet POSTed the withdrawal details! Ask the
// user to authorize the operation (here CAPTCHA).
return ( return (
<WithdrawalConfirmationQuestion <WithdrawalConfirmationQuestion
withdrawUri={withdrawUri} withdrawUri={withdrawUri}
onConfirmed={() => {
notifyInfo(i18n.str`Operation confirmed`);
onConfirmed();
}}
onAborted={() => { onAborted={() => {
notifyInfo(i18n.str`Operation canceled`); notifyInfo(i18n.str`Operation canceled`);
onAborted(); clearCurrentWithdrawal()
}} onContinue()
}}
/> />
); );
} }

View File

@ -315,3 +315,39 @@ h1.nav {
margin-bottom: 1em; margin-bottom: 1em;
} }
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid black;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: black transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View File

@ -134,7 +134,7 @@ export function buildRequestErrorMessage(
specialCases.onClientError && specialCases.onClientError(cause.status); specialCases.onClientError && specialCases.onClientError(cause.status);
result = { result = {
title: title ? title : i18n.str`The server didn't accept the request`, title: title ? title : i18n.str`The server didn't accept the request`,
description: cause.payload.error.description, description: cause?.payload?.error?.description,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;
@ -146,7 +146,7 @@ export function buildRequestErrorMessage(
title: title title: title
? title ? title
: i18n.str`The server had problems processing the request`, : i18n.str`The server had problems processing the request`,
description: cause.payload.error.description, description: cause?.payload?.error?.description,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;
@ -154,7 +154,7 @@ export function buildRequestErrorMessage(
case ErrorType.UNREADABLE: { case ErrorType.UNREADABLE: {
result = { result = {
title: i18n.str`Unexpected error`, title: i18n.str`Unexpected error`,
description: `Response from ${cause.info?.url} is unreadable, status: ${cause.status}`, description: `Response from ${cause?.info?.url} is unreadable, status: ${cause?.status}`,
debug: JSON.stringify(cause), debug: JSON.stringify(cause),
}; };
break; break;

View File

@ -26,7 +26,7 @@ import {
} from "@gnu-taler/web-util/browser"; } from "@gnu-taler/web-util/browser";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { route } from "preact-router"; import { route } from "preact-router";
import { useMemo } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js";
import { Loading } from "./components/exception/loading.js"; import { Loading } from "./components/exception/loading.js";
import { import {
@ -42,6 +42,7 @@ import { useBackendConfig } from "./hooks/backend.js";
import { strings } from "./i18n/strings.js"; import { strings } from "./i18n/strings.js";
import LoginPage from "./paths/login/index.js"; import LoginPage from "./paths/login/index.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { Settings } from "./paths/settings/index.js";
export function Application(): VNode { export function Application(): VNode {
return ( return (
@ -70,10 +71,19 @@ function ApplicationStatusRoutes(): VNode {
: { currency: "unknown", version: "unknown" }; : { currency: "unknown", version: "unknown" };
const ctx = useMemo(() => ({ currency, version }), [currency, version]); const ctx = useMemo(() => ({ currency, version }), [currency, version]);
const [showSettings, setShowSettings] = useState(false)
if (showSettings) {
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" />
<Settings />
</Fragment>
}
if (!triedToLog) { if (!triedToLog) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Welcome!" /> <NotYetReadyAppMenu title="Welcome!" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -87,7 +97,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Login" /> <NotYetReadyAppMenu title="Login" onShowSettings={() => setShowSettings(true)} />
<LoginPage onConfirm={updateLoginInfoAndGoToRoot} /> <LoginPage onConfirm={updateLoginInfoAndGoToRoot} />
</Fragment> </Fragment>
); );
@ -98,7 +108,7 @@ function ApplicationStatusRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server not found`, message: i18n.str`Server not found`,
@ -112,7 +122,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.SERVER) { if (result.type === ErrorType.SERVER) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Server response with an error code`, message: i18n.str`Server response with an error code`,
@ -125,7 +135,7 @@ function ApplicationStatusRoutes(): VNode {
} }
if (result.type === ErrorType.UNREADABLE) { if (result.type === ErrorType.UNREADABLE) {
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Response from server is unreadable, http status: ${result.status}`, message: i18n.str`Response from server is unreadable, http status: ${result.status}`,
@ -138,7 +148,7 @@ function ApplicationStatusRoutes(): VNode {
} }
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" /> <NotYetReadyAppMenu title="Error" onShowSettings={() => setShowSettings(true)} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Unexpected Error`, message: i18n.str`Unexpected Error`,

View File

@ -33,6 +33,7 @@ import { InstanceRoutes } from "./InstanceRoutes.js";
import LoginPage from "./paths/login/index.js"; import LoginPage from "./paths/login/index.js";
import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { INSTANCE_ID_LOOKUP } from "./utils/constants.js";
import { HttpStatusCode } from "@gnu-taler/taler-util"; import { HttpStatusCode } from "@gnu-taler/taler-util";
import { Settings } from "./paths/settings/index.js";
export function ApplicationReadyRoutes(): VNode { export function ApplicationReadyRoutes(): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -48,8 +49,15 @@ export function ApplicationReadyRoutes(): VNode {
clearAllTokens(); clearAllTokens();
route("/"); route("/");
}; };
const [showSettings, setShowSettings] = useState(false)
if (result.loading) return <NotYetReadyAppMenu title="Loading..." />; if (showSettings) {
return <Fragment>
<NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} />
<Settings/>
</Fragment>
}
if (result.loading) return <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Loading..." />;
let admin = true; let admin = true;
let instanceNameByBackendURL; let instanceNameByBackendURL;
@ -61,7 +69,7 @@ export function ApplicationReadyRoutes(): VNode {
) { ) {
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Login" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Access denied`, message: i18n.str`Access denied`,
@ -81,7 +89,7 @@ export function ApplicationReadyRoutes(): VNode {
// does not match our pattern // does not match our pattern
return ( return (
<Fragment> <Fragment>
<NotYetReadyAppMenu title="Error" onLogout={clearTokenAndGoToRoot} /> <NotYetReadyAppMenu onShowSettings={() => setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} />
<NotificationCard <NotificationCard
notification={{ notification={{
message: i18n.str`Couldn't access the server.`, message: i18n.str`Couldn't access the server.`,

View File

@ -68,6 +68,7 @@ import LoginPage from "./paths/login/index.js";
import NotFoundPage from "./paths/notfound/index.js"; import NotFoundPage from "./paths/notfound/index.js";
import { Notification } from "./utils/types.js"; import { Notification } from "./utils/types.js";
import { MerchantBackend } from "./declaration.js"; import { MerchantBackend } from "./declaration.js";
import { Settings } from "./paths/settings/index.js";
export enum InstancePaths { export enum InstancePaths {
// details = '/', // details = '/',
@ -100,6 +101,8 @@ export enum InstancePaths {
webhooks_list = "/webhooks", webhooks_list = "/webhooks",
webhooks_update = "/webhooks/:tid/update", webhooks_update = "/webhooks/:tid/update",
webhooks_new = "/webhooks/new", webhooks_new = "/webhooks/new",
settings = "/settings",
} }
// eslint-disable-next-line @typescript-eslint/no-empty-function // eslint-disable-next-line @typescript-eslint/no-empty-function
@ -240,6 +243,9 @@ export function InstanceRoutes({
<Menu <Menu
instance={id} instance={id}
admin={admin} admin={admin}
onShowSettings={() => {
route("/settings")
}}
path={path} path={path}
onLogout={clearTokenAndGoToRoot} onLogout={clearTokenAndGoToRoot}
setInstanceName={setInstanceName} setInstanceName={setInstanceName}
@ -558,6 +564,7 @@ export function InstanceRoutes({
}} }}
/> />
<Route path={InstancePaths.kyc} component={ListKYCPage} /> <Route path={InstancePaths.kyc} component={ListKYCPage} />
<Route path={InstancePaths.settings} component={Settings} />
{/** {/**
* Example pages * Example pages
*/} */}

View File

@ -20,7 +20,7 @@
*/ */
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact"; import { ComponentChildren, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { useBackendContext } from "../../context/backend.js"; import { useBackendContext } from "../../context/backend.js";
import { useInstanceContext } from "../../context/instance.js"; import { useInstanceContext } from "../../context/instance.js";
@ -40,7 +40,7 @@ function getTokenValuePart(t: string): string {
} }
function normalizeToken(r: string): string { function normalizeToken(r: string): string {
return `secret-token:${encodeURIComponent(r)}`; return `secret-token:${r}`;
} }
function cleanUp(s: string): string { function cleanUp(s: string): string {
@ -53,7 +53,7 @@ function cleanUp(s: string): string {
export function LoginModal({ onConfirm, withMessage }: Props): VNode { export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const { url: backendUrl, token: baseToken } = useBackendContext(); const { url: backendUrl, token: baseToken } = useBackendContext();
const { admin, token: instanceToken } = useInstanceContext(); const { admin, token: instanceToken, id } = useInstanceContext();
const testLogin = useCredentialsChecker(); const testLogin = useCredentialsChecker();
const currentToken = getTokenValuePart( const currentToken = getTokenValuePart(
(!admin ? baseToken : instanceToken) ?? "", (!admin ? baseToken : instanceToken) ?? "",
@ -63,6 +63,78 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
const [url, setURL] = useState(cleanUp(backendUrl)); const [url, setURL] = useState(cleanUp(backendUrl));
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
if (admin && id !== "default") {
//admin trying to access another instance
return (<div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds ">
<div class="modal-card" style={{ width: "100%", margin: 0 }}>
<header
class="modal-card-head"
style={{ border: "1px solid", borderBottom: 0 }}
>
<p class="modal-card-title">{i18n.str`Login required`}</p>
</header>
<section
class="modal-card-body"
style={{ border: "1px solid", borderTop: 0, borderBottom: 0 }}
>
<p>
<i18n.Translate>Need the access token for the instance.</i18n.Translate>
</p>
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label">
<i18n.Translate>Access Token</i18n.Translate>
</label>
</div>
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="password"
placeholder={"set new access token"}
name="token"
onKeyPress={(e) =>
e.keyCode === 13
? onConfirm(url, normalizeToken(token))
: null
}
value={token}
onInput={(e): void => setToken(e?.currentTarget.value)}
/>
</p>
</div>
</div>
</div>
</section>
<footer
class="modal-card-foot "
style={{
justifyContent: "flex-end",
border: "1px solid",
borderTop: 0,
}}
>
<AsyncButton
onClick={async () => {
const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(`${url}/instances/${id}`, secretToken);
if (valid) {
onConfirm(url, secretToken);
} else {
onConfirm(url);
}
}}
>
<i18n.Translate>Confirm</i18n.Translate>
</AsyncButton>
</footer>
</div>
</div>
</div>)
}
return ( return (
<div class="columns is-centered" style={{ margin: "auto" }}> <div class="columns is-centered" style={{ margin: "auto" }}>
<div class="column is-two-thirds "> <div class="column is-two-thirds ">
@ -137,8 +209,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
borderTop: 0, borderTop: 0,
}} }}
> >
<button <AsyncButton
class="button is-info"
onClick={async () => { onClick={async () => {
const secretToken = normalizeToken(token); const secretToken = normalizeToken(token);
const { valid, cause } = await testLogin(url, secretToken); const { valid, cause } = await testLogin(url, secretToken);
@ -150,10 +221,24 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode {
}} }}
> >
<i18n.Translate>Confirm</i18n.Translate> <i18n.Translate>Confirm</i18n.Translate>
</button> </AsyncButton>
</footer> </footer>
</div> </div>
</div> </div>
</div> </div>
); );
} }
function AsyncButton({ onClick, children }: { onClick: () => Promise<void>, children: ComponentChildren }): VNode {
const [running, setRunning] = useState(false)
return <button class="button is-info" disabled={running} onClick={() => {
setRunning(true)
onClick().then(() => {
setRunning(false)
}).catch(() => {
setRunning(false)
})
}}>
{children}
</button>
}

View File

@ -0,0 +1,91 @@
/*
This file is part of GNU Taler
(C) 2021-2023 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 <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, VNode } from "preact";
import { InputProps, useField } from "./useField.js";
interface Props<T> extends InputProps<T> {
name: T;
readonly?: boolean;
expand?: boolean;
threeState?: boolean;
toBoolean?: (v?: any) => boolean | undefined;
fromBoolean?: (s: boolean | undefined) => any;
}
const defaultToBoolean = (f?: any): boolean | undefined => f || "";
const defaultFromBoolean = (v: boolean | undefined): any => v as any;
export function InputToggle<T>({
name,
readonly,
placeholder,
tooltip,
label,
help,
threeState,
expand,
fromBoolean = defaultFromBoolean,
toBoolean = defaultToBoolean,
}: Props<keyof T>): VNode {
const { error, value, onChange } = useField<T>(name);
const onCheckboxClick = (): void => {
const c = toBoolean(value);
if (c === false && threeState) return onChange(undefined as any);
return onChange(fromBoolean(!c));
};
return (
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" style={{ width: 200 }}>
{label}
{tooltip && (
<span class="icon has-tooltip-right" data-tooltip={tooltip}>
<i class="mdi mdi-information" />
</span>
)}
</label>
</div>
<div class="field-body is-flex-grow-1">
<div class="field">
<p class={expand ? "control is-expanded" : "control"}>
<label class="toggle" style={{ marginLeft: 4, marginTop: 0 }}>
<input
type="checkbox"
class={toBoolean(value) === undefined ? "is-indeterminate" : "toggle-checkbox"}
checked={toBoolean(value)}
placeholder={placeholder}
readonly={readonly}
name={String(name)}
disabled={readonly}
onChange={onCheckboxClick}
/>
<div class="toggle-switch"></div>
</label>
{help}
</p>
{error && <p class="help is-danger">{error}</p>}
</div>
</div>
</div>
);
}

View File

@ -20,7 +20,6 @@
*/ */
import { h, VNode } from "preact"; import { h, VNode } from "preact";
import { LangSelector } from "./LangSelector.js";
import logo from "../../assets/logo-2021.svg"; import logo from "../../assets/logo-2021.svg";
interface Props { interface Props {
@ -65,7 +64,6 @@ export function NavigationBar({ onMobileMenu, title }: Props): VNode {
</a> </a>
<div class="navbar-end"> <div class="navbar-end">
<div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}> <div class="navbar-item" style={{ paddingTop: 4, paddingBottom: 4 }}>
<LangSelector />
</div> </div>
</div> </div>
</div> </div>

View File

@ -31,6 +31,7 @@ const VERSION = typeof __VERSION__ !== "undefined" ? __VERSION__ : undefined;
interface Props { interface Props {
onLogout: () => void; onLogout: () => void;
onShowSettings: () => void;
mobile?: boolean; mobile?: boolean;
instance: string; instance: string;
admin?: boolean; admin?: boolean;
@ -40,6 +41,7 @@ interface Props {
export function Sidebar({ export function Sidebar({
mobile, mobile,
instance, instance,
onShowSettings,
onLogout, onLogout,
admin, admin,
mimic, mimic,
@ -78,21 +80,8 @@ export function Sidebar({
<div class="menu is-menu-main"> <div class="menu is-menu-main">
{instance ? ( {instance ? (
<Fragment> <Fragment>
<p class="menu-label">
<i18n.Translate>Instance</i18n.Translate>
</p>
<ul class="menu-list"> <ul class="menu-list">
<li> <li>
<a href={"/update"} class="has-icon">
<span class="icon">
<i class="mdi mdi-square-edit-outline" />
</span>
<span class="menu-item-label">
<i18n.Translate>Settings</i18n.Translate>
</span>
</a>
</li>
<li>
<a href={"/orders"} class="has-icon"> <a href={"/orders"} class="has-icon">
<span class="icon"> <span class="icon">
<i class="mdi mdi-cash-register" /> <i class="mdi mdi-cash-register" />
@ -132,6 +121,31 @@ export function Sidebar({
</span> </span>
</a> </a>
</li> </li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
<span class="icon">
<i class="mdi mdi-account-check" />
</span>
<span class="menu-item-label">KYC Status</span>
</a>
</li>
)}
</ul>
<p class="menu-label">
<i18n.Translate>Configuration</i18n.Translate>
</p>
<ul class="menu-list">
<li>
<a href={"/update"} class="has-icon">
<span class="icon">
<i class="mdi mdi-square-edit-outline" />
</span>
<span class="menu-item-label">
<i18n.Translate>Account</i18n.Translate>
</span>
</a>
</li>
<li> <li>
<a href={"/reserves"} class="has-icon"> <a href={"/reserves"} class="has-icon">
<span class="icon"> <span class="icon">
@ -150,16 +164,6 @@ export function Sidebar({
</span> </span>
</a> </a>
</li> </li>
{needKYC && (
<li>
<a href={"/kyc"} class="has-icon">
<span class="icon">
<i class="mdi mdi-account-check" />
</span>
<span class="menu-item-label">KYC Status</span>
</a>
</li>
)}
</ul> </ul>
</Fragment> </Fragment>
) : undefined} ) : undefined}
@ -167,6 +171,18 @@ export function Sidebar({
<i18n.Translate>Connection</i18n.Translate> <i18n.Translate>Connection</i18n.Translate>
</p> </p>
<ul class="menu-list"> <ul class="menu-list">
<li>
<a class="has-icon is-state-info is-hoverable"
onClick={(): void => onShowSettings()}
>
<span class="icon">
<i class="mdi mdi-newspaper" />
</span>
<span class="menu-item-label">
<i18n.Translate>Settings</i18n.Translate>
</span>
</a>
</li>
<li> <li>
<div> <div>
<span style={{ width: "3rem" }} class="icon"> <span style={{ width: "3rem" }} class="icon">

View File

@ -75,6 +75,7 @@ interface MenuProps {
instance: string; instance: string;
admin?: boolean; admin?: boolean;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
setInstanceName: (s: string) => void; setInstanceName: (s: string) => void;
} }
@ -93,6 +94,7 @@ function WithTitle({
export function Menu({ export function Menu({
onLogout, onLogout,
onShowSettings,
title, title,
instance, instance,
path, path,
@ -121,6 +123,7 @@ export function Menu({
{onLogout && ( {onLogout && (
<Sidebar <Sidebar
onShowSettings={onShowSettings}
onLogout={onLogout} onLogout={onLogout}
admin={admin} admin={admin}
mimic={mimic} mimic={mimic}
@ -130,7 +133,12 @@ export function Menu({
)} )}
{mimic && ( {mimic && (
<nav class="level"> <nav class="level" style={{
zIndex: 100,
position:"fixed",
width:"50%",
marginLeft: "20%"
}}>
<div class="level-item has-text-centered has-background-warning"> <div class="level-item has-text-centered has-background-warning">
<p class="is-size-5"> <p class="is-size-5">
You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "} You are viewing the instance <b>&quot;{instance}&quot;</b>.{" "}
@ -154,6 +162,7 @@ export function Menu({
interface NotYetReadyAppMenuProps { interface NotYetReadyAppMenuProps {
title: string; title: string;
onLogout?: () => void; onLogout?: () => void;
onShowSettings: () => void;
} }
interface NotifProps { interface NotifProps {
@ -194,6 +203,7 @@ export function NotificationCard({
export function NotYetReadyAppMenu({ export function NotYetReadyAppMenu({
onLogout, onLogout,
onShowSettings,
title, title,
}: NotYetReadyAppMenuProps): VNode { }: NotYetReadyAppMenuProps): VNode {
const [mobileOpen, setMobileOpen] = useState(false); const [mobileOpen, setMobileOpen] = useState(false);
@ -212,7 +222,7 @@ export function NotYetReadyAppMenu({
title={title} title={title}
/> />
{onLogout && ( {onLogout && (
<Sidebar onLogout={onLogout} instance="" mobile={mobileOpen} /> <Sidebar onShowSettings={onShowSettings} onLogout={onLogout} instance="" mobile={mobileOpen} />
)} )}
</div> </div>
); );

View File

@ -239,16 +239,16 @@ export function useBackendInstanceRequest(): useBackendInstanceRequestType {
searchDate?: Date, searchDate?: Date,
delta?: number, delta?: number,
): Promise<HttpResponseOk<T>> { ): Promise<HttpResponseOk<T>> {
const date_ms = const date_s =
delta && delta < 0 && searchDate delta && delta < 0 && searchDate
? searchDate.getTime() + 1 ? (searchDate.getTime() / 1000) + 1
: searchDate?.getTime(); : searchDate !== undefined ? (searchDate.getTime() / 1000) : undefined;
const params: any = {}; const params: any = {};
if (paid !== undefined) params.paid = paid; if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta; if (delta !== undefined) params.delta = delta;
if (refunded !== undefined) params.refunded = refunded; if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired; if (wired !== undefined) params.wired = wired;
if (date_ms !== undefined) params.date_ms = date_ms; if (date_s !== undefined) params.date_s = date_s;
return requestHandler<T>(baseUrl, endpoint, { params, token }); return requestHandler<T>(baseUrl, endpoint, { params, token });
}, },
[baseUrl, token], [baseUrl, token],

View File

@ -21,6 +21,7 @@
import { StateUpdater, useCallback, useState } from "preact/hooks"; import { StateUpdater, useCallback, useState } from "preact/hooks";
import { ValueOrFunction } from "../utils/types.js"; import { ValueOrFunction } from "../utils/types.js";
import { useMemoryStorage } from "@gnu-taler/web-util/browser";
const calculateRootPath = () => { const calculateRootPath = () => {
const rootPath = const rootPath =
@ -52,14 +53,17 @@ export function useBackendURL(
export function useBackendDefaultToken( export function useBackendDefaultToken(
initialValue?: string, initialValue?: string,
): [string | undefined, StateUpdater<string | undefined>] { ): [string | undefined, ((d: string | undefined) => void)] {
return useLocalStorage("backend-token", initialValue); // uncomment for testing
initialValue = "secret-token:secret" as string | undefined
const { update, value } = useMemoryStorage(`backend-token`, initialValue)
return [value, update];
} }
export function useBackendInstanceToken( export function useBackendInstanceToken(
id: string, id: string,
): [string | undefined, StateUpdater<string | undefined>] { ): [string | undefined, ((d: string | undefined) => void)] {
const [token, setToken] = useLocalStorage(`backend-token-${id}`); const { update: setToken, value: token, reset } = useMemoryStorage(`backend-token-${id}`)
const [defaultToken, defaultSetToken] = useBackendDefaultToken(); const [defaultToken, defaultSetToken] = useBackendDefaultToken();
// instance named 'default' use the default token // instance named 'default' use the default token
@ -67,15 +71,16 @@ export function useBackendInstanceToken(
return [defaultToken, defaultSetToken]; return [defaultToken, defaultSetToken];
} }
function updateToken( function updateToken(
value: value: (string | undefined)
| (string | undefined)
| ((s: string | undefined) => string | undefined),
): void { ): void {
setToken((p) => { console.log("seeting token", value)
const toStore = value instanceof Function ? value(p) : value; if (value === undefined) {
return toStore; reset()
}); } else {
setToken(value)
}
} }
console.log("token", token)
return [token, updateToken]; return [token, updateToken];
} }

View File

@ -0,0 +1,59 @@
/*
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 <http://www.gnu.org/licenses/>
*/
import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
import {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
if (str === undefined) return undefined;
try {
return JSON.parse(str);
} catch {
return undefined;
}
}
export interface Settings {
advanceOrderMode: boolean
}
const defaultSettings: Settings = {
advanceOrderMode: false,
}
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
.property("advanceOrderMode", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("merchant-settings", codecForSettings());
export function useSettings(): [
Readonly<Settings>,
<T extends keyof Settings>(key: T, value: Settings[T]) => void,
] {
const { value, update } = useLocalStorage(SETTINGS_KEY);
const parsed: Settings = value ?? defaultSettings;
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
update({ ...parsed, [k]: v });
}
return [parsed, updateField];
}

View File

@ -12,20 +12,21 @@
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
# #
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: Stefan Kügel <skuegel@web.de>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: German <https://weblate.taler.net/projects/gnu-taler/"
"Language: \n" "merchant-backoffice/de/>\n"
"Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71 #: src/components/modal/index.tsx:71
#, c-format #, c-format
@ -1252,7 +1253,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "" msgstr "Rückerstattet"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format

View File

@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-04-24 06:43+0000\n" "PO-Revision-Date: 2023-08-13 10:14+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"merchant-backoffice/es/>\n" "merchant-backoffice/es/>\n"
"Language: es\n" "Language: es\n"
@ -1273,7 +1273,7 @@ msgstr "No se pudo create el reembolso"
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "Reembolzado" msgstr "Reembolsado"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format

View File

@ -12,20 +12,21 @@
# You should have received a copy of the GNU General Public License along with # You should have received a copy of the GNU General Public License along with
# TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> # TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
# #
#, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: taler@gnu.org\n" "Report-Msgid-Bugs-To: taler@gnu.org\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"Language: \n" "merchant-backoffice/it/>\n"
"Language: it\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 4.13.1\n"
#: src/components/modal/index.tsx:71 #: src/components/modal/index.tsx:71
#, c-format #, c-format
@ -439,7 +440,7 @@ msgstr ""
#: src/components/form/InputTaxes.tsx:119 #: src/components/form/InputTaxes.tsx:119
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "" msgstr "Importo"
#: src/components/form/InputTaxes.tsx:120 #: src/components/form/InputTaxes.tsx:120
#, c-format #, c-format
@ -887,7 +888,7 @@ msgstr ""
#: src/paths/instance/orders/list/Table.tsx:154 #: src/paths/instance/orders/list/Table.tsx:154
#, c-format #, c-format
msgid "Date" msgid "Date"
msgstr "" msgstr "Data"
#: src/paths/instance/orders/list/Table.tsx:200 #: src/paths/instance/orders/list/Table.tsx:200
#, c-format #, c-format
@ -1252,7 +1253,7 @@ msgstr ""
#: src/paths/instance/orders/list/ListPage.tsx:145 #: src/paths/instance/orders/list/ListPage.tsx:145
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "" msgstr "Rimborsato"
#: src/paths/instance/orders/list/ListPage.tsx:152 #: src/paths/instance/orders/list/ListPage.tsx:152
#, c-format #, c-format
@ -1633,7 +1634,7 @@ msgstr ""
#: src/paths/instance/reserves/details/DetailPage.tsx:119 #: src/paths/instance/reserves/details/DetailPage.tsx:119
#, c-format #, c-format
msgid "Subject" msgid "Subject"
msgstr "" msgstr "Soggetto"
#: src/paths/instance/reserves/details/DetailPage.tsx:130 #: src/paths/instance/reserves/details/DetailPage.tsx:130
#, c-format #, c-format

View File

@ -43,6 +43,7 @@ import { Duration, MerchantBackend, WithId } from "../../../../declaration.js";
import { OrderCreateSchema as schema } from "../../../../schemas/index.js"; import { OrderCreateSchema as schema } from "../../../../schemas/index.js";
import { rate } from "../../../../utils/amount.js"; import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js"; import { undefinedIfEmpty } from "../../../../utils/table.js";
import { useSettings } from "../../../../hooks/useSettings.js";
interface Props { interface Props {
onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void; onCreate: (d: MerchantBackend.Orders.PostOrderRequest) => void;
@ -62,8 +63,8 @@ function with_defaults(config: InstanceConfig): Partial<Entity> {
!config.default_pay_delay || config.default_pay_delay.d_us === "forever" !config.default_pay_delay || config.default_pay_delay.d_us === "forever"
? undefined ? undefined
: add(new Date(), { : add(new Date(), {
seconds: config.default_pay_delay.d_us / (1000 * 1000), seconds: config.default_pay_delay.d_us / (1000 * 1000),
}); });
return { return {
inventoryProducts: {}, inventoryProducts: {},
@ -138,7 +139,7 @@ export function CreatePage({
const [value, valueHandler] = useState(with_defaults(instanceConfig)); const [value, valueHandler] = useState(with_defaults(instanceConfig));
const config = useConfigContext(); const config = useConfigContext();
const zero = Amounts.zeroOfCurrency(config.currency); const zero = Amounts.zeroOfCurrency(config.currency);
const [settings] = useSettings()
const inventoryList = Object.values(value.inventoryProducts || {}); const inventoryList = Object.values(value.inventoryProducts || {});
const productList = Object.values(value.products || {}); const productList = Object.values(value.products || {});
@ -154,10 +155,10 @@ export function CreatePage({
order_price: !value.pricing?.order_price order_price: !value.pricing?.order_price
? i18n.str`required` ? i18n.str`required`
: !parsedPrice : !parsedPrice
? i18n.str`not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedPrice) : Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0` ? i18n.str`must be greater than 0`
: undefined, : undefined,
}), }),
extra: extra:
value.extra && !stringIsValidJSON(value.extra) value.extra && !stringIsValidJSON(value.extra)
@ -167,47 +168,47 @@ export function CreatePage({
refund_deadline: !value.payments?.refund_deadline refund_deadline: !value.payments?.refund_deadline
? undefined ? undefined
: !isFuture(value.payments.refund_deadline) : !isFuture(value.payments.refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.pay_deadline && : value.payments.pay_deadline &&
isBefore(value.payments.refund_deadline, value.payments.pay_deadline) isBefore(value.payments.refund_deadline, value.payments.pay_deadline)
? i18n.str`refund deadline cannot be before pay deadline` ? i18n.str`refund deadline cannot be before pay deadline`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.refund_deadline, value.payments.refund_deadline,
) )
? i18n.str`wire transfer deadline cannot be before refund deadline` ? i18n.str`wire transfer deadline cannot be before refund deadline`
: undefined, : undefined,
pay_deadline: !value.payments?.pay_deadline pay_deadline: !value.payments?.pay_deadline
? undefined ? undefined
: !isFuture(value.payments.pay_deadline) : !isFuture(value.payments.pay_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: value.payments.wire_transfer_deadline && : value.payments.wire_transfer_deadline &&
isBefore( isBefore(
value.payments.wire_transfer_deadline, value.payments.wire_transfer_deadline,
value.payments.pay_deadline, value.payments.pay_deadline,
) )
? i18n.str`wire transfer deadline cannot be before pay deadline` ? i18n.str`wire transfer deadline cannot be before pay deadline`
: undefined, : undefined,
auto_refund_deadline: !value.payments?.auto_refund_deadline auto_refund_deadline: !value.payments?.auto_refund_deadline
? undefined ? undefined
: !isFuture(value.payments.auto_refund_deadline) : !isFuture(value.payments.auto_refund_deadline)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: !value.payments?.refund_deadline : !value.payments?.refund_deadline
? i18n.str`should have a refund deadline` ? i18n.str`should have a refund deadline`
: !isAfter( : !isAfter(
value.payments.refund_deadline, value.payments.refund_deadline,
value.payments.auto_refund_deadline, value.payments.auto_refund_deadline,
) )
? i18n.str`auto refund cannot be after refund deadline` ? i18n.str`auto refund cannot be after refund deadline`
: undefined, : undefined,
}), }),
shipping: undefinedIfEmpty({ shipping: undefinedIfEmpty({
delivery_date: !value.shipping?.delivery_date delivery_date: !value.shipping?.delivery_date
? undefined ? undefined
: !isFuture(value.shipping.delivery_date) : !isFuture(value.shipping.delivery_date)
? i18n.str`should be in the future` ? i18n.str`should be in the future`
: undefined, : undefined,
}), }),
}; };
const hasErrors = Object.keys(errors).some( const hasErrors = Object.keys(errors).some(
@ -227,27 +228,27 @@ export function CreatePage({
extra: value.extra, extra: value.extra,
pay_deadline: value.payments.pay_deadline pay_deadline: value.payments.pay_deadline
? { ? {
t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000), t_s: Math.floor(value.payments.pay_deadline.getTime() / 1000),
} }
: undefined, : undefined,
wire_transfer_deadline: value.payments.wire_transfer_deadline wire_transfer_deadline: value.payments.wire_transfer_deadline
? { ? {
t_s: Math.floor( t_s: Math.floor(
value.payments.wire_transfer_deadline.getTime() / 1000, value.payments.wire_transfer_deadline.getTime() / 1000,
), ),
} }
: undefined, : undefined,
refund_deadline: value.payments.refund_deadline refund_deadline: value.payments.refund_deadline
? { ? {
t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000), t_s: Math.floor(value.payments.refund_deadline.getTime() / 1000),
} }
: undefined, : undefined,
auto_refund: value.payments.auto_refund_deadline auto_refund: value.payments.auto_refund_deadline
? { ? {
d_us: Math.floor( d_us: Math.floor(
value.payments.auto_refund_deadline.getTime() * 1000, value.payments.auto_refund_deadline.getTime() * 1000,
), ),
} }
: undefined, : undefined,
wire_fee_amortization: value.payments.wire_fee_amortization as number, wire_fee_amortization: value.payments.wire_fee_amortization as number,
max_fee: value.payments.max_fee as string, max_fee: value.payments.max_fee as string,
@ -374,13 +375,15 @@ export function CreatePage({
inventory={instanceInventory} inventory={instanceInventory}
/> />
<NonInventoryProductFrom {settings.advanceOrderMode &&
productToEdit={editingProduct} <NonInventoryProductFrom
onAddProduct={(p) => { productToEdit={editingProduct}
setEditingProduct(undefined); onAddProduct={(p) => {
return addNewProduct(p); setEditingProduct(undefined);
}} return addNewProduct(p);
/> }}
/>
}
{allProducts.length > 0 && ( {allProducts.length > 0 && (
<ProductList <ProductList
@ -423,8 +426,8 @@ export function CreatePage({
discountOrRise > 0 && discountOrRise > 0 &&
(discountOrRise < 1 (discountOrRise < 1
? `discount of %${Math.round( ? `discount of %${Math.round(
(1 - discountOrRise) * 100, (1 - discountOrRise) * 100,
)}` )}`
: `rise of %${Math.round((discountOrRise - 1) * 100)}`) : `rise of %${Math.round((discountOrRise - 1) * 100)}`)
} }
tooltip={i18n.str`Amount to be paid by the customer`} tooltip={i18n.str`Amount to be paid by the customer`}
@ -445,102 +448,108 @@ export function CreatePage({
tooltip={i18n.str`Title of the order to be shown to the customer`} tooltip={i18n.str`Title of the order to be shown to the customer`}
/> />
<InputGroup {settings.advanceOrderMode &&
name="shipping" <InputGroup
label={i18n.str`Shipping and Fulfillment`} name="shipping"
initialActive label={i18n.str`Shipping and Fulfillment`}
> initialActive
<InputDate >
name="shipping.delivery_date" <InputDate
label={i18n.str`Delivery date`} name="shipping.delivery_date"
tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`} label={i18n.str`Delivery date`}
/> tooltip={i18n.str`Deadline for physical delivery assured by the merchant.`}
{value.shipping?.delivery_date && ( />
<InputGroup {value.shipping?.delivery_date && (
name="shipping.delivery_location" <InputGroup
label={i18n.str`Location`} name="shipping.delivery_location"
tooltip={i18n.str`address where the products will be delivered`} label={i18n.str`Location`}
> tooltip={i18n.str`address where the products will be delivered`}
<InputLocation name="shipping.delivery_location" /> >
</InputGroup> <InputLocation name="shipping.delivery_location" />
)} </InputGroup>
<Input )}
name="shipping.fullfilment_url" <Input
label={i18n.str`Fulfillment URL`} name="shipping.fullfilment_url"
tooltip={i18n.str`URL to which the user will be redirected after successful payment.`} label={i18n.str`Fulfillment URL`}
/> tooltip={i18n.str`URL to which the user will be redirected after successful payment.`}
</InputGroup> />
</InputGroup>
}
<InputGroup {settings.advanceOrderMode &&
name="payments" <InputGroup
label={i18n.str`Taler payment options`} name="payments"
tooltip={i18n.str`Override default Taler payment settings for this order`} label={i18n.str`Taler payment options`}
> tooltip={i18n.str`Override default Taler payment settings for this order`}
<InputDate >
name="payments.pay_deadline" <InputDate
label={i18n.str`Payment deadline`} name="payments.pay_deadline"
tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`} label={i18n.str`Payment deadline`}
/> tooltip={i18n.str`Deadline for the customer to pay for the offer before it expires. Inventory products will be reserved until this deadline.`}
<InputDate />
name="payments.refund_deadline" <InputDate
label={i18n.str`Refund deadline`} name="payments.refund_deadline"
tooltip={i18n.str`Time until which the order can be refunded by the merchant.`} label={i18n.str`Refund deadline`}
/> tooltip={i18n.str`Time until which the order can be refunded by the merchant.`}
<InputDate />
name="payments.wire_transfer_deadline" <InputDate
label={i18n.str`Wire transfer deadline`} name="payments.wire_transfer_deadline"
tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`} label={i18n.str`Wire transfer deadline`}
/> tooltip={i18n.str`Deadline for the exchange to make the wire transfer.`}
<InputDate />
name="payments.auto_refund_deadline" <InputDate
label={i18n.str`Auto-refund deadline`} name="payments.auto_refund_deadline"
tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} label={i18n.str`Auto-refund deadline`}
/> tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`}
/>
<InputCurrency <InputCurrency
name="payments.max_fee" name="payments.max_fee"
label={i18n.str`Maximum deposit fee`} label={i18n.str`Maximum deposit fee`}
tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`} tooltip={i18n.str`Maximum deposit fees the merchant is willing to cover for this order. Higher deposit fees must be covered in full by the consumer.`}
/> />
<InputCurrency <InputCurrency
name="payments.max_wire_fee" name="payments.max_wire_fee"
label={i18n.str`Maximum wire fee`} label={i18n.str`Maximum wire fee`}
tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`} tooltip={i18n.str`Maximum aggregate wire fees the merchant is willing to cover for this order. Wire fees exceeding this amount are to be covered by the customers.`}
/> />
<InputNumber <InputNumber
name="payments.wire_fee_amortization" name="payments.wire_fee_amortization"
label={i18n.str`Wire fee amortization`} label={i18n.str`Wire fee amortization`}
tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`} tooltip={i18n.str`Factor by which wire fees exceeding the above threshold are divided to determine the share of excess wire fees to be paid explicitly by the consumer.`}
/> />
<InputBoolean <InputBoolean
name="payments.createToken" name="payments.createToken"
label={i18n.str`Create token`} label={i18n.str`Create token`}
tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`} tooltip={i18n.str`Uncheck this option if the merchant backend generated an order ID with enough entropy to prevent adversarial claims.`}
/> />
<InputNumber <InputNumber
name="payments.minimum_age" name="payments.minimum_age"
label={i18n.str`Minimum age required`} label={i18n.str`Minimum age required`}
tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`} tooltip={i18n.str`Any value greater than 0 will limit the coins able be used to pay this contract. If empty the age restriction will be defined by the products`}
help={ help={
minAgeByProducts > 0 minAgeByProducts > 0
? i18n.str`Min age defined by the producs is ${minAgeByProducts}` ? i18n.str`Min age defined by the producs is ${minAgeByProducts}`
: undefined : undefined
} }
/> />
</InputGroup> </InputGroup>
}
<InputGroup {settings.advanceOrderMode &&
name="extra" <InputGroup
label={i18n.str`Additional information`}
tooltip={i18n.str`Custom information to be included in the contract for this order.`}
>
<Input
name="extra" name="extra"
inputType="multiline" label={i18n.str`Additional information`}
label={`Value`} tooltip={i18n.str`Custom information to be included in the contract for this order.`}
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`} >
/> <Input
</InputGroup> name="extra"
inputType="multiline"
label={`Value`}
tooltip={i18n.str`You must enter a value in JavaScript Object Notation (JSON).`}
/>
</InputGroup>
}
</FormProvider> </FormProvider>
<div class="buttons is-right mt-5"> <div class="buttons is-right mt-5">

View File

@ -21,7 +21,7 @@
import { AmountJson, Amounts } from "@gnu-taler/taler-util"; import { AmountJson, Amounts } from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { format } from "date-fns"; import { format, formatDistance } from "date-fns";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { FormProvider } from "../../../../components/form/FormProvider.js"; import { FormProvider } from "../../../../components/form/FormProvider.js";
@ -223,6 +223,7 @@ function ClaimedPage({
</div> </div>
</div> </div>
</div> </div>
<div class="level"> <div class="level">
<div class="level-left"> <div class="level-left">
<div class="level-item"> <div class="level-item">
@ -419,6 +420,11 @@ function PaidPage({
} }
} }
const now = new Date()
const nextEvent = events.find((e) => {
return e.when.getTime() > now.getTime()
})
const [value, valueHandler] = useState<Partial<Paid>>(order); const [value, valueHandler] = useState<Partial<Paid>>(order);
const { url } = useBackendContext(); const { url } = useBackendContext();
const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part const refundHost = url.replace(/.*:\/\//, ""); // remove protocol part
@ -504,22 +510,13 @@ function PaidPage({
whiteSpace: "nowrap", whiteSpace: "nowrap",
overflow: "hidden", overflow: "hidden",
textOverflow: "ellipsis", textOverflow: "ellipsis",
// maxWidth: '100%',
}} }}
> >
<p> <p>
<a <i18n.Translate>Next event in </i18n.Translate> {formatDistance(
href={order.contract_terms.fulfillment_url} nextEvent!.when,
rel="nofollow" new Date(),
target="new" // "yyyy/MM/dd HH:mm:ss",
>
{order.contract_terms.fulfillment_url}
</a>
</p>
<p>
{format(
new Date(order.contract_terms.timestamp.t_s * 1000),
"yyyy/MM/dd HH:mm:ss",
)} )}
</p> </p>
</div> </div>
@ -668,9 +665,9 @@ function UnpaidPage({
{order.creation_time.t_s === "never" {order.creation_time.t_s === "never"
? "never" ? "never"
: format( : format(
new Date(order.creation_time.t_s * 1000), new Date(order.creation_time.t_s * 1000),
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss",
)} )}
</p> </p>
</div> </div>
</div> </div>

View File

@ -67,7 +67,7 @@ export function Timeline({ events: e }: Props) {
); );
case "start": case "start":
return ( return (
<div class="timeline-marker is-icon is-success"> <div class="timeline-marker is-icon">
<i class="mdi mdi-flag " /> <i class="mdi mdi-flag " />
</div> </div>
); );
@ -104,7 +104,7 @@ export function Timeline({ events: e }: Props) {
} }
})()} })()}
<div class="timeline-content"> <div class="timeline-content">
<p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p> {e.description !== "now" && <p class="heading">{format(e.when, "yyyy/MM/dd HH:mm:ss")}</p>}
<p>{e.description}</p> <p>{e.description}</p>
</div> </div>
</div> </div>
@ -117,12 +117,12 @@ export interface Event {
when: Date; when: Date;
description: string; description: string;
type: type:
| "start" | "start"
| "refund" | "refund"
| "refund-taken" | "refund-taken"
| "wired" | "wired"
| "wired-range" | "wired-range"
| "deadline" | "deadline"
| "delivery" | "delivery"
| "now"; | "now";
} }

View File

@ -164,7 +164,7 @@ export function ListPage({
<div class="field has-addons"> <div class="field has-addons">
{jumpToDate && ( {jumpToDate && (
<div class="control"> <div class="control">
<a class="button" onClick={() => onSelectDate(undefined)}> <a class="button is-fullwidth" onClick={() => onSelectDate(undefined)}>
<span <span
class="icon" class="icon"
data-tooltip={i18n.str`clear date filter`} data-tooltip={i18n.str`clear date filter`}
@ -191,7 +191,7 @@ export function ListPage({
<div class="control"> <div class="control">
<span class="has-tooltip-left" data-tooltip={dateTooltip}> <span class="has-tooltip-left" data-tooltip={dateTooltip}>
<a <a
class="button" class="button is-fullwidth"
onClick={() => { onClick={() => {
setPickDate(true); setPickDate(true);
}} }}

View File

@ -85,34 +85,34 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
template_contract: !state.template_contract template_contract: !state.template_contract
? undefined ? undefined
: undefinedIfEmpty({ : undefinedIfEmpty({
amount: !state.template_contract?.amount amount: !state.template_contract?.amount
? undefined ? undefined
: !parsedPrice : !parsedPrice
? i18n.str`not valid` ? i18n.str`not valid`
: Amounts.isZero(parsedPrice) : Amounts.isZero(parsedPrice)
? i18n.str`must be greater than 0` ? i18n.str`must be greater than 0`
: undefined,
minimum_age:
state.template_contract.minimum_age < 0
? i18n.str`should be greater that 0`
: undefined, : undefined,
pay_duration: !state.template_contract.pay_duration minimum_age:
? i18n.str`can't be empty` state.template_contract.minimum_age < 0
: state.template_contract.pay_duration.d_us === "forever" ? i18n.str`should be greater that 0`
: undefined,
pay_duration: !state.template_contract.pay_duration
? i18n.str`can't be empty`
: state.template_contract.pay_duration.d_us === "forever"
? undefined ? undefined
: state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second : state.template_contract.pay_duration.d_us < 1000 * 1000 //less than one second
? i18n.str`to short` ? i18n.str`to short`
: undefined, : undefined,
} as Partial<MerchantTemplateContractDetails>), } as Partial<MerchantTemplateContractDetails>),
pos_key: !state.pos_key pos_key: !state.pos_key
? !state.pos_algorithm ? !state.pos_algorithm
? undefined ? undefined
: i18n.str`required` : i18n.str`required`
: !isBase32RFC3548Charset(state.pos_key) : !isBase32RFC3548Charset(state.pos_key)
? i18n.str`just letters and numbers from 2 to 7` ? i18n.str`just letters and numbers from 2 to 7`
: state.pos_key.length !== 32 : state.pos_key.length !== 32
? i18n.str`size of the key should be 32` ? i18n.str`size of the key should be 32`
: undefined, : undefined,
}; };
const hasErrors = Object.keys(errors).some( const hasErrors = Object.keys(errors).some(
@ -139,7 +139,7 @@ export function CreatePage({ onCreate, onBack }: Props): VNode {
> >
<InputWithAddon<Entity> <InputWithAddon<Entity>
name="template_id" name="template_id"
addonBefore={`${backend.url}/instances/templates/`} help={`${backend.url}/instances/templates/${state.template_id ?? ""}`}
label={i18n.str`Identifier`} label={i18n.str`Identifier`}
tooltip={i18n.str`Name of the template in URLs.`} tooltip={i18n.str`Name of the template in URLs.`}
/> />

View File

@ -34,6 +34,7 @@ import { useBackendContext } from "../../../../context/backend.js";
import { useConfigContext } from "../../../../context/config.js"; import { useConfigContext } from "../../../../context/config.js";
import { useInstanceContext } from "../../../../context/instance.js"; import { useInstanceContext } from "../../../../context/instance.js";
import { MerchantBackend } from "../../../../declaration.js"; import { MerchantBackend } from "../../../../declaration.js";
import { stringifyPayTemplateUri } from "@gnu-taler/taler-util";
type Entity = MerchantBackend.Template.UsingTemplateDetails; type Entity = MerchantBackend.Template.UsingTemplateDetails;
@ -64,46 +65,47 @@ export function QrPage({ template, id: templateId, onBack }: Props): VNode {
const fixedAmount = !!template.template_contract.amount; const fixedAmount = !!template.template_contract.amount;
const fixedSummary = !!template.template_contract.summary; const fixedSummary = !!template.template_contract.summary;
const params = new URLSearchParams(); const templateParams: Record<string, string> = {}
if (!fixedAmount) { if (!fixedAmount) {
if (state.amount) { if (state.amount) {
params.append("amount", state.amount); templateParams.amount = state.amount
} else { } else {
params.append("amount", config.currency); templateParams.amount = config.currency
} }
} }
if (!fixedSummary) { if (!fixedSummary) {
params.append("summary", state.summary ?? ""); templateParams.summary = state.summary ?? ""
} }
const paramsStr = fixedAmount && fixedSummary ? "" : "?" + params.toString(); const merchantBaseUrl = new URL(backendUrl).href;
const merchantURL = new URL(backendUrl);
const talerProto = const payTemplateUri = stringifyPayTemplateUri({
merchantURL.protocol === "http:" ? "taler+http:" : "taler:"; merchantBaseUrl,
templateId,
const payTemplateUri = `${talerProto}//pay-template/${merchantURL.hostname}/${templateId}${paramsStr}`; templateParams
})
const issuer = encodeURIComponent( const issuer = encodeURIComponent(
`${new URL(backendUrl).hostname}/${instanceId}`, `${new URL(backendUrl).host}/${instanceId}`,
); );
const oauthUri = !template.pos_algorithm const oauthUri = !template.pos_algorithm
? undefined ? undefined
: template.pos_algorithm === 1 : template.pos_algorithm === 1
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: template.pos_algorithm === 2 : template.pos_algorithm === 2
? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${template.pos_key}&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: undefined; : undefined;
const keySlice = template.pos_key?.substring(0, 4); const keySlice = template.pos_key?.substring(0, 4);
const oauthUriWithoutSecret = !template.pos_algorithm const oauthUriWithoutSecret = !template.pos_algorithm
? undefined ? undefined
: template.pos_algorithm === 1 : template.pos_algorithm === 1
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: template.pos_algorithm === 2 : template.pos_algorithm === 2
? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30` ? `otpauth://totp/${issuer}:${templateId}?secret=${keySlice}...&issuer=${issuer}&algorithm=SHA1&digits=8&period=30`
: undefined; : undefined;
return ( return (
<div> <div>
{oauthUri && ( {oauthUri && (

View File

@ -25,7 +25,6 @@ import { Link } from "preact-router";
export default function NotFoundPage(): VNode { export default function NotFoundPage(): VNode {
return ( return (
<div> <div>
<h1>Error 404</h1>
<p>That page doesn&apos;t exist.</p> <p>That page doesn&apos;t exist.</p>
<Link href="/"> <Link href="/">
<h4>Back to Home</h4> <h4>Back to Home</h4>

View File

@ -0,0 +1,77 @@
import { VNode, h } from "preact";
import { LangSelector } from "../../components/menu/LangSelector.js";
import { useLang, useTranslationContext } from "@gnu-taler/web-util/browser";
import { InputToggle } from "../../components/form/InputToggle.js";
import { Settings, useSettings } from "../../hooks/useSettings.js";
import { FormErrors, FormProvider } from "../../components/form/FormProvider.js";
import { useState } from "preact/hooks";
function getBrowserLang(): string | undefined {
if (typeof window === "undefined") return undefined;
if (window.navigator.languages) return window.navigator.languages[0];
if (window.navigator.language) return window.navigator.language;
return undefined;
}
export function Settings(): VNode {
const { i18n } = useTranslationContext()
const borwserLang = getBrowserLang()
const { update } = useLang()
const [value, updateValue] = useSettings()
const errors: FormErrors<Settings> = {
}
function valueHandler(s: (d: Partial<Settings>) => Partial<Settings>): void {
const next = s(value)
updateValue("advanceOrderMode", next.advanceOrderMode ?? false)
}
return <div>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
<div class="field is-horizontal">
<div class="field-label is-normal">
<label class="label" style={{ width: 200 }}>
<i18n.Translate>Language</i18n.Translate>
<span class="icon has-tooltip-right" data-tooltip={"Force language setting instance of taking the browser"}>
<i class="mdi mdi-information" />
</span>
</label>
</div>
<div class="field has-addons">
<LangSelector />
&nbsp;
{borwserLang !== undefined && <button
data-tooltip={i18n.str`generate random secret key`}
class="button is-info mr-3"
onClick={(e) => {
update(borwserLang.substring(0, 2))
}}
>
<i18n.Translate>Set default</i18n.Translate>
</button>}
</div>
</div>
<FormProvider<Settings>
name="settings"
errors={errors}
object={value}
valueHandler={valueHandler}
>
<InputToggle<Settings>
label={i18n.str`Advance order creation`}
tooltip={i18n.str`Shows more options in the order creation form`}
name="advanceOrderMode"
/>
</FormProvider>
</div>
<div class="column" />
</div>
</section>
</div>
}

View File

@ -52,6 +52,8 @@ $tooltip-color: red;
@import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css"; @import "../../node_modules/@creativebulma/bulma-tooltip/dist/bulma-tooltip.min.css";
@import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css"; @import "../../node_modules/bulma-timeline/dist/css/bulma-timeline.min.css";
@import "toggle";
.notification { .notification {
background-color: transparent; background-color: transparent;
} }
@ -82,7 +84,7 @@ $tooltip-color: red;
pointer-events: none; pointer-events: none;
} }
.toast > .message { .toast>.message {
white-space: pre-wrap; white-space: pre-wrap;
opacity: 80%; opacity: 80%;
} }
@ -92,6 +94,7 @@ div {
position: relative; position: relative;
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;
&:after { &:after {
// @include loader; // @include loader;
position: absolute; position: absolute;
@ -104,7 +107,7 @@ div {
} }
} }
input[type="checkbox"]:indeterminate + .check { input[type="checkbox"]:indeterminate+.check {
background: red !important; background: red !important;
} }
@ -125,6 +128,7 @@ input[type="checkbox"]:indeterminate + .check {
tr:hover .right-sticky { tr:hover .right-sticky {
background-color: hsl(0, 0%, 80%); background-color: hsl(0, 0%, 80%);
} }
.table.is-striped tbody tr:nth-child(even):hover .right-sticky { .table.is-striped tbody tr:nth-child(even):hover .right-sticky {
background-color: hsl(0, 0%, 95%); background-color: hsl(0, 0%, 95%);
} }
@ -181,11 +185,11 @@ div[data-tooltip]::before {
position: absolute; position: absolute;
} }
.modal-card-body > p { .modal-card-body>p {
padding: 1em; padding: 1em;
} }
.modal-card-body > p.warning { .modal-card-body>p.warning {
background-color: #fffbdd; background-color: #fffbdd;
border: solid 1px #f2e9bf; border: solid 1px #f2e9bf;
} }

View File

@ -0,0 +1,51 @@
$green: #56c080;
.toggle {
cursor: pointer;
display: inline-block;
}
.toggle-switch {
display: inline-block;
background: #ccc;
border-radius: 16px;
width: 58px;
height: 32px;
position: relative;
vertical-align: middle;
transition: background 0.25s;
&:before,
&:after {
content: "";
}
&:before {
display: block;
background: linear-gradient(to bottom, #fff 0%, #eee 100%);
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25);
width: 24px;
height: 24px;
position: absolute;
top: 4px;
left: 4px;
transition: left 0.25s;
}
.toggle:hover &:before {
background: linear-gradient(to bottom, #fff 0%, #fff 100%);
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5);
}
.toggle-checkbox:checked + & {
background: $green;
&:before {
left: 30px;
}
}
}
.toggle-checkbox {
position: absolute;
visibility: hidden;
}
.toggle-label {
margin-left: 5px;
position: relative;
top: 2px;
}

View File

@ -26,7 +26,7 @@ import {
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { defaultCoinConfig } from "../harness/denomStructures.js"; import { defaultCoinConfig } from "../harness/denomStructures.js";
import { GlobalTestState, WalletCli } from "../harness/harness.js"; import { GlobalTestState } from "../harness/harness.js";
import { import {
createSimpleTestkudosEnvironmentV2, createSimpleTestkudosEnvironmentV2,
createWalletDaemonWithClient, createWalletDaemonWithClient,
@ -106,7 +106,7 @@ export async function runAgeRestrictionsPeerTest(t: GlobalTestState) {
); );
await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, { await wallet2.call(WalletApiOperation.ConfirmPeerPushCredit, {
peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId, transactionId: checkResp.transactionId,
}); });
const peerPullCreditDoneCond = wallet2.waitForNotificationCond( const peerPullCreditDoneCond = wallet2.waitForNotificationCond(

View File

@ -338,8 +338,17 @@ export async function runTests(spec: TestRunSpec) {
currentChild.stdout?.pipe(harnessLogStream); currentChild.stdout?.pipe(harnessLogStream);
currentChild.stderr?.pipe(harnessLogStream); currentChild.stderr?.pipe(harnessLogStream);
const defaultTimeout = 60000; // Default timeout when the test doesn't override it.
const testTimeoutMs = testCase.timeoutMs ?? defaultTimeout; let defaultTimeout = 60000;
const overrideDefaultTimeout = process.env.TALER_TEST_TIMEOUT;
if (overrideDefaultTimeout) {
defaultTimeout = Number.parseInt(overrideDefaultTimeout, 10) * 1000;
}
// Set the timeout to at least be the default timeout.
const testTimeoutMs = testCase.timeoutMs
? Math.max(testCase.timeoutMs, defaultTimeout)
: defaultTimeout;
if (spec.noTimeout) { if (spec.noTimeout) {
console.log(`running ${testName}, no timeout`); console.log(`running ${testName}, no timeout`);

View File

@ -767,7 +767,7 @@ function getUrlInfo(
const qp = new URLSearchParams(); const qp = new URLSearchParams();
let withParams = false; let withParams = false;
Object.entries(params).forEach(([name, value]) => { Object.entries(params).forEach(([name, value]) => {
if (value) { if (value !== undefined) {
withParams = true; withParams = true;
qp.append(name, value); qp.append(name, value);
} }

View File

@ -379,6 +379,54 @@ export interface Balance {
requiresUserInput: boolean; requiresUserInput: boolean;
} }
export const codecForScopeInfoGlobal = (): Codec<ScopeInfoGlobal> =>
buildCodecForObject<ScopeInfoGlobal>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Global))
.build("ScopeInfoGlobal");
export const codecForScopeInfoExchange = (): Codec<ScopeInfoExchange> =>
buildCodecForObject<ScopeInfoExchange>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Exchange))
.property("url", codecForString())
.build("ScopeInfoExchange");
export const codecForScopeInfoAuditor = (): Codec<ScopeInfoAuditor> =>
buildCodecForObject<ScopeInfoAuditor>()
.property("currency", codecForString())
.property("type", codecForConstString(ScopeType.Auditor))
.property("url", codecForString())
.build("ScopeInfoAuditor");
export const codecForScopeInfo = (): Codec<ScopeInfo> =>
buildCodecForUnion<ScopeInfo>()
.discriminateOn("type")
.alternative(ScopeType.Global, codecForScopeInfoGlobal())
.alternative(ScopeType.Exchange, codecForScopeInfoExchange())
.alternative(ScopeType.Auditor, codecForScopeInfoAuditor())
.build("ScopeInfo");
export interface GetCurrencyInfoRequest {
scope: ScopeInfo;
}
export const codecForGetCurrencyInfoRequest =
(): Codec<GetCurrencyInfoRequest> =>
buildCodecForObject<GetCurrencyInfoRequest>()
.property("scope", codecForScopeInfo())
.build("GetCurrencyInfoRequest");
export interface GetCurrencyInfoResponse {
decimalSeparator: string;
numFractionalDigits: number;
numTinyDigits: number;
/**
* Is the currency name leading or trailing?
*/
isCurrencyNameLeading: boolean;
}
export interface InitRequest { export interface InitRequest {
skipDefaults?: boolean; skipDefaults?: boolean;
} }
@ -393,10 +441,19 @@ export enum ScopeType {
Auditor = "auditor", Auditor = "auditor",
} }
export type ScopeInfo = export type ScopeInfoGlobal = { type: ScopeType.Global; currency: string };
| { type: ScopeType.Global; currency: string } export type ScopeInfoExchange = {
| { type: ScopeType.Exchange; currency: string; url: string } type: ScopeType.Exchange;
| { type: ScopeType.Auditor; currency: string; url: string }; currency: string;
url: string;
};
export type ScopeInfoAuditor = {
type: ScopeType.Auditor;
currency: string;
url: string;
};
export type ScopeInfo = ScopeInfoGlobal | ScopeInfoExchange | ScopeInfoAuditor;
export interface BalancesResponse { export interface BalancesResponse {
balances: Balance[]; balances: Balance[];

View File

@ -711,7 +711,10 @@ export interface RewardCoinSource {
coinIndex: number; coinIndex: number;
} }
export type CoinSource = WithdrawCoinSource | RefreshCoinSource | RewardCoinSource; export type CoinSource =
| WithdrawCoinSource
| RefreshCoinSource
| RewardCoinSource;
/** /**
* CoinRecord as stored in the "coins" data store * CoinRecord as stored in the "coins" data store

View File

@ -114,6 +114,8 @@ import {
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
SharePaymentRequest, SharePaymentRequest,
SharePaymentResult, SharePaymentResult,
GetCurrencyInfoRequest,
GetCurrencyInfoResponse,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { AuditorTrustRecord, WalletContractData } from "./db.js"; import { AuditorTrustRecord, WalletContractData } from "./db.js";
import { import {
@ -210,6 +212,7 @@ export enum WalletApiOperation {
ApplyDevExperiment = "applyDevExperiment", ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban", ValidateIban = "validateIban",
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal", TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
GetScopedCurrencyInfo = "getScopedCurrencyInfo",
} }
// group: Initialization // group: Initialization
@ -601,6 +604,12 @@ export type ListCurrenciesOp = {
response: WalletCurrencyInfo; response: WalletCurrencyInfo;
}; };
export type GetScopedCurrencyInfoOp = {
op: WalletApiOperation.GetScopedCurrencyInfo;
request: GetCurrencyInfoRequest;
response: GetCurrencyInfoResponse;
};
// group: Deposits // group: Deposits
/** /**
@ -1072,6 +1081,7 @@ export type WalletOperations = {
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp; [WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.ValidateIban]: ValidateIbanOp; [WalletApiOperation.ValidateIban]: ValidateIbanOp;
[WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal; [WalletApiOperation.TestingWaitTransactionsFinal]: TestingWaitTransactionsFinal;
[WalletApiOperation.GetScopedCurrencyInfo]: GetScopedCurrencyInfoOp;
}; };
export type WalletCoreRequestType< export type WalletCoreRequestType<

View File

@ -118,6 +118,8 @@ import {
sampleWalletCoreTransactions, sampleWalletCoreTransactions,
validateIban, validateIban,
codecForSharePaymentRequest, codecForSharePaymentRequest,
GetCurrencyInfoResponse,
codecForGetCurrencyInfoRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
HttpRequestLibrary, HttpRequestLibrary,
@ -1395,6 +1397,17 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
const resp = await getBackupRecovery(ws); const resp = await getBackupRecovery(ws);
return resp; return resp;
} }
case WalletApiOperation.GetScopedCurrencyInfo: {
// Ignore result, just validate in this mock implementation
codecForGetCurrencyInfoRequest().decode(payload);
const resp: GetCurrencyInfoResponse = {
decimalSeparator: ",",
isCurrencyNameLeading: false,
numFractionalDigits: 2,
numTinyDigits: 1,
};
return resp;
}
case WalletApiOperation.ImportBackupRecovery: { case WalletApiOperation.ImportBackupRecovery: {
const req = codecForAny().decode(payload); const req = codecForAny().decode(payload);
await loadBackupRecovery(ws, req); await loadBackupRecovery(ws, req);

View File

@ -45,11 +45,11 @@ import {
reduceAction, reduceAction,
getBackupStartState, getBackupStartState,
getRecoveryStartState, getRecoveryStartState,
discoverPolicies,
mergeDiscoveryAggregate,
ReducerState, ReducerState,
} from "@gnu-taler/anastasis-core"; } from "@gnu-taler/anastasis-core";
import { import { userIdentifierDerive } from "@gnu-taler/anastasis-core/lib/crypto.js";
userIdentifierDerive,
} from "@gnu-taler/anastasis-core/lib/crypto.js";
setGlobalLogLevelFromString("trace"); setGlobalLogLevelFromString("trace");
@ -195,18 +195,33 @@ async function handleAnastasisRequest(
}; };
}; };
let req = args ?? {};
switch (operation) { switch (operation) {
case "anastasisReduce": case "anastasisReduce":
// TODO: do some input validation here // TODO: do some input validation here
let req = args ?? {}; let reduceRes = await reduceAction(req.state, req.action, req.args ?? {});
let res = await reduceAction(req.state, req.action, req.args ?? {});
// For now, this will return "success" even if the wrapped Anastasis // For now, this will return "success" even if the wrapped Anastasis
// response is a ReducerStateError. // response is a ReducerStateError.
return wrapSuccessResponse(res); return wrapSuccessResponse(reduceRes);
case "anastasisStartBackup": case "anastasisStartBackup":
return wrapSuccessResponse(await getBackupStartState()); return wrapSuccessResponse(await getBackupStartState());
case "anastasisStartRecovery": case "anastasisStartRecovery":
return wrapSuccessResponse(await getRecoveryStartState()); return wrapSuccessResponse(await getRecoveryStartState());
case "anastasisDiscoverPolicies":
let discoverRes = await discoverPolicies(req.state, req.cursor);
let aggregatedPolicies = mergeDiscoveryAggregate(
discoverRes.policies ?? [],
req.state.discoveryState?.aggregatedPolicies ?? [],
);
return wrapSuccessResponse({
...req.state,
discoveryState: {
state: "finished",
aggregatedPolicies,
cursor: discoverRes.cursor,
},
});
} }
} }
@ -318,13 +333,15 @@ export async function testArgon2id() {
}, },
input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4", input_server_salt: "FZ48EFS7WS3R2ZR4V53A3GFFY4",
output_id: output_id:
"YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18", "YS45R6CGJV84K1NN7T14ZBCPVTZ6H15XJSM1FV0R748MHPV82SM0126EBZKBAAGCR34Q9AFKPEW1HRT2Q9GQ5JRA3642AB571DKZS18",
}; };
if (await userIdentifierDerive( if (
userIdVector.input_id_data, (await userIdentifierDerive(
userIdVector.input_server_salt, userIdVector.input_id_data,
) != userIdVector.output_id) { userIdVector.input_server_salt,
)) != userIdVector.output_id
) {
throw Error("argon2id is not working!"); throw Error("argon2id is not working!");
} }
@ -337,4 +354,7 @@ globalThis.testWithGv = testWithGv;
globalThis.testWithLocal = testWithLocal; globalThis.testWithLocal = testWithLocal;
// @ts-ignore // @ts-ignore
globalThis.testArgon2id = testArgon2id; globalThis.testArgon2id = testArgon2id;
// @ts-ignore
globalThis.testReduceAction = reduceAction;
// @ts-ignore
globalThis.testDiscoverPolicies = discoverPolicies;

View File

@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n" "Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-04-24 06:43+0000\n" "PO-Revision-Date: 2023-08-13 10:14+0000\n"
"Last-Translator: José Huamán <princetomato@firemail.cc>\n" "Last-Translator: Javier Sepulveda <javier.sepulveda@uv.es>\n"
"Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Spanish <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/es/>\n" "webextensions/es/>\n"
"Language: es\n" "Language: es\n"
@ -828,7 +828,7 @@ msgstr "Precio"
#: src/wallet/Transaction.tsx:1156 #: src/wallet/Transaction.tsx:1156
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "Reembolzado" msgstr "Reembolsado"
#: src/wallet/Transaction.tsx:1220 #: src/wallet/Transaction.tsx:1220
#, c-format #, c-format

View File

@ -17,8 +17,8 @@ msgstr ""
"Project-Id-Version: Taler Wallet\n" "Project-Id-Version: Taler Wallet\n"
"Report-Msgid-Bugs-To: languages@taler.net\n" "Report-Msgid-Bugs-To: languages@taler.net\n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n" "POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: 2023-03-06 22:06+0000\n" "PO-Revision-Date: 2023-08-15 07:28+0000\n"
"Last-Translator: Stefan Kügel <skuegel@web.de>\n" "Last-Translator: Krystian Baran <kiszkot@murena.io>\n"
"Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/" "Language-Team: Italian <https://weblate.taler.net/projects/gnu-taler/"
"webextensions/it/>\n" "webextensions/it/>\n"
"Language: it\n" "Language: it\n"
@ -328,7 +328,7 @@ msgstr ""
#: src/components/ShowFullContractTermPopup.tsx:195 #: src/components/ShowFullContractTermPopup.tsx:195
#, c-format #, c-format
msgid "Amount" msgid "Amount"
msgstr "" msgstr "Importo"
#: src/components/ShowFullContractTermPopup.tsx:203 #: src/components/ShowFullContractTermPopup.tsx:203
#, c-format #, c-format
@ -530,7 +530,7 @@ msgstr ""
#: src/components/BankDetailsByPaytoType.tsx:148 #: src/components/BankDetailsByPaytoType.tsx:148
#, c-format #, c-format
msgid "Subject" msgid "Subject"
msgstr "" msgstr "Soggetto"
#: src/components/BankDetailsByPaytoType.tsx:154 #: src/components/BankDetailsByPaytoType.tsx:154
#, c-format #, c-format
@ -784,7 +784,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:935 #: src/wallet/Transaction.tsx:935
#, c-format #, c-format
msgid "Date" msgid "Date"
msgstr "" msgstr "Data"
#: src/wallet/Transaction.tsx:990 #: src/wallet/Transaction.tsx:990
#, c-format #, c-format
@ -799,7 +799,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:1074 #: src/wallet/Transaction.tsx:1074
#, c-format #, c-format
msgid "Withdraw" msgid "Withdraw"
msgstr "" msgstr "Prelevare"
#: src/wallet/Transaction.tsx:1146 #: src/wallet/Transaction.tsx:1146
#, c-format #, c-format
@ -809,7 +809,7 @@ msgstr ""
#: src/wallet/Transaction.tsx:1156 #: src/wallet/Transaction.tsx:1156
#, c-format #, c-format
msgid "Refunded" msgid "Refunded"
msgstr "" msgstr "Rimborsato"
#: src/wallet/Transaction.tsx:1220 #: src/wallet/Transaction.tsx:1220
#, c-format #, c-format
@ -1270,7 +1270,7 @@ msgstr ""
#: src/wallet/CreateManualWithdraw.tsx:277 #: src/wallet/CreateManualWithdraw.tsx:277
#, c-format #, c-format
msgid "Start withdrawal" msgid "Start withdrawal"
msgstr "" msgstr "Inizia a prelevare"
#: src/wallet/DepositPage/views.tsx:38 #: src/wallet/DepositPage/views.tsx:38
#, c-format #, c-format
@ -1576,7 +1576,7 @@ msgstr ""
#: src/wallet/Settings.tsx:191 #: src/wallet/Settings.tsx:191
#, c-format #, c-format
msgid "ok" msgid "ok"
msgstr "" msgstr "ok"
#: src/wallet/Settings.tsx:197 #: src/wallet/Settings.tsx:197
#, c-format #, c-format

File diff suppressed because it is too large Load Diff