From 64e78d03a117fffeb18e18154d9028a2532285a5 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Fri, 1 Sep 2023 10:52:15 +0200 Subject: [PATCH 01/34] wallet-core: implement and test stored backups --- packages/idb-bridge/src/SqliteBackend.ts | 27 ++++- packages/idb-bridge/src/bridge-idb.ts | 7 +- .../integrationtests/test-stored-backups.ts | 110 ++++++++++++++++++ .../src/integrationtests/testrunner.ts | 2 + packages/taler-util/src/backup-types.ts | 19 +++ packages/taler-util/src/wallet-types.ts | 12 ++ packages/taler-wallet-cli/src/index.ts | 42 ++++++- packages/taler-wallet-core/src/db.ts | 53 ++++++--- .../taler-wallet-core/src/host-impl.node.ts | 6 +- packages/taler-wallet-core/src/wallet.ts | 68 ++++++++++- 10 files changed, 318 insertions(+), 28 deletions(-) create mode 100644 packages/taler-harness/src/integrationtests/test-stored-backups.ts diff --git a/packages/idb-bridge/src/SqliteBackend.ts b/packages/idb-bridge/src/SqliteBackend.ts index c40281861..a25ec0045 100644 --- a/packages/idb-bridge/src/SqliteBackend.ts +++ b/packages/idb-bridge/src/SqliteBackend.ts @@ -1882,7 +1882,7 @@ export class SqliteBackend implements Backend { } } - clearObjectStore( + async clearObjectStore( btx: DatabaseTransaction, objectStoreName: string, ): Promise { @@ -1906,7 +1906,21 @@ export class SqliteBackend implements Backend { ); } - throw new Error("Method not implemented."); + this._prep(sqlClearObjectStore).run({ + object_store_id: scopeInfo.objectStoreId, + }); + + for (const index of scopeInfo.indexMap.values()) { + let stmt: Sqlite3Statement; + if (index.unique) { + stmt = this._prep(sqlClearUniqueIndexData); + } else { + stmt = this._prep(sqlClearIndexData); + } + stmt.run({ + index_id: index.indexId, + }); + } } } @@ -1963,6 +1977,15 @@ CREATE TABLE IF NOT EXISTS unique_index_data ); `; +const sqlClearObjectStore = ` +DELETE FROM object_data WHERE object_store_id=$object_store_id`; + +const sqlClearIndexData = ` +DELETE FROM index_data WHERE index_id=$index_id`; + +const sqlClearUniqueIndexData = ` +DELETE FROM unique_index_data WHERE index_id=$index_id`; + const sqlListDatabases = ` SELECT name, version FROM databases; `; diff --git a/packages/idb-bridge/src/bridge-idb.ts b/packages/idb-bridge/src/bridge-idb.ts index 8cecba534..f3749c77c 100644 --- a/packages/idb-bridge/src/bridge-idb.ts +++ b/packages/idb-bridge/src/bridge-idb.ts @@ -735,7 +735,9 @@ export class BridgeIDBDatabase extends FakeEventTarget implements IDBDatabase { } if (this._closePending) { - throw new InvalidStateError(); + throw new InvalidStateError( + `tried to start transaction on ${this._name}, but a close is pending`, + ); } if (!Array.isArray(storeNames)) { @@ -930,6 +932,9 @@ export class BridgeIDBFactory { // http://www.w3.org/TR/2015/REC-IndexedDB-20150108/#dfn-steps-for-running-a-versionchange-transaction for (const otherConn of this.connections) { + if (otherConn._name != db._name) { + continue; + } if (otherConn._closePending) { continue; } diff --git a/packages/taler-harness/src/integrationtests/test-stored-backups.ts b/packages/taler-harness/src/integrationtests/test-stored-backups.ts new file mode 100644 index 000000000..831506d83 --- /dev/null +++ b/packages/taler-harness/src/integrationtests/test-stored-backups.ts @@ -0,0 +1,110 @@ +/* + This file is part of GNU Taler + (C) 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 + */ + +/** + * Imports. + */ +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { GlobalTestState } from "../harness/harness.js"; +import { + withdrawViaBankV2, + makeTestPaymentV2, + useSharedTestkudosEnvironment, +} from "../harness/helpers.js"; + +/** + * Test stored backup wallet-core API. + */ +export async function runStoredBackupsTest(t: GlobalTestState) { + // Set up test environment + + const { walletClient, bank, exchange, merchant } = + await useSharedTestkudosEnvironment(t); + + // Withdraw digital cash into the wallet. + + await withdrawViaBankV2(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:20", + }); + + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const sb1Resp = await walletClient.call( + WalletApiOperation.CreateStoredBackup, + {}, + ); + const sbList = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbList.storedBackups.length === 1); + t.assertTrue(sbList.storedBackups[0].name === sb1Resp.name); + + const order = { + summary: "Buy me!", + amount: "TESTKUDOS:5", + fulfillment_url: "taler://fulfillment-success/thx", + }; + + await makeTestPaymentV2(t, { walletClient, merchant, order }); + await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); + + const txn1 = await walletClient.call(WalletApiOperation.GetTransactions, {}); + t.assertDeepEqual(txn1.transactions.length, 2); + + // Recover from the stored backup now. + + const sb2Resp = await walletClient.call( + WalletApiOperation.CreateStoredBackup, + {}, + ); + + console.log("recovering backup"); + + await walletClient.call(WalletApiOperation.RecoverStoredBackup, { + name: sb1Resp.name, + }); + + console.log("first recovery done"); + + // Recovery went well, now we can delete the backup + // of the old database we stored before importing. + { + const sbl1 = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbl1.storedBackups.length === 2); + + await walletClient.call(WalletApiOperation.DeleteStoredBackup, { + name: sb1Resp.name, + }); + const sbl2 = await walletClient.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + t.assertTrue(sbl2.storedBackups.length === 1); + } + + const txn2 = await walletClient.call(WalletApiOperation.GetTransactions, {}); + // We only have the withdrawal after restoring + t.assertDeepEqual(txn2.transactions.length, 1); +} + +runStoredBackupsTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts index 501af98a4..7afd9bc83 100644 --- a/packages/taler-harness/src/integrationtests/testrunner.ts +++ b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -114,6 +114,7 @@ import { runSimplePaymentTest } from "./test-simple-payment.js"; import { runTermOfServiceFormatTest } from "./test-tos-format.js"; import { runExchangePurseTest } from "./test-exchange-purse.js"; import { getSharedTestDir } from "../harness/helpers.js"; +import { runStoredBackupsTest } from "./test-stored-backups.js"; /** * Test runner. @@ -212,6 +213,7 @@ const allTests: TestMainFunction[] = [ runWithdrawalFeesTest, runWithdrawalHugeTest, runTermOfServiceFormatTest, + runStoredBackupsTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/backup-types.ts b/packages/taler-util/src/backup-types.ts index 2eba1e4ca..8c38b70a6 100644 --- a/packages/taler-util/src/backup-types.ts +++ b/packages/taler-util/src/backup-types.ts @@ -14,6 +14,8 @@ GNU Taler; see the file COPYING. If not, see */ +import { AmountString } from "./taler-types.js"; + export interface BackupRecovery { walletRootPriv: string; providers: { @@ -21,3 +23,20 @@ export interface BackupRecovery { url: string; }[]; } + +export class BackupBackupProviderTerms { + /** + * Last known supported protocol version. + */ + supported_protocol_version: string; + + /** + * Last known annual fee. + */ + annual_fee: AmountString; + + /** + * Last known storage limit. + */ + storage_limit_in_megabytes: number; +} diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index accab746f..d49182e26 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -2673,3 +2673,15 @@ export interface RecoverStoredBackupRequest { export interface DeleteStoredBackupRequest { name: string; } + +export const codecForDeleteStoredBackupRequest = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .build("DeleteStoredBackupRequest"); + +export const codecForRecoverStoredBackupRequest = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .build("RecoverStoredBackupRequest"); diff --git a/packages/taler-wallet-cli/src/index.ts b/packages/taler-wallet-cli/src/index.ts index 36e7f7768..a0f44fb41 100644 --- a/packages/taler-wallet-cli/src/index.ts +++ b/packages/taler-wallet-cli/src/index.ts @@ -883,7 +883,7 @@ backupCli.subcommand("exportDb", "export-db").action(async (args) => { }); }); -backupCli.subcommand("storeBackup", "store-backup").action(async (args) => { +backupCli.subcommand("storeBackup", "store").action(async (args) => { await withWallet(args, async (wallet) => { const resp = await wallet.client.call( WalletApiOperation.CreateStoredBackup, @@ -893,6 +893,46 @@ backupCli.subcommand("storeBackup", "store-backup").action(async (args) => { }); }); +backupCli.subcommand("storeBackup", "list-stored").action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.ListStoredBackups, + {}, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); +}); + +backupCli + .subcommand("storeBackup", "delete-stored") + .requiredArgument("name", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.DeleteStoredBackup, + { + name: args.storeBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + +backupCli + .subcommand("recoverBackup", "recover-stored") + .requiredArgument("name", clk.STRING) + .action(async (args) => { + await withWallet(args, async (wallet) => { + const resp = await wallet.client.call( + WalletApiOperation.RecoverStoredBackup, + { + name: args.recoverBackup.name, + }, + ); + console.log(JSON.stringify(resp, undefined, 2)); + }); + }); + backupCli.subcommand("importDb", "import-db").action(async (args) => { await withWallet(args, async (wallet) => { const dumpRaw = await read(process.stdin); diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index a642c0203..b9d86eb25 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -22,6 +22,7 @@ import { IDBDatabase, IDBFactory, IDBObjectStore, + IDBRequest, IDBTransaction, structuredEncapsulate, } from "@gnu-taler/idb-bridge"; @@ -59,6 +60,7 @@ import { Logger, CoinPublicKeyString, TalerPreciseTimestamp, + j2s, } from "@gnu-taler/taler-util"; import { DbAccess, @@ -117,7 +119,8 @@ export const TALER_WALLET_META_DB_NAME = "taler-wallet-meta"; /** * Stored backups, mainly created when manually importing a backup. */ -export const TALER_WALLET_STORED_BACKUPS_DB_NAME = "taler-wallet-stored-backups"; +export const TALER_WALLET_STORED_BACKUPS_DB_NAME = + "taler-wallet-stored-backups"; export const CURRENT_DB_CONFIG_KEY = "currentMainDbName"; @@ -2833,11 +2836,10 @@ export async function exportSingleDb( dbName, undefined, () => { - // May not happen, since we're not requesting a specific version - throw Error("unexpected version change"); + logger.info(`unexpected onversionchange in exportSingleDb of ${dbName}`); }, () => { - logger.info("unexpected onupgradeneeded"); + logger.info(`unexpected onupgradeneeded in exportSingleDb of ${dbName}`); }, ); @@ -2849,7 +2851,7 @@ export async function exportSingleDb( return new Promise((resolve, reject) => { const tx = myDb.transaction(Array.from(myDb.objectStoreNames)); tx.addEventListener("complete", () => { - myDb.close(); + //myDb.close(); resolve(singleDbDump); }); // tslint:disable-next-line:prefer-for-of @@ -2885,6 +2887,7 @@ export async function exportSingleDb( if (store.keyPath == null) { rec.key = structuredEncapsulate(cursor.key); } + storeDump.records.push(rec); cursor.continue(); } }); @@ -2913,21 +2916,22 @@ async function recoverFromDump( db: IDBDatabase, dbDump: DbDumpDatabase, ): Promise { - return new Promise((resolve, reject) => { - const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); - tx.addEventListener("complete", () => { - resolve(); - }); - for (let i = 0; i < db.objectStoreNames.length; i++) { - const name = db.objectStoreNames[i]; - const storeDump = dbDump.stores[name]; - if (!storeDump) continue; - for (let rec of storeDump.records) { - tx.objectStore(name).put(rec.value, rec.key); - } + const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); + const txProm = promiseFromTransaction(tx); + const storeNames = db.objectStoreNames; + for (let i = 0; i < storeNames.length; i++) { + const name = db.objectStoreNames[i]; + const storeDump = dbDump.stores[name]; + if (!storeDump) continue; + await promiseFromRequest(tx.objectStore(name).clear()); + logger.info(`importing ${storeDump.records.length} records into ${name}`); + for (let rec of storeDump.records) { + await promiseFromRequest(tx.objectStore(name).put(rec.value, rec.key)); + logger.info("importing record done"); } - tx.commit(); - }); + } + tx.commit(); + return await txProm; } function checkDbDump(x: any): x is DbDump { @@ -3184,6 +3188,17 @@ function promiseFromTransaction(transaction: IDBTransaction): Promise { }); } +export function promiseFromRequest(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => { + resolve(request.result); + }; + request.onerror = () => { + reject(request.error); + }; + }); +} + /** * Purge all data in the given database. */ diff --git a/packages/taler-wallet-core/src/host-impl.node.ts b/packages/taler-wallet-core/src/host-impl.node.ts index 0b6539306..0626b9254 100644 --- a/packages/taler-wallet-core/src/host-impl.node.ts +++ b/packages/taler-wallet-core/src/host-impl.node.ts @@ -52,7 +52,6 @@ interface MakeDbResult { async function makeFileDb( args: DefaultNodeWalletArgs = {}, ): Promise { - BridgeIDBFactory.enableTracing = false; const myBackend = new MemoryBackend(); myBackend.enableTracing = false; const storagePath = args.persistentStoragePath; @@ -141,7 +140,10 @@ export async function createNativeWalletHost2( let dbResp: MakeDbResult; - if (args.persistentStoragePath &&args.persistentStoragePath.endsWith(".json")) { + if ( + args.persistentStoragePath && + args.persistentStoragePath.endsWith(".json") + ) { logger.info("using legacy file-based DB backend"); dbResp = await makeFileDb(args); } else { diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 626409dd6..5666d67e0 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -121,6 +121,11 @@ import { GetCurrencyInfoResponse, codecForGetCurrencyInfoRequest, CreateStoredBackupResponse, + StoredBackupList, + codecForDeleteStoredBackupRequest, + DeleteStoredBackupRequest, + RecoverStoredBackupRequest, + codecForRecoverStoredBackupRequest, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -1041,6 +1046,57 @@ async function createStoredBackup( }; } +async function listStoredBackups( + ws: InternalWalletState, +): Promise { + const storedBackups: StoredBackupList = { + storedBackups: [], + }; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupMeta.iter().forEach((x) => { + storedBackups.storedBackups.push({ + name: x.name, + }); + }); + }); + return storedBackups; +} + +async function deleteStoredBackup( + ws: InternalWalletState, + req: DeleteStoredBackupRequest, +): Promise { + const backupsDb = await openStoredBackupsDatabase(ws.idb); + await backupsDb.mktxAll().runReadWrite(async (tx) => { + await tx.backupData.delete(req.name); + await tx.backupMeta.delete(req.name); + }); +} + +async function recoverStoredBackup( + ws: InternalWalletState, + req: RecoverStoredBackupRequest, +): Promise { + logger.info(`Recovering stored backup ${req.name}`); + const { name } = req; + const backupsDb = await openStoredBackupsDatabase(ws.idb); + const bd = await backupsDb.mktxAll().runReadWrite(async (tx) => { + const backupMeta = tx.backupMeta.get(name); + if (!backupMeta) { + throw Error("backup not found"); + } + const backupData = await tx.backupData.get(name); + if (!backupData) { + throw Error("no backup data (DB corrupt)"); + } + return backupData; + }); + logger.info(`backup found, now importing`); + await importDb(ws.db.idbHandle(), bd); + logger.info(`import done`); +} + /** * Implementation of the "wallet-core" API. */ @@ -1059,12 +1115,18 @@ async function dispatchRequestInternal( switch (operation) { case WalletApiOperation.CreateStoredBackup: return createStoredBackup(ws); - case WalletApiOperation.DeleteStoredBackup: + case WalletApiOperation.DeleteStoredBackup: { + const req = codecForDeleteStoredBackupRequest().decode(payload); + await deleteStoredBackup(ws, req); return {}; + } case WalletApiOperation.ListStoredBackups: + return listStoredBackups(ws); + case WalletApiOperation.RecoverStoredBackup: { + const req = codecForRecoverStoredBackupRequest().decode(payload); + await recoverStoredBackup(ws, req); return {}; - case WalletApiOperation.RecoverStoredBackup: - return {}; + } case WalletApiOperation.InitWallet: { logger.trace("initializing wallet"); ws.initCalled = true; From 1c3e9473fd81761d01fafce1ddce8f3f80d35385 Mon Sep 17 00:00:00 2001 From: Florian Dold Date: Mon, 4 Sep 2023 14:25:06 +0200 Subject: [PATCH 02/34] -remove bogus logging --- packages/taler-harness/src/harness/harness.ts | 3 --- packages/taler-harness/src/index.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts index 7db9d82bd..337f3ca44 100644 --- a/packages/taler-harness/src/harness/harness.ts +++ b/packages/taler-harness/src/harness/harness.ts @@ -1407,9 +1407,7 @@ export class MerchantApiClient { } async getPrivateInstanceInfo(): Promise { - console.log(this.makeAuthHeader()); const url = new URL("private", this.baseUrl); - logger.info(`request url ${url.href}`); const resp = await this.httpClient.fetch(url.href, { method: "GET", headers: this.makeAuthHeader(), @@ -1418,7 +1416,6 @@ export class MerchantApiClient { } async getPrivateTipReserves(): Promise { - console.log(this.makeAuthHeader()); const url = new URL("private/reserves", this.baseUrl); const resp = await this.httpClient.fetch(url.href, { method: "GET", diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts index cd688ed89..3b50acf75 100644 --- a/packages/taler-harness/src/index.ts +++ b/packages/taler-harness/src/index.ts @@ -402,7 +402,6 @@ deploymentCli ); const res = await merchantClient.getPrivateInstanceInfo(); - console.log(res); const tipRes = await merchantClient.getPrivateTipReserves(); console.log(j2s(tipRes)); From 241a37c889d132423676b95af18f9349d0501298 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 12:20:39 -0300 Subject: [PATCH 03/34] add payto type --- packages/taler-util/src/bitcoin.ts | 8 ++++---- packages/taler-util/src/payto.ts | 6 ++++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/taler-util/src/bitcoin.ts b/packages/taler-util/src/bitcoin.ts index 8c22ba522..37b7ae6b9 100644 --- a/packages/taler-util/src/bitcoin.ts +++ b/packages/taler-util/src/bitcoin.ts @@ -69,10 +69,10 @@ export function generateFakeSegwitAddress( addr[0] === "t" && addr[1] == "b" ? "tb" : addr[0] === "b" && addr[1] == "c" && addr[2] === "r" && addr[3] == "t" - ? "bcrt" - : addr[0] === "b" && addr[1] == "c" - ? "bc" - : undefined; + ? "bcrt" + : addr[0] === "b" && addr[1] == "c" + ? "bc" + : undefined; if (prefix === undefined) throw new Error("unknown bitcoin net"); const addr1 = segwit.default.encode(prefix, 0, first_part); diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index 2b0af4cc2..60c4ba838 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -24,7 +24,7 @@ export type PaytoUri = | PaytoUriBitcoin; export interface PaytoUriGeneric { - targetType: string; + targetType: PaytoType | string; targetPath: string; params: { [name: string]: string }; } @@ -55,6 +55,8 @@ export interface PaytoUriBitcoin extends PaytoUriGeneric { const paytoPfx = "payto://"; +export type PaytoType = "iban" | "bitcoin" | "x-taler-bank" + export function buildPayto( type: "iban", iban: string, @@ -71,7 +73,7 @@ export function buildPayto( account: string, ): PaytoUriTalerBank; export function buildPayto( - type: "iban" | "bitcoin" | "x-taler-bank", + type: PaytoType, first: string, second?: string, ): PaytoUriGeneric { From ff20c3e25e076c24f7cb93eabe58b6f934f51f35 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 12:21:39 -0300 Subject: [PATCH 04/34] upgrade swr --- packages/merchant-backoffice-ui/package.json | 6 ++--- pnpm-lock.yaml | 24 +++++++++++--------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index 3a0c22adb..3d568a502 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "@gnu-taler/merchant-backoffice-ui", - "version": "0.0.5", + "version": "0.1.0", "license": "AGPL-3.0-or-later", "type": "module", "scripts": { @@ -41,7 +41,7 @@ "preact": "10.11.3", "preact-router": "3.2.1", "qrcode-generator": "1.4.4", - "swr": "1.3.0", + "swr": "2.2.2", "yup": "^0.32.9" }, "devDependencies": { @@ -81,4 +81,4 @@ "pogen": { "domain": "taler-merchant-backoffice" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b733353b..9a389bf50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -452,8 +452,8 @@ importers: specifier: 1.4.4 version: 1.4.4 swr: - specifier: 1.3.0 - version: 1.3.0(react@18.2.0) + specifier: 2.2.2 + version: 2.2.2(react@18.2.0) yup: specifier: ^0.32.9 version: 0.32.11 @@ -16350,14 +16350,6 @@ packages: stable: 0.1.8 dev: true - /swr@1.3.0(react@18.2.0): - resolution: {integrity: sha512-dkghQrOl2ORX9HYrMDtPa7LTVHJjCTeZoB1dqTbnnEDlSvN8JEKpYIYurDfvbQFUUS8Cg8PceFVZNkW0KNNYPw==} - peerDependencies: - react: ^16.11.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - dev: false - /swr@2.0.3(react@18.2.0): resolution: {integrity: sha512-sGvQDok/AHEWTPfhUWXEHBVEXmgGnuahyhmRQbjl9XBYxT/MSlAzvXEKQpyM++bMPaI52vcWS2HiKNaW7+9OFw==} engines: {pnpm: '7'} @@ -16367,6 +16359,16 @@ packages: react: 18.2.0 use-sync-external-store: 1.2.0(react@18.2.0) + /swr@2.2.2(react@18.2.0): + resolution: {integrity: sha512-CbR41AoMD4TQBQw9ic3GTXspgfM9Y8Mdhb5Ob4uIKXhWqnRLItwA5fpGvB7SmSw3+zEjb0PdhiEumtUvYoQ+bQ==} + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 + dependencies: + client-only: 0.0.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + /symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: true From e1d86816a7c07cb8ca2d54676d5cdbbe513f2ba7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 4 Sep 2023 14:17:55 -0300 Subject: [PATCH 05/34] backoffcie new version, lot of changes --- .../src/Application.tsx | 60 ++- .../src/ApplicationReadyRoutes.tsx | 86 ++-- .../src/InstanceRoutes.tsx | 219 ++++++--- .../src/components/exception/login.tsx | 4 +- .../src/components/form/InputDate.tsx | 11 +- .../src/components/form/InputPaytoForm.tsx | 305 +++---------- ...earchProduct.tsx => InputSearchOnList.tsx} | 97 ++-- .../src/components/form/InputToggle.tsx | 4 +- .../instance/DefaultInstanceFormFields.tsx | 31 +- .../src/components/menu/SideBar.tsx | 81 +++- .../src/components/menu/index.tsx | 58 ++- .../product/InventoryProductForm.tsx | 8 +- .../src/components/product/ProductForm.tsx | 18 +- .../src/context/backend.ts | 52 +-- .../src/declaration.d.ts | 428 +++++++++++------- .../src/hooks/backend.ts | 111 +++-- .../merchant-backoffice-ui/src/hooks/bank.ts | 217 +++++++++ .../merchant-backoffice-ui/src/hooks/index.ts | 23 +- .../src/hooks/instance.test.ts | 6 +- .../src/hooks/instance.ts | 5 +- .../merchant-backoffice-ui/src/hooks/otp.ts | 223 +++++++++ .../src/hooks/reserve.test.ts | 114 ++--- .../src/hooks/reserves.ts | 64 +-- .../merchant-backoffice-ui/src/hooks/urls.ts | 34 +- .../src/hooks/useSettings.ts | 39 +- .../src/paths/admin/create/CreatePage.tsx | 96 ++-- .../accounts/create/Create.stories.tsx | 28 ++ .../instance/accounts/create/CreatePage.tsx | 175 +++++++ .../paths/instance/accounts/create/index.tsx | 65 +++ .../instance/accounts/list/List.stories.tsx | 28 ++ .../paths/instance/accounts/list/ListPage.tsx | 64 +++ .../paths/instance/accounts/list/Table.tsx | 385 ++++++++++++++++ .../paths/instance/accounts/list/index.tsx | 107 +++++ .../accounts/update/Update.stories.tsx | 32 ++ .../instance/accounts/update/UpdatePage.tsx | 114 +++++ .../paths/instance/accounts/update/index.tsx | 96 ++++ .../src/paths/instance/details/DetailPage.tsx | 10 +- .../src/paths/instance/details/stories.tsx | 6 +- .../instance/kyc/list/ListPage.stories.tsx | 2 +- .../src/paths/instance/kyc/list/ListPage.tsx | 6 +- .../instance/orders/create/Create.stories.tsx | 7 +- .../instance/orders/create/CreatePage.tsx | 161 +++++-- .../paths/instance/orders/create/index.tsx | 6 +- .../orders/details/Detail.stories.tsx | 2 - .../instance/orders/details/DetailPage.tsx | 28 +- .../instance/orders/details/Timeline.tsx | 5 +- .../paths/instance/orders/list/ListPage.tsx | 42 +- .../src/paths/instance/orders/list/Table.tsx | 27 +- .../src/paths/instance/orders/list/index.tsx | 18 +- .../paths/instance/products/list/Table.tsx | 62 +-- .../paths/instance/products/list/index.tsx | 52 ++- .../instance/reserves/create/CreatePage.tsx | 14 +- .../reserves/create/CreatedSuccessfully.tsx | 22 +- .../paths/instance/reserves/create/index.tsx | 8 +- .../instance/reserves/details/DetailPage.tsx | 36 +- .../reserves/details/Details.stories.tsx | 6 +- .../details/{TipInfo.tsx => RewardInfo.tsx} | 23 +- ...zeTipModal.tsx => AutorizeRewardModal.tsx} | 40 +- .../reserves/list/CreatedSuccessfully.tsx | 18 +- .../instance/reserves/list/List.stories.tsx | 6 - .../paths/instance/reserves/list/Table.tsx | 39 +- .../paths/instance/reserves/list/index.tsx | 82 +++- .../instance/templates/create/CreatePage.tsx | 127 ++---- .../paths/instance/templates/list/index.tsx | 55 ++- .../paths/instance/templates/qr/QrPage.tsx | 60 +-- .../src/paths/instance/templates/qr/index.tsx | 2 +- .../instance/templates/update/UpdatePage.tsx | 121 +---- .../src/paths/instance/token/DetailPage.tsx | 165 +++++++ .../src/paths/instance/token/index.tsx | 90 ++++ .../src/paths/instance/token/stories.tsx | 28 ++ .../paths/instance/transfers/create/index.tsx | 3 +- .../paths/instance/transfers/list/Table.tsx | 10 +- .../paths/instance/transfers/list/index.tsx | 3 +- .../paths/instance/update/Update.stories.tsx | 6 +- .../src/paths/instance/update/UpdatePage.tsx | 114 ++--- .../validators/create/Create.stories.tsx | 28 ++ .../instance/validators/create/CreatePage.tsx | 195 ++++++++ .../validators/create/CreatedSuccessfully.tsx | 104 +++++ .../instance/validators/create/index.tsx | 70 +++ .../instance/validators/list/List.stories.tsx | 28 ++ .../instance/validators/list/ListPage.tsx | 64 +++ .../paths/instance/validators/list/Table.tsx | 213 +++++++++ .../paths/instance/validators/list/index.tsx | 106 +++++ .../validators/update/Update.stories.tsx | 32 ++ .../instance/validators/update/UpdatePage.tsx | 185 ++++++++ .../instance/validators/update/index.tsx | 102 +++++ .../paths/instance/webhooks/list/Table.tsx | 5 - .../src/paths/settings/index.tsx | 105 +++-- .../src/schemas/index.ts | 4 +- .../src/wallet/ProviderAddPage.tsx | 7 +- 90 files changed, 4665 insertions(+), 1583 deletions(-) rename packages/merchant-backoffice-ui/src/components/form/{InputSearchProduct.tsx => InputSearchOnList.tsx} (65%) create mode 100644 packages/merchant-backoffice-ui/src/hooks/bank.ts create mode 100644 packages/merchant-backoffice-ui/src/hooks/otp.ts create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/accounts/update/index.tsx rename packages/merchant-backoffice-ui/src/paths/instance/reserves/details/{TipInfo.tsx => RewardInfo.tsx} (77%) rename packages/merchant-backoffice-ui/src/paths/instance/reserves/list/{AutorizeTipModal.tsx => AutorizeRewardModal.tsx} (74%) create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/DetailPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/token/stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/Create.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/CreatedSuccessfully.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/create/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/List.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/ListPage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/Table.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/list/index.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/Update.stories.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/UpdatePage.tsx create mode 100644 packages/merchant-backoffice-ui/src/paths/instance/validators/update/index.tsx diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx index f6a81ff8d..5e82821ae 100644 --- a/packages/merchant-backoffice-ui/src/Application.tsx +++ b/packages/merchant-backoffice-ui/src/Application.tsx @@ -19,19 +19,20 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { HttpStatusCode, LibtoolVersion } from "@gnu-taler/taler-util"; import { ErrorType, TranslationProvider, useTranslationContext, } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { route } from "preact-router"; -import { useMemo, useState } from "preact/hooks"; +import { useMemo } from "preact/hooks"; import { ApplicationReadyRoutes } from "./ApplicationReadyRoutes.js"; import { Loading } from "./components/exception/loading.js"; import { - NotificationCard, - NotYetReadyAppMenu, + NotConnectedAppMenu, + NotificationCard } from "./components/menu/index.js"; import { BackendContextProvider, @@ -41,23 +42,24 @@ import { ConfigContextProvider } from "./context/config.js"; import { useBackendConfig } from "./hooks/backend.js"; import { strings } from "./i18n/strings.js"; import LoginPage from "./paths/login/index.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; -import { Settings } from "./paths/settings/index.js"; export function Application(): VNode { return ( - // - // ); } +/** + * Check connection testing against /config + * + * @returns + */ function ApplicationStatusRoutes(): VNode { - const { updateLoginStatus, triedToLog } = useBackendContext(); + const { url, updateLoginStatus, triedToLog } = useBackendContext(); const result = useBackendConfig(); const { i18n } = useTranslationContext(); @@ -71,19 +73,10 @@ function ApplicationStatusRoutes(): VNode { : { currency: "unknown", version: "unknown" }; const ctx = useMemo(() => ({ currency, version }), [currency, version]); - const [showSettings, setShowSettings] = useState(false) - - if (showSettings) { - return - setShowSettings(true)} title="UI Settings" /> - - - } - if (!triedToLog) { return ( - setShowSettings(true)} /> + ); @@ -97,7 +90,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( - setShowSettings(true)} /> + ); @@ -108,7 +101,7 @@ function ApplicationStatusRoutes(): VNode { ) { return ( - setShowSettings(true)} /> + - setShowSettings(true)} /> + - setShowSettings(true)} /> + - setShowSettings(true)} /> + + + + + + + } + return (
diff --git a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx index 277c2b176..46dea98e3 100644 --- a/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/ApplicationReadyRoutes.tsx @@ -22,7 +22,7 @@ import { ErrorType, useTranslationContext } from "@gnu-taler/web-util/browser"; import { createHashHistory } from "history"; import { Fragment, h, VNode } from "preact"; import { Router, Route, route } from "preact-router"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; import { NotificationCard, NotYetReadyAppMenu, @@ -35,52 +35,55 @@ import { INSTANCE_ID_LOOKUP } from "./utils/constants.js"; import { HttpStatusCode } from "@gnu-taler/taler-util"; import { Settings } from "./paths/settings/index.js"; +/** + * Check if admin against /management/instances + * @returns + */ export function ApplicationReadyRoutes(): VNode { const { i18n } = useTranslationContext(); + const [unauthorized, setUnauthorized] = useState(false) const { url: backendURL, - updateLoginStatus, - clearAllTokens, + updateLoginStatus: updateLoginStatus2, } = useBackendContext(); + function updateLoginStatus(url: string, token: string | undefined) { + console.log("updateing", url, token) + updateLoginStatus2(url, token) + setUnauthorized(false) + } + const result = useBackendInstancesTestForAdmin(); const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); }; const [showSettings, setShowSettings] = useState(false) + // useEffect(() => { + // setUnauthorized(FF) + // }, [FF]) + const unauthorizedAdmin = !result.loading && !result.ok && result.type === ErrorType.CLIENT && result.status === HttpStatusCode.Unauthorized if (showSettings) { return - setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} /> - + setShowSettings(true)} title="UI Settings" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + } - if (result.loading) return setShowSettings(true)} title="Loading..." />; - let admin = true; - let instanceNameByBackendURL; + if (result.loading) { + return setShowSettings(true)} title="Loading..." isPasswordOk={false} />; + } - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) { - return ( - - setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} /> - - - - ); - } + let admin = result.ok || unauthorizedAdmin; + let instanceNameByBackendURL: string | undefined; + + if (!admin) { + // * the testing against admin endpoint failed and it's not + // an authorization problem + // * merchant backend will return this SPA under the main + // endpoint or /instance/ endpoint + // => trying to infer the instance id const path = new URL(backendURL).pathname; const match = INSTANCE_ID_LOOKUP.exec(path); if (!match || !match[1]) { @@ -89,7 +92,7 @@ export function ApplicationReadyRoutes(): VNode { // does not match our pattern return ( - setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} /> + setShowSettings(true)} title="Error" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + setShowSettings(true)} title="Login" onLogout={clearTokenAndGoToRoot} isPasswordOk={false} /> + + + + } + const history = createHashHistory(); return ( @@ -113,6 +130,11 @@ export function ApplicationReadyRoutes(): VNode { default component={DefaultMainRoute} admin={admin} + onUnauthorized={() => setUnauthorized(true)} + onLoginPass={() => { + console.log("ahora si") + setUnauthorized(false) + }} instanceNameByBackendURL={instanceNameByBackendURL} /> @@ -122,6 +144,8 @@ export function ApplicationReadyRoutes(): VNode { function DefaultMainRoute({ instance, admin, + onUnauthorized, + onLoginPass, instanceNameByBackendURL, url, //from preact-router }: any): VNode { @@ -133,6 +157,8 @@ function DefaultMainRoute({ diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index 1547442ea..4a4b3fee4 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -40,6 +40,7 @@ import { import { useInstanceKYCDetails } from "./hooks/instance.js"; import InstanceCreatePage from "./paths/admin/create/index.js"; import InstanceListPage from "./paths/admin/list/index.js"; +import TokenPage from "./paths/instance/token/index.js"; import ListKYCPage from "./paths/instance/kyc/list/index.js"; import OrderCreatePage from "./paths/instance/orders/create/index.js"; import OrderDetailsPage from "./paths/instance/orders/details/index.js"; @@ -47,6 +48,9 @@ import OrderListPage from "./paths/instance/orders/list/index.js"; import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; +import BankAccountCreatePage from "./paths/instance/accounts/create/index.js"; +import BankAccountListPage from "./paths/instance/accounts/list/index.js"; +import BankAccountUpdatePage from "./paths/instance/accounts/update/index.js"; import ReservesCreatePage from "./paths/instance/reserves/create/index.js"; import ReservesDetailsPage from "./paths/instance/reserves/details/index.js"; import ReservesListPage from "./paths/instance/reserves/list/index.js"; @@ -58,6 +62,9 @@ import TemplateUpdatePage from "./paths/instance/templates/update/index.js"; import WebhookCreatePage from "./paths/instance/webhooks/create/index.js"; import WebhookListPage from "./paths/instance/webhooks/list/index.js"; import WebhookUpdatePage from "./paths/instance/webhooks/update/index.js"; +import ValidatorCreatePage from "./paths/instance/validators/create/index.js"; +import ValidatorListPage from "./paths/instance/validators/list/index.js"; +import ValidatorUpdatePage from "./paths/instance/validators/update/index.js"; import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { @@ -69,11 +76,16 @@ import NotFoundPage from "./paths/notfound/index.js"; import { Notification } from "./utils/types.js"; import { MerchantBackend } from "./declaration.js"; import { Settings } from "./paths/settings/index.js"; +import { dateFormatForSettings, useSettings } from "./hooks/useSettings.js"; export enum InstancePaths { - // details = '/', error = "/error", - update = "/update", + server = "/server", + token = "/token", + + bank_list = "/bank", + bank_update = "/bank/:bid/update", + bank_new = "/bank/new", product_list = "/products", product_update = "/product/:pid/update", @@ -102,11 +114,15 @@ export enum InstancePaths { webhooks_update = "/webhooks/:tid/update", webhooks_new = "/webhooks/new", - settings = "/settings", + validators_list = "/validators", + validators_update = "/validators/:vid/update", + validators_new = "/validators/new", + + settings = "/inteface", } // eslint-disable-next-line @typescript-eslint/no-empty-function -const noop = () => {}; +const noop = () => { }; export enum AdminPaths { list_instances = "/instances", @@ -118,6 +134,8 @@ export interface Props { id: string; admin?: boolean; path: string; + onUnauthorized: () => void; + onLoginPass: () => void; setInstanceName: (s: string) => void; } @@ -125,40 +143,29 @@ export function InstanceRoutes({ id, admin, path, + onUnauthorized, + onLoginPass, setInstanceName, }: Props): VNode { - const [_, updateDefaultToken] = useBackendDefaultToken(); + const [defaultToken, updateDefaultToken] = useBackendDefaultToken(); const [token, updateToken] = useBackendInstanceToken(id); - const { - updateLoginStatus: changeBackend, - addTokenCleaner, - clearAllTokens, - } = useBackendContext(); - const cleaner = useCallback(() => { - updateToken(undefined); - }, [id]); const { i18n } = useTranslationContext(); type GlobalNotifState = (Notification & { to: string }) | undefined; const [globalNotification, setGlobalNotification] = useState(undefined); - useEffect(() => { - addTokenCleaner(cleaner); - }, [addTokenCleaner, cleaner]); - const changeToken = (token?: string) => { if (admin) { updateToken(token); } else { updateDefaultToken(token); } + onLoginPass() }; - const updateLoginStatus = (url: string, token?: string) => { - changeBackend(url); - if (!token) return; - changeToken(token); - }; + // const updateLoginStatus = (url: string, token?: string) => { + // changeToken(token); + // }; const value = useMemo( () => ({ id, token, admin, changeToken }), @@ -192,18 +199,17 @@ export function InstanceRoutes({ }; } - const LoginPageAccessDenied = () => ( - - - - - ); + // const LoginPageAccessDeniend = onUnauthorized + const LoginPageAccessDenied = () => { + onUnauthorized() + return + } function IfAdminCreateDefaultOr(Next: FunctionComponent) { return function IfAdminCreateDefaultOrImpl(props?: T) { @@ -234,8 +240,10 @@ export function InstanceRoutes({ } const clearTokenAndGoToRoot = () => { - clearAllTokens(); route("/"); + // clear all tokens + updateToken(undefined) + updateDefaultToken(undefined) }; return ( @@ -244,11 +252,12 @@ export function InstanceRoutes({ instance={id} admin={admin} onShowSettings={() => { - route("/settings") + route("/inteface") }} path={path} onLogout={clearTokenAndGoToRoot} setInstanceName={setInstanceName} + isPasswordOk={defaultToken !== undefined} /> @@ -308,7 +317,7 @@ export function InstanceRoutes({ * Update instance page */} { route(`/`); @@ -321,6 +330,19 @@ export function InstanceRoutes({ onUnauthorized={LoginPageAccessDenied} onLoadError={ServerErrorRedirectTo(InstancePaths.error)} /> + {/** + * Update instance page + */} + { + route(`/`); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.error)} + /> {/** * Product pages */} @@ -328,7 +350,7 @@ export function InstanceRoutes({ path={InstancePaths.product_list} component={ProductListPage} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.product_new); }} @@ -360,6 +382,45 @@ export function InstanceRoutes({ route(InstancePaths.product_list); }} /> + {/** + * Bank pages + */} + { + route(InstancePaths.bank_new); + }} + onSelect={(id: string) => { + route(InstancePaths.bank_update.replace(":bid", id)); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + /> + { + route(InstancePaths.bank_list); + }} + onBack={() => { + route(InstancePaths.bank_list); + }} + /> {/** * Order pages */} @@ -373,7 +434,7 @@ export function InstanceRoutes({ route(InstancePaths.order_details.replace(":oid", id)); }} onUnauthorized={LoginPageAccessDenied} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} /> { - route(InstancePaths.order_list); + onConfirm={(orderId: string) => { + route(InstancePaths.order_details.replace(":oid", orderId)); }} onBack={() => { route(InstancePaths.order_list); @@ -404,7 +465,7 @@ export function InstanceRoutes({ component={TransferListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.transfers_new); }} @@ -427,7 +488,7 @@ export function InstanceRoutes({ component={WebhookListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.webhooks_new); }} @@ -458,6 +519,45 @@ export function InstanceRoutes({ route(InstancePaths.webhooks_list); }} /> + {/** + * Validator pages + */} + { + route(InstancePaths.validators_new); + }} + onSelect={(id: string) => { + route(InstancePaths.validators_update.replace(":vid", id)); + }} + /> + { + route(InstancePaths.validators_list); + }} + onUnauthorized={LoginPageAccessDenied} + onLoadError={ServerErrorRedirectTo(InstancePaths.validators_list)} + onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> + { + route(InstancePaths.validators_list); + }} + onBack={() => { + route(InstancePaths.validators_list); + }} + /> {/** * Templates pages */} @@ -466,7 +566,7 @@ export function InstanceRoutes({ component={TemplateListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onCreate={() => { route(InstancePaths.templates_new); }} @@ -535,7 +635,7 @@ export function InstanceRoutes({ component={ReservesListPage} onUnauthorized={LoginPageAccessDenied} onNotFound={IfAdminCreateDefaultOr(NotFoundPage)} - onLoadError={ServerErrorRedirectTo(InstancePaths.update)} + onLoadError={ServerErrorRedirectTo(InstancePaths.server)} onSelect={(id: string) => { route(InstancePaths.reserves_details.replace(":rid", id)); }} @@ -590,7 +690,7 @@ function AdminInstanceUpdatePage({ const { updateLoginStatus: changeBackend } = useBackendContext(); const updateLoginStatus = (url: string, token?: string): void => { changeBackend(url); - if (token) changeToken(token); + changeToken(token); }; const value = useMemo( () => ({ id, token, admin: true, changeToken }), @@ -607,20 +707,20 @@ function AdminInstanceUpdatePage({ const notif = error.type === ErrorType.TIMEOUT ? { - message: i18n.str`The request to the backend take too long and was cancelled`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - type: "ERROR" as const, - } + message: i18n.str`The request to the backend take too long and was cancelled`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + type: "ERROR" as const, + } : { - message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, - description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, - details: - error.type === ErrorType.CLIENT || + message: i18n.str`The backend reported a problem: HTTP status #${error.status}`, + description: i18n.str`Diagnostic from ${error.info.url} is '${error.message}'`, + details: + error.type === ErrorType.CLIENT || error.type === ErrorType.SERVER - ? error.payload.detail - : undefined, - type: "ERROR" as const, - }; + ? error.payload.detail + : undefined, + type: "ERROR" as const, + }; return ( @@ -650,7 +750,8 @@ function AdminInstanceUpdatePage({ function KycBanner(): VNode { const kycStatus = useInstanceKYCDetails(); const { i18n } = useTranslationContext(); - const today = format(new Date(), "yyyy-MM-dd"); + const [settings] = useSettings(); + const today = format(new Date(), dateFormatForSettings(settings)); const [lastHide, setLastHide] = useLocalStorage("kyc-last-hide"); const hasBeenHidden = today === lastHide; const needsToBeShown = kycStatus.ok && kycStatus.data.type === "redirect"; diff --git a/packages/merchant-backoffice-ui/src/components/exception/login.tsx b/packages/merchant-backoffice-ui/src/components/exception/login.tsx index f2f94a7c5..4fa440fc7 100644 --- a/packages/merchant-backoffice-ui/src/components/exception/login.tsx +++ b/packages/merchant-backoffice-ui/src/components/exception/login.tsx @@ -93,7 +93,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { e.keyCode === 13 @@ -186,7 +186,7 @@ export function LoginModal({ onConfirm, withMessage }: Props): VNode { e.keyCode === 13 diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx index 1f41c3564..a398629dc 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputDate.tsx @@ -20,16 +20,18 @@ */ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; -import { h, VNode } from "preact"; +import { ComponentChildren, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { DatePicker } from "../picker/DatePicker.js"; import { InputProps, useField } from "./useField.js"; +import { dateFormatForSettings, useSettings } from "../../hooks/useSettings.js"; export interface Props extends InputProps { readonly?: boolean; expand?: boolean; //FIXME: create separated components InputDate and InputTimestamp withTimestampSupport?: boolean; + side?: ComponentChildren; } export function InputDate({ @@ -41,9 +43,11 @@ export function InputDate({ tooltip, expand, withTimestampSupport, + side, }: Props): VNode { const [opened, setOpened] = useState(false); const { i18n } = useTranslationContext(); + const [settings] = useSettings() const { error, required, value, onChange } = useField(name); @@ -51,14 +55,14 @@ export function InputDate({ if (!value) { strValue = withTimestampSupport ? "unknown" : ""; } else if (value instanceof Date) { - strValue = format(value, "yyyy/MM/dd"); + strValue = format(value, dateFormatForSettings(settings)); } else if (value.t_s) { strValue = value.t_s === "never" ? withTimestampSupport ? "never" : "" - : format(new Date(value.t_s * 1000), "yyyy/MM/dd"); + : format(new Date(value.t_s * 1000), dateFormatForSettings(settings)); } return ( @@ -142,6 +146,7 @@ export function InputDate({ )} + {side}
extends InputProps { isValid?: (e: any) => boolean; } +// type Entity = PaytoUriGeneric // https://datatracker.ietf.org/doc/html/rfc8905 type Entity = { // iban, bitcoin, x-taler-bank. it defined the format target: string; // path1 if the first field to be used - path1: string; + path1?: string; // path2 if the second field to be used, optional path2?: string; - // options of the payto uri - options: { + // params of the payto uri + params: { "receiver-name"?: string; sender?: string; message?: string; @@ -52,13 +52,6 @@ type Entity = { instruction?: string; [name: string]: string | undefined; }; - auth: { - type: "unset" | "basic" | "none"; - url?: string; - username?: string; - password?: string; - repeat?: string; - }; }; function isEthereumAddress(address: string) { @@ -171,14 +164,10 @@ const targets = [ "bitcoin", "ethereum", ]; -const accountAuthType = ["none", "basic"]; const noTargetValue = targets[0]; -const defaultTarget: Partial = { +const defaultTarget: Entity = { target: noTargetValue, - options: {}, - auth: { - type: "unset" as const, - }, + params: {}, }; export function InputPaytoForm({ @@ -187,110 +176,91 @@ export function InputPaytoForm({ label, tooltip, }: Props): VNode { - const { value: paytos, onChange, required } = useField(name); + const { value: initialValueStr, onChange } = useField(name); - const [value, valueHandler] = useState>(defaultTarget); - - let payToPath; - if (value.target === "iban" && value.path1) { - payToPath = `/${value.path1.toUpperCase()}`; - } else if (value.path1) { - if (value.path2) { - payToPath = `/${value.path1}/${value.path2}`; - } else { - payToPath = `/${value.path1}`; - } + const initialPayto = parsePaytoUri(initialValueStr ?? "") + const paths = !initialPayto ? [] : initialPayto.targetPath.split("/") + const initialPath1 = paths.length >= 1 ? paths[0] : undefined; + const initialPath2 = paths.length >= 2 ? paths[1] : undefined; + const initial: Entity = initialPayto === undefined ? defaultTarget : { + target: initialPayto.targetType, + params: initialPayto.params, + path1: initialPath1, + path2: initialPath2, } + const [value, setValue] = useState>(initial) + const { i18n } = useTranslationContext(); - const ops = value.options ?? {}; - const url = tryUrl(`payto://${value.target}${payToPath}`); - if (url) { - Object.keys(ops).forEach((opt_key) => { - const opt_value = ops[opt_key]; - if (opt_value) url.searchParams.set(opt_key, opt_value); - }); - } - const paytoURL = !url ? "" : url.href; - const errors: FormErrors = { target: - value.target === noTargetValue && !paytos.length + value.target === noTargetValue ? i18n.str`required` : undefined, path1: !value.path1 ? i18n.str`required` : value.target === "iban" - ? validateIBAN(value.path1, i18n) - : value.target === "bitcoin" - ? validateBitcoin(value.path1, i18n) - : value.target === "ethereum" - ? validateEthereum(value.path1, i18n) - : undefined, + ? validateIBAN(value.path1, i18n) + : value.target === "bitcoin" + ? validateBitcoin(value.path1, i18n) + : value.target === "ethereum" + ? validateEthereum(value.path1, i18n) + : undefined, path2: value.target === "x-taler-bank" ? !value.path2 ? i18n.str`required` : undefined : undefined, - options: undefinedIfEmpty({ - "receiver-name": !value.options?.["receiver-name"] + params: undefinedIfEmpty({ + "receiver-name": !value.params?.["receiver-name"] ? i18n.str`required` : undefined, }), - auth: !value.auth - ? undefined - : undefinedIfEmpty({ - username: - value.auth.type === "basic" && !value.auth.username - ? i18n.str`required` - : undefined, - password: - value.auth.type === "basic" && !value.auth.password - ? i18n.str`required` - : undefined, - repeat: - value.auth.type === "basic" && !value.auth.repeat - ? i18n.str`required` - : value.auth.repeat !== value.auth.password - ? i18n.str`is not the same` - : undefined, - }), }; const hasErrors = Object.keys(errors).some( (k) => (errors as any)[k] !== undefined, ); + const str = hasErrors || !value.target ? undefined : stringifyPaytoUri({ + targetType: value.target, + targetPath: value.path2 ? `${value.path1}/${value.path2}` : (value.path1 ?? ""), + params: value.params ?? {} as any, + isKnown: false, + }) + useEffect(() => { + onChange(str as any) + }, [str]) - const submit = useCallback((): void => { - const accounts: MerchantBackend.Instances.MerchantBankAccount[] = paytos; - const alreadyExists = - accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; - if (!alreadyExists) { - const newValue: MerchantBackend.Instances.MerchantBankAccount = { - payto_uri: paytoURL, - }; - if (value.auth) { - if (value.auth.url) { - newValue.credit_facade_url = value.auth.url; - } - if (value.auth.type === "none") { - newValue.credit_facade_credentials = { - type: "none", - }; - } - if (value.auth.type === "basic") { - newValue.credit_facade_credentials = { - type: "basic", - username: value.auth.username ?? "", - password: value.auth.password ?? "", - }; - } - } - onChange([newValue, ...accounts] as any); - } - valueHandler(defaultTarget); - }, [value]); + // const submit = useCallback((): void => { + // // const accounts: MerchantBackend.BankAccounts.AccountAddDetails[] = paytos; + // // const alreadyExists = + // // accounts.findIndex((x) => x.payto_uri === paytoURL) !== -1; + // // if (!alreadyExists) { + // const newValue: MerchantBackend.BankAccounts.AccountAddDetails = { + // payto_uri: paytoURL, + // }; + // if (value.auth) { + // if (value.auth.url) { + // newValue.credit_facade_url = value.auth.url; + // } + // if (value.auth.type === "none") { + // newValue.credit_facade_credentials = { + // type: "none", + // }; + // } + // if (value.auth.type === "basic") { + // newValue.credit_facade_credentials = { + // type: "basic", + // username: value.auth.username ?? "", + // password: value.auth.password ?? "", + // }; + // } + // } + // onChange(newValue as any); + // // } + // // valueHandler(defaultTarget); + // }, [value]); //FIXME: translating plural singular return ( @@ -299,11 +269,11 @@ export function InputPaytoForm({ name="tax" errors={errors} object={value} - valueHandler={valueHandler} + valueHandler={setValue} > name="target" - label={i18n.str`Target type`} + label={i18n.str`Account type`} tooltip={i18n.str`Method to use for wire transfer`} values={targets} toStr={(v) => (v === noTargetValue ? i18n.str`Choose one...` : v)} @@ -400,150 +370,15 @@ export function InputPaytoForm({ {value.target !== noTargetValue && ( - - { - // if (str === "unset") { - // return "Without change"; - // } - if (str === "none") return "Without authentication"; - return "Username and password"; - }} - /> - {value.auth?.type === "basic" ? ( - - - - - - ) : undefined} - - {/* v.toUpperCase()} - addonAfter={ - - {showKey ? ( - - ) : ( - - )} - - } - side={ - - - - } - /> */} )} - {/** - * Show the values in the list - */} -
- - {value.target !== noTargetValue && ( -
- -
- )} ); } -function tryUrl(s: string): URL | undefined { - try { - return new URL(s); - } catch (e) { - return undefined; - } -} diff --git a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx similarity index 65% rename from packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx rename to packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx index 1c1fcb907..be5800d14 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputSearchProduct.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputSearchOnList.tsx @@ -22,32 +22,41 @@ import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { h, VNode } from "preact"; import { useState } from "preact/hooks"; import emptyImage from "../../assets/empty.png"; -import { MerchantBackend, WithId } from "../../declaration.js"; import { FormErrors, FormProvider } from "./FormProvider.js"; import { InputWithAddon } from "./InputWithAddon.js"; +import { TranslatedString } from "@gnu-taler/taler-util"; -type Entity = MerchantBackend.Products.ProductDetail & WithId; +type Entity = { + id: string, + description: string; + image?: string; + extra?: string; +}; -export interface Props { - selected?: Entity; - onChange: (p?: Entity) => void; - products: (MerchantBackend.Products.ProductDetail & WithId)[]; +export interface Props { + selected?: T; + onChange: (p?: T) => void; + label: TranslatedString; + list: T[]; + withImage?: boolean; } -interface ProductSearch { +interface Search { name: string; } -export function InputSearchProduct({ +export function InputSearchOnList({ selected, onChange, - products, -}: Props): VNode { - const [prodForm, setProdName] = useState>({ + label, + list, + withImage, +}: Props): VNode { + const [nameForm, setNameForm] = useState>({ name: "", }); - const errors: FormErrors = { + const errors: FormErrors = { name: undefined, }; const { i18n } = useTranslationContext(); @@ -55,15 +64,17 @@ export function InputSearchProduct({ if (selected) { return (
-
-

- -

-
+ {withImage && +
+

+ +

+
+ }

- Product id: {selected.id} + ID: {selected.id}

Description:{" "} @@ -84,15 +95,15 @@ export function InputSearchProduct({ } return ( - + errors={errors} - object={prodForm} - valueHandler={setProdName} + object={nameForm} + valueHandler={setNameForm} > - + name="name" - label={i18n.str`Product`} - tooltip={i18n.str`search products by it's description or id`} + label={label} + tooltip={i18n.str`enter description or id`} addonAfter={ @@ -100,13 +111,14 @@ export function InputSearchProduct({ } >

- { - setProdName({ name: "" }); + setNameForm({ name: "" }); onChange(p); }} + withImage={!!withImage} />
@@ -114,13 +126,14 @@ export function InputSearchProduct({ ); } -interface ProductListProps { +interface DropdownListProps { name?: string; - onSelect: (p: MerchantBackend.Products.ProductDetail & WithId) => void; - list: (MerchantBackend.Products.ProductDetail & WithId)[]; + onSelect: (p: T) => void; + list: T[]; + withImage: boolean; } -function ProductList({ name, onSelect, list }: ProductListProps) { +function DropdownList({ name, onSelect, list, withImage }: DropdownListProps) { const { i18n } = useTranslationContext(); if (!name) { /* FIXME @@ -149,7 +162,7 @@ function ProductList({ name, onSelect, list }: ProductListProps) { {!filtered.length ? ( ) : ( @@ -161,18 +174,20 @@ function ProductList({ name, onSelect, list }: ProductListProps) { style={{ cursor: "pointer" }} >
-
-
- + {withImage && +
+
+ +
-
+ }

- {p.id} {p.price} + {p.id} {p.extra !== undefined ? {p.extra} : undefined}
{p.description}

diff --git a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx index 61ddf3c84..f95dfcd05 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputToggle.tsx @@ -56,7 +56,7 @@ export function InputToggle({ return (
-
-
+

diff --git a/packages/merchant-backoffice-ui/src/components/menu/index.tsx b/packages/merchant-backoffice-ui/src/components/menu/index.tsx index cdbae4ae0..cb318906f 100644 --- a/packages/merchant-backoffice-ui/src/components/menu/index.tsx +++ b/packages/merchant-backoffice-ui/src/components/menu/index.tsx @@ -24,7 +24,7 @@ import { Sidebar } from "./SideBar.js"; function getInstanceTitle(path: string, id: string): string { switch (path) { - case InstancePaths.update: + case InstancePaths.server: return `${id}: Settings`; case InstancePaths.order_list: return `${id}: Orders`; @@ -50,6 +50,12 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: New webhook`; case InstancePaths.webhooks_update: return `${id}: Update webhook`; + case InstancePaths.validators_list: + return `${id}: Validators`; + case InstancePaths.validators_new: + return `${id}: New validator`; + case InstancePaths.validators_update: + return `${id}: Update validators`; case InstancePaths.templates_new: return `${id}: New template`; case InstancePaths.templates_update: @@ -58,6 +64,10 @@ function getInstanceTitle(path: string, id: string): string { return `${id}: Templates`; case InstancePaths.templates_use: return `${id}: Use template`; + case InstancePaths.settings: + return `${id}: Interface`; + case InstancePaths.settings: + return `${id}: Interface`; default: return ""; } @@ -77,6 +87,7 @@ interface MenuProps { onLogout?: () => void; onShowSettings: () => void; setInstanceName: (s: string) => void; + isPasswordOk: boolean; } function WithTitle({ @@ -100,14 +111,15 @@ export function Menu({ path, admin, setInstanceName, + isPasswordOk }: MenuProps): VNode { const [mobileOpen, setMobileOpen] = useState(false); const titleWithSubtitle = title ? title : !admin - ? getInstanceTitle(path, instance) - : getAdminTitle(path, instance); + ? getInstanceTitle(path, instance) + : getAdminTitle(path, instance); const adminInstance = instance === "default"; const mimic = admin && !adminInstance; return ( @@ -129,14 +141,15 @@ export function Menu({ mimic={mimic} instance={instance} mobile={mobileOpen} + isPasswordOk={isPasswordOk} /> )} {mimic && (