Merge branch 'master' into age-withdraw
This commit is contained in:
commit
819949d7f2
@ -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();
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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 />
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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={() => {
|
||||||
|
@ -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(
|
||||||
|
@ -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()
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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`,
|
||||||
|
@ -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.`,
|
||||||
|
@ -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
|
||||||
*/}
|
*/}
|
||||||
|
@ -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>
|
||||||
|
}
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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>"{instance}"</b>.{" "}
|
You are viewing the instance <b>"{instance}"</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>
|
||||||
);
|
);
|
||||||
|
@ -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],
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
|
59
packages/merchant-backoffice-ui/src/hooks/useSettings.ts
Normal file
59
packages/merchant-backoffice-ui/src/hooks/useSettings.ts
Normal 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];
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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">
|
||||||
|
@ -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>
|
||||||
|
@ -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";
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}}
|
}}
|
||||||
|
@ -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.`}
|
||||||
/>
|
/>
|
||||||
|
@ -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 && (
|
||||||
|
@ -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't exist.</p>
|
<p>That page doesn't exist.</p>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<h4>Back to Home</h4>
|
<h4>Back to Home</h4>
|
||||||
|
77
packages/merchant-backoffice-ui/src/paths/settings/index.tsx
Normal file
77
packages/merchant-backoffice-ui/src/paths/settings/index.tsx
Normal 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 />
|
||||||
|
|
||||||
|
{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>
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
51
packages/merchant-backoffice-ui/src/scss/toggle.scss
Normal file
51
packages/merchant-backoffice-ui/src/scss/toggle.scss
Normal 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;
|
||||||
|
}
|
@ -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(
|
||||||
|
@ -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`);
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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[];
|
||||||
|
@ -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
|
||||||
|
@ -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<
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
@ -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
|
||||||
|
@ -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
|
||||||
|
1950
packages/taler-wallet-webextension/src/i18n/nl.po
Normal file
1950
packages/taler-wallet-webextension/src/i18n/nl.po
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user