This commit is contained in:
Sebastian 2023-04-19 12:42:47 -03:00
parent 7330f0daf9
commit d483a3f557
No known key found for this signature in database
GPG Key ID: 173909D1A5F66069
48 changed files with 960 additions and 1011 deletions

View File

@ -725,6 +725,9 @@ export interface WalletCoreVersion {
exchange: string;
merchant: string;
bank: string;
/**
* @deprecated will be removed
*/
devMode: boolean;
}
@ -2157,15 +2160,6 @@ export interface ConfirmPeerPullDebitRequest {
peerPullPaymentIncomingId: string;
}
export interface SetDevModeRequest {
devModeEnabled: boolean;
}
export const codecForSetDevModeRequest = (): Codec<SetDevModeRequest> =>
buildCodecForObject<SetDevModeRequest>()
.property("devModeEnabled", codecForBoolean())
.build("SetDevModeRequest");
export interface ApplyDevExperimentRequest {
devExperimentUri: string;
}

View File

@ -27,25 +27,18 @@ import {
CoreApiResponse,
Duration,
encodeCrock,
ExtendedStatus,
getErrorDetailFromException,
getRandomBytes,
j2s,
Logger,
parsePaytoUri,
PaymentStatus,
PreparePayResultType,
RecoveryMergeStrategy,
RefreshReason,
sampleWalletCoreTransactions,
setDangerousTimetravel,
setGlobalLogLevelFromString,
summarizeTalerErrorDetail,
TalerUriType,
Transaction,
TransactionState,
TransactionSubstate,
TransactionType,
WalletNotification,
} from "@gnu-taler/taler-util";
import { clk } from "@gnu-taler/taler-util/clk";
@ -61,15 +54,9 @@ import { JsonMessage, runRpcServer } from "@gnu-taler/taler-util/twrpc";
import {
createNativeWalletHost,
createNativeWalletHost2,
CryptoDispatcher,
nativeCrypto,
//NodeThreadCryptoWorkerFactory,
//SynchronousCryptoWorkerFactoryPlain,
TalerCryptoInterface,
Wallet,
WalletApiOperation,
WalletCoreApiClient,
walletCoreDebugFlags,
} from "@gnu-taler/taler-wallet-core";
import {
createRemoteWallet,
@ -264,12 +251,19 @@ async function createLocalWallet(
}
},
cryptoWorkerType: walletCliArgs.wallet.cryptoWorker as any,
config: {
features: {
batchWithdrawal: checkEnvFlag("TALER_WALLET_BATCH_WITHDRAWAL"),
},
testing: {
devModeActive: checkEnvFlag("TALER_WALLET_DEV_MODE"),
denomselAllowLate: checkEnvFlag(
"TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE",
),
},
},
});
if (checkEnvFlag("TALER_WALLET_BATCH_WITHDRAWAL")) {
wallet.setBatchWithdrawal(true);
}
applyVerbose(walletCliArgs.wallet.verbose);
try {
await wallet.handleCoreApiRequest("initWallet", "native-init", {
@ -1405,30 +1399,6 @@ advancedCli
});
});
advancedCli
.subcommand("enableDevMode", "enable-dev-mode", {
help: "Enable developer mode (dangerous!)",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.SetDevMode, {
devModeEnabled: true,
});
});
});
advancedCli
.subcommand("disableDevMode", "disable-dev-mode", {
help: "Disable developer mode",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.SetDevMode, {
devModeEnabled: false,
});
});
});
const coinPubListCodec = codecForList(codecForString());
advancedCli
@ -1647,35 +1617,6 @@ async function read(stream: NodeJS.ReadStream) {
return Buffer.concat(chunks).toString("utf8");
}
// testCli
// .subcommand("cryptoworker", "cryptoworker")
// .maybeOption("impl", ["--impl"], clk.STRING)
// .action(async (args) => {
// let cryptoApi: TalerCryptoInterface;
// if (!args.cryptoworker.impl || args.cryptoworker.impl === "node") {
// const workerFactory = new NodeThreadCryptoWorkerFactory();
// const cryptoDisp = new CryptoDispatcher(workerFactory);
// cryptoApi = cryptoDisp.cryptoApi;
// } else if (args.cryptoworker.impl === "sync") {
// const workerFactory = new SynchronousCryptoWorkerFactoryPlain();
// const cryptoDisp = new CryptoDispatcher(workerFactory);
// cryptoApi = cryptoDisp.cryptoApi;
// } else if (args.cryptoworker.impl === "none") {
// cryptoApi = nativeCrypto;
// } else {
// throw Error(`invalid crypto worker type ${args.cryptoworker.impl}`);
// }
// const input = "foo";
// console.log(`testing crypto worker by hashing string '${input}'`);
// const res = await cryptoApi.hashString({ str: input });
// console.log(res);
// });
export function main() {
if (getenv("TALER_WALLET_DEBUG_DENOMSEL_ALLOW_LATE")) {
logger.warn("Allowing withdrawal of late denominations for debugging");
walletCoreDebugFlags.denomselAllowLate = true;
}
walletCli.run();
}

View File

@ -65,7 +65,7 @@ import {
} from "./operations/withdraw.js";
import { ExchangeInfo } from "./operations/exchanges.js";
import { assembleRefreshRevealRequest } from "./operations/refresh.js";
import { isWithdrawableDenom } from "./index.js";
import { isWithdrawableDenom, WalletConfig } from "./index.js";
const logger = new Logger("dbless.ts");
@ -206,6 +206,7 @@ export async function withdrawCoin(args: {
}
export function findDenomOrThrow(
denomselAllowLate: boolean,
exchangeInfo: ExchangeInfo,
amount: AmountString,
): DenominationRecord {
@ -215,7 +216,10 @@ export function findDenomOrThrow(
fraction: d.amountFrac,
value: d.amountVal,
};
if (Amounts.cmp(value, amount) === 0 && isWithdrawableDenom(d)) {
if (
Amounts.cmp(value, amount) === 0 &&
isWithdrawableDenom(d, denomselAllowLate)
) {
return d;
}
}

View File

@ -36,35 +36,6 @@ import {
const logger = new Logger("dev-experiments.ts");
export async function setDevMode(
ws: InternalWalletState,
enabled: boolean,
): Promise<void> {
if (enabled) {
logger.info("enabling devmode");
await ws.db
.mktx((x) => [x.config])
.runReadWrite(async (tx) => {
tx.config.put({
key: ConfigRecordKey.DevMode,
value: true,
});
});
await maybeInitDevMode(ws);
} else {
logger.info("disabling devmode");
await ws.db
.mktx((x) => [x.config])
.runReadWrite(async (tx) => {
tx.config.put({
key: ConfigRecordKey.DevMode,
value: false,
});
});
await leaveDevMode(ws);
}
}
/**
* Apply a dev experiment to the wallet database / state.
*/
@ -78,7 +49,7 @@ export async function applyDevExperiment(
logger.info("unable to parse dev experiment URI");
return;
}
if (!ws.devModeActive) {
if (!ws.config.testing.devModeActive) {
throw Error(
"can't handle devmode URI (other than enable-devmode) unless devmode is active",
);
@ -86,37 +57,6 @@ export async function applyDevExperiment(
throw Error(`dev-experiment id not understood ${parsedUri.devExperimentId}`);
}
/**
* Enter dev mode, if the wallet's config entry in the DB demands it.
*/
export async function maybeInitDevMode(ws: InternalWalletState): Promise<void> {
const devMode = await ws.db
.mktx((x) => [x.config])
.runReadOnly(async (tx) => {
const rec = await tx.config.get(ConfigRecordKey.DevMode);
if (!rec || rec.key !== ConfigRecordKey.DevMode) {
return false;
}
return rec.value;
});
if (!devMode) {
ws.devModeActive = false;
return;
}
ws.devModeActive = true;
if (ws.http instanceof DevExperimentHttpLib) {
return;
}
ws.http = new DevExperimentHttpLib(ws.http);
}
export async function leaveDevMode(ws: InternalWalletState): Promise<void> {
if (ws.http instanceof DevExperimentHttpLib) {
ws.http = ws.http.underlyingLib;
}
ws.devModeActive = false;
}
export class DevExperimentHttpLib implements HttpRequestLibrary {
_isDevExperimentLib = true;
underlyingLib: HttpRequestLibrary;

View File

@ -16,6 +16,7 @@
import { WalletNotification } from "@gnu-taler/taler-util";
import { HttpRequestLibrary } from "@gnu-taler/taler-util/http";
import { WalletConfig, WalletConfigParameter } from "./index.js";
/**
* Helpers to initiate a wallet in a host environment.
@ -44,6 +45,11 @@ export interface DefaultNodeWalletArgs {
httpLib?: HttpRequestLibrary;
cryptoWorkerType?: "sync" | "node-worker-thread";
/**
* Config parameters
*/
config?: WalletConfigParameter;
}
/**

View File

@ -144,7 +144,13 @@ export async function createNativeWalletHost2(
const timer = new SetTimeoutTimerAPI();
const w = await Wallet.create(myDb, myHttpLib, timer, workerFactory);
const w = await Wallet.create(
myDb,
myHttpLib,
timer,
workerFactory,
args.config,
);
if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler);

View File

@ -108,7 +108,13 @@ export async function createNativeWalletHost2(
const timer = new SetTimeoutTimerAPI();
const w = await Wallet.create(myDb, myHttpLib, timer, workerFactory);
const w = await Wallet.create(
myDb,
myHttpLib,
timer,
workerFactory,
args.config,
);
if (args.notifyHandler) {
w.addNotificationListener(args.notifyHandler);

View File

@ -36,7 +36,6 @@ export {
export * from "./pending-types.js";
export * from "./util/debugFlags.js";
export { InternalWalletState } from "./internal-wallet-state.js";
export * from "./wallet-api-types.js";
export * from "./wallet.js";

View File

@ -52,6 +52,7 @@ import {
GetReadWriteAccess,
} from "./util/query.js";
import { TimerGroup } from "./util/timer.js";
import { WalletConfig } from "./wallet-api-types.js";
export const EXCHANGE_COINS_LOCK = "exchange-coins-lock";
export const EXCHANGE_RESERVES_LOCK = "exchange-reserves-lock";
@ -168,9 +169,7 @@ export interface InternalWalletState {
timerGroup: TimerGroup;
stopped: boolean;
insecureTrustExchange: boolean;
batchWithdrawal: boolean;
config: Readonly<WalletConfig>;
/**
* Asynchronous condition to interrupt the sleep of the
@ -191,8 +190,6 @@ export interface InternalWalletState {
merchantOps: MerchantOperations;
refreshOps: RefreshOperations;
devModeActive: boolean;
getDenomInfo(
ws: InternalWalletState,
tx: GetReadOnlyAccess<{

View File

@ -840,7 +840,12 @@ export async function getTotalFeesForDepositAmount(
denom.value,
pcs.coinContributions[i],
).amount;
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
const refreshCost = getTotalRefreshCost(
allDenoms,
denom,
amountLeft,
ws.config.testing.denomselAllowLate,
);
refreshFee.push(refreshCost);
}

View File

@ -245,7 +245,7 @@ async function validateWireInfo(
for (const a of wireInfo.accounts) {
logger.trace("validating exchange acct");
let isValid = false;
if (ws.insecureTrustExchange) {
if (ws.config.testing.insecureTrustExchange) {
isValid = true;
} else {
const { valid: v } = await ws.cryptoApi.isValidWireAccount({
@ -275,7 +275,7 @@ async function validateWireInfo(
wireFee: Amounts.stringify(x.wire_fee),
};
let isValid = false;
if (ws.insecureTrustExchange) {
if (ws.config.testing.insecureTrustExchange) {
isValid = true;
} else {
const { valid: v } = await ws.cryptoApi.isValidWireFee({
@ -308,7 +308,7 @@ async function validateGlobalFees(
for (const gf of fees) {
logger.trace("validating exchange global fees");
let isValid = false;
if (ws.insecureTrustExchange) {
if (ws.config.testing.insecureTrustExchange) {
isValid = true;
} else {
const { valid: v } = await ws.cryptoApi.isValidGlobalFees({
@ -665,7 +665,10 @@ export async function updateExchangeFromUrlHandler(
let ageMask = 0;
for (const x of keysInfo.currentDenominations) {
if (isWithdrawableDenom(x) && x.denomPub.age_mask != 0) {
if (
isWithdrawableDenom(x, ws.config.testing.denomselAllowLate) &&
x.denomPub.age_mask != 0
) {
ageMask = x.denomPub.age_mask;
break;
}

View File

@ -168,6 +168,7 @@ export async function getTotalPaymentCost(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
ws.config.testing.denomselAllowLate,
);
costs.push(Amounts.parseOrThrow(pcs.coinContributions[i]));
costs.push(refreshCost);
@ -1659,6 +1660,7 @@ async function applySuccessfulRefund(
p: PurchaseRecord,
refreshCoinsMap: Record<string, CoinRefreshRequest>,
r: MerchantCoinRefundSuccessStatus,
denomselAllowLate: boolean,
): Promise<void> {
// FIXME: check signature before storing it as valid!
@ -1688,6 +1690,7 @@ async function applySuccessfulRefund(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
denomselAllowLate,
);
refreshCoinsMap[coin.coinPub] = {
@ -1714,6 +1717,7 @@ async function storePendingRefund(
}>,
p: PurchaseRecord,
r: MerchantCoinRefundFailureStatus,
denomselAllowLate: boolean,
): Promise<void> {
const refundKey = getRefundKey(r);
@ -1745,6 +1749,7 @@ async function storePendingRefund(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
denomselAllowLate,
);
p.refunds[refundKey] = {
@ -1767,6 +1772,7 @@ async function storeFailedRefund(
p: PurchaseRecord,
refreshCoinsMap: Record<string, CoinRefreshRequest>,
r: MerchantCoinRefundFailureStatus,
denomselAllowLate: boolean,
): Promise<void> {
const refundKey = getRefundKey(r);
@ -1797,6 +1803,7 @@ async function storeFailedRefund(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
denomselAllowLate,
);
p.refunds[refundKey] = {
@ -1905,11 +1912,28 @@ async function acceptRefunds(
// Invariant: (!existingRefundInfo) || (existingRefundInfo === Pending)
if (refundStatus.type === "success") {
await applySuccessfulRefund(tx, p, refreshCoinsMap, refundStatus);
await applySuccessfulRefund(
tx,
p,
refreshCoinsMap,
refundStatus,
ws.config.testing.denomselAllowLate,
);
} else if (isPermanentFailure) {
await storeFailedRefund(tx, p, refreshCoinsMap, refundStatus);
await storeFailedRefund(
tx,
p,
refreshCoinsMap,
refundStatus,
ws.config.testing.denomselAllowLate,
);
} else {
await storePendingRefund(tx, p, refundStatus);
await storePendingRefund(
tx,
p,
refundStatus,
ws.config.testing.denomselAllowLate,
);
}
}

View File

@ -417,6 +417,7 @@ export async function getTotalPeerPaymentCost(
allDenoms,
DenominationRecord.toDenomInfo(denom),
amountLeft,
ws.config.testing.denomselAllowLate,
);
costs.push(Amounts.parseOrThrow(pcs[i].contribution));
costs.push(refreshCost);

View File

@ -86,7 +86,7 @@ import {
import { makeCoinAvailable } from "./common.js";
import { updateExchangeFromUrl } from "./exchanges.js";
import { selectWithdrawalDenominations } from "../util/coinSelection.js";
import { isWithdrawableDenom } from "../index.js";
import { isWithdrawableDenom, WalletConfig } from "../index.js";
const logger = new Logger("refresh.ts");
@ -105,13 +105,18 @@ export function getTotalRefreshCost(
denoms: DenominationRecord[],
refreshedDenom: DenominationInfo,
amountLeft: AmountJson,
denomselAllowLate: boolean,
): AmountJson {
const withdrawAmount = Amounts.sub(
amountLeft,
refreshedDenom.feeRefresh,
).amount;
const denomMap = Object.fromEntries(denoms.map((x) => [x.denomPubHash, x]));
const withdrawDenoms = selectWithdrawalDenominations(withdrawAmount, denoms);
const withdrawDenoms = selectWithdrawalDenominations(
withdrawAmount,
denoms,
denomselAllowLate,
);
const resultingAmount = Amounts.add(
Amounts.zeroOfCurrency(withdrawAmount.currency),
...withdrawDenoms.selectedDenoms.map(
@ -232,6 +237,7 @@ async function refreshCreateSession(
const newCoinDenoms = selectWithdrawalDenominations(
availableAmount,
availableDenoms,
ws.config.testing.denomselAllowLate,
);
if (newCoinDenoms.selectedDenoms.length === 0) {
@ -897,7 +903,7 @@ export async function createRefreshGroup(
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
.iter(exchangeBaseUrl)
.filter((x) => {
return isWithdrawableDenom(x);
return isWithdrawableDenom(x, ws.config.testing.denomselAllowLate);
});
denomsPerExchange[exchangeBaseUrl] = allDenoms;
return allDenoms;
@ -955,6 +961,7 @@ export async function createRefreshGroup(
denoms,
denom,
Amounts.parseOrThrow(refreshAmount),
ws.config.testing.denomselAllowLate,
);
const output = Amounts.sub(refreshAmount, cost).amount;
estimatedOutputPerCoin.push(output);

View File

@ -210,7 +210,9 @@ export async function getCandidateWithdrawalDenoms(
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
exchangeBaseUrl,
);
return allDenoms.filter(isWithdrawableDenom);
return allDenoms.filter((d) =>
isWithdrawableDenom(d, ws.config.testing.denomselAllowLate),
);
});
}
@ -719,7 +721,7 @@ export async function updateWithdrawalDenoms(
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
if (ws.insecureTrustExchange) {
if (ws.config.testing.insecureTrustExchange) {
valid = true;
} else {
const res = await ws.cryptoApi.isValidDenom({
@ -1003,7 +1005,7 @@ export async function processWithdrawalGroup(
const resp = await processPlanchetExchangeBatchRequest(ws, wgContext, {
batchSize: maxBatchSize,
coinStartIndex: i,
useBatchRequest: ws.batchWithdrawal,
useBatchRequest: ws.config.features.batchWithdrawal,
});
let work: Promise<void>[] = [];
work = [];
@ -1180,6 +1182,7 @@ export async function getExchangeWithdrawalInfo(
const selectedDenoms = selectWithdrawalDenominations(
instructedAmount,
denoms,
ws.config.testing.denomselAllowLate,
);
if (selectedDenoms.selectedDenoms.length === 0) {
@ -1710,9 +1713,14 @@ export async function internalCreateWithdrawalGroup(
amount,
denoms,
args.forcedDenomSel,
ws.config.testing.denomselAllowLate,
);
} else {
initialDenomSel = selectWithdrawalDenominations(amount, denoms);
initialDenomSel = selectWithdrawalDenominations(
amount,
denoms,
ws.config.testing.denomselAllowLate,
);
}
const withdrawalGroup: WithdrawalGroupRecord = {

View File

@ -48,7 +48,11 @@ import {
AllowedExchangeInfo,
DenominationRecord,
} from "../db.js";
import { getExchangeDetails, isWithdrawableDenom } from "../index.js";
import {
getExchangeDetails,
isWithdrawableDenom,
WalletConfig,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { getMerchantPaymentBalanceDetails } from "../operations/balance.js";
import { checkDbInvariant, checkLogicInvariant } from "./invariants.js";
@ -664,6 +668,7 @@ export async function selectCandidates(
export function selectWithdrawalDenominations(
amountAvailable: AmountJson,
denoms: DenominationRecord[],
denomselAllowLate: boolean = false,
): DenomSelectionState {
let remaining = Amounts.copy(amountAvailable);
@ -675,7 +680,7 @@ export function selectWithdrawalDenominations(
let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
denoms = denoms.filter(isWithdrawableDenom);
denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
denoms.sort((d1, d2) =>
Amounts.cmp(
DenominationRecord.getValue(d2),
@ -737,6 +742,7 @@ export function selectForcedWithdrawalDenominations(
amountAvailable: AmountJson,
denoms: DenominationRecord[],
forcedDenomSel: ForcedDenomSel,
denomselAllowLate: boolean,
): DenomSelectionState {
const selectedDenoms: {
count: number;
@ -746,7 +752,7 @@ export function selectForcedWithdrawalDenominations(
let totalCoinValue = Amounts.zeroOfCurrency(amountAvailable.currency);
let totalWithdrawCost = Amounts.zeroOfCurrency(amountAvailable.currency);
denoms = denoms.filter(isWithdrawableDenom);
denoms = denoms.filter((d) => isWithdrawableDenom(d, denomselAllowLate));
denoms.sort((d1, d2) =>
Amounts.cmp(
DenominationRecord.getValue(d2),

View File

@ -1,32 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021 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/>
*/
/**
* Debug flags for wallet-core.
*
* @author Florian Dold
*/
export interface WalletCoreDebugFlags {
/**
* Allow withdrawal of denominations even though they are about to expire.
*/
denomselAllowLate: boolean;
}
export const walletCoreDebugFlags: WalletCoreDebugFlags = {
denomselAllowLate: false,
};

View File

@ -29,7 +29,7 @@ import {
WireFee,
} from "@gnu-taler/taler-util";
import { DenominationRecord } from "../db.js";
import { walletCoreDebugFlags } from "./debugFlags.js";
import { WalletConfig } from "../index.js";
/**
* Given a list of denominations with the same value and same period of time:
@ -452,13 +452,16 @@ export function createTimeline<Type extends object>(
* Check if a denom is withdrawable based on the expiration time,
* revocation and offered state.
*/
export function isWithdrawableDenom(d: DenominationRecord): boolean {
export function isWithdrawableDenom(
d: DenominationRecord,
denomselAllowLate?: boolean,
): boolean {
const now = AbsoluteTime.now();
const start = AbsoluteTime.fromTimestamp(d.stampStart);
const withdrawExpire = AbsoluteTime.fromTimestamp(d.stampExpireWithdraw);
const started = AbsoluteTime.cmp(now, start) >= 0;
let lastPossibleWithdraw: AbsoluteTime;
if (walletCoreDebugFlags.denomselAllowLate) {
if (denomselAllowLate) {
lastPossibleWithdraw = start;
} else {
lastPossibleWithdraw = AbsoluteTime.subtractDuraction(

View File

@ -29,8 +29,6 @@ import {
AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest,
AcceptManualWithdrawalResult,
ConfirmPeerPullDebitRequest,
ConfirmPeerPushCreditRequest,
AcceptTipRequest,
AcceptTipResponse,
AcceptWithdrawalResponse,
@ -42,17 +40,18 @@ import {
ApplyRefundResponse,
BackupRecovery,
BalancesResponse,
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
PreparePeerPushCredit,
PreparePeerPushCreditResponse,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest,
CheckPeerPushDebitResponse,
CoinDumpJson,
ConfirmPayRequest,
ConfirmPayResult,
ConfirmPeerPullDebitRequest,
ConfirmPeerPushCreditRequest,
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DeleteTransactionRequest,
DepositGroupFees,
ExchangeDetailedResponse,
ExchangesListResponse,
ForceRefreshRequest,
@ -63,12 +62,12 @@ import {
GetExchangeTosResult,
GetWithdrawalDetailsForAmountRequest,
GetWithdrawalDetailsForUriRequest,
InitRequest,
InitResponse,
InitiatePeerPullCreditRequest,
InitiatePeerPullCreditResponse,
InitiatePeerPushPaymentRequest,
InitiatePeerPushPaymentResponse,
InitRequest,
InitResponse,
IntegrationTestArgs,
KnownBankAccounts,
ListKnownBankAccountsRequest,
@ -78,10 +77,10 @@ import {
PreparePayRequest,
PreparePayResult,
PreparePayTemplateRequest,
CheckPeerPullCreditRequest,
CheckPeerPullCreditResponse,
CheckPeerPushDebitRequest,
CheckPeerPushDebitResponse,
PreparePeerPullDebitRequest,
PreparePeerPullDebitResponse,
PreparePeerPushCredit,
PreparePeerPushCreditResponse,
PrepareRefundRequest,
PrepareRefundResult,
PrepareTipRequest,
@ -89,7 +88,6 @@ import {
RecoveryLoadRequest,
RetryTransactionRequest,
SetCoinSuspendedRequest,
SetDevModeRequest,
SetWalletDeviceIdRequest,
TestPayArgs,
TestPayResult,
@ -97,21 +95,21 @@ import {
TransactionByIdRequest,
TransactionsRequest,
TransactionsResponse,
TxIdResponse,
UserAttentionByIdRequest,
UserAttentionsCountResponse,
UserAttentionsRequest,
UserAttentionsResponse,
ValidateIbanRequest,
ValidateIbanResponse,
WalletBackupContentV1,
WalletCoreVersion,
WalletCurrencyInfo,
WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
ValidateIbanRequest,
ValidateIbanResponse,
TxIdResponse,
} from "@gnu-taler/taler-util";
import { WalletContractData } from "./db.js";
import { AuditorTrustRecord, WalletContractData } from "./db.js";
import {
AddBackupProviderRequest,
AddBackupProviderResponse,
@ -195,13 +193,11 @@ export enum WalletApiOperation {
ConfirmPeerPullDebit = "confirmPeerPullDebit",
ClearDb = "clearDb",
Recycle = "recycle",
SetDevMode = "setDevMode",
ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban",
}
// group: Initialization
type EmptyObject = Record<string, never>;
/**
* Initialize wallet-core.
@ -739,12 +735,6 @@ export type ApplyDevExperimentOp = {
response: EmptyObject;
};
export type SetDevModeOp = {
op: WalletApiOperation.SetDevMode;
request: SetDevModeRequest;
response: EmptyObject;
};
/**
* Run a simple integration test on a test deployment
* of the exchange and merchant.
@ -953,7 +943,6 @@ export type WalletOperations = {
[WalletApiOperation.ClearDb]: ClearDbOp;
[WalletApiOperation.Recycle]: RecycleOp;
[WalletApiOperation.ApplyDevExperiment]: ApplyDevExperimentOp;
[WalletApiOperation.SetDevMode]: SetDevModeOp;
[WalletApiOperation.ValidateIban]: ValidateIbanOp;
};
@ -973,3 +962,50 @@ export interface WalletCoreApiClient {
payload: WalletCoreRequestType<Op>,
): Promise<WalletCoreResponseType<Op>>;
}
type Primitives = string | number | boolean;
export type RecursivePartial<T extends object> = {
[P in keyof T]?: T[P] extends Array<infer U extends object>
? Array<RecursivePartial<U>>
: T[P] extends Array<infer J extends Primitives>
? Array<J>
: T[P] extends object
? RecursivePartial<T[P]>
: T[P];
} & object;
export type WalletConfigParameter = RecursivePartial<WalletConfig>;
export interface WalletConfig {
/**
* Initialization values useful for a complete startup.
*
* These are values may be overridden by different wallets
*/
builtin: {
exchanges: string[];
auditors: AuditorTrustRecord[];
};
/**
* Unsafe options which it should only be used to create
* testing environment.
*/
testing: {
/**
* Allow withdrawal of denominations even though they are about to expire.
*/
denomselAllowLate: boolean;
devModeActive: boolean;
insecureTrustExchange: boolean;
};
/**
* Configurations values that may be safe to show to the user
*/
features: {
batchWithdrawal: boolean;
allowHttp: boolean;
};
}

View File

@ -25,12 +25,38 @@
import {
AbsoluteTime,
Amounts,
CoinDumpJson,
CoinRefreshRequest,
CoinStatus,
CoreApiResponse,
DenomOperationMap,
DenominationInfo,
Duration,
ExchangeDetailedResponse,
ExchangeListItem,
ExchangeTosStatusDetails,
ExchangesListResponse,
FeeDescription,
GetExchangeTosResult,
InitResponse,
KnownBankAccounts,
KnownBankAccountsInfo,
Logger,
ManualWithdrawalDetails,
MerchantUsingTemplateDetails,
NotificationType,
RefreshReason,
TalerError,
TalerErrorCode,
URL,
ValidateIbanResponse,
WalletCoreVersion,
WalletNotification,
codecForAbortTransaction,
codecForAcceptBankIntegratedWithdrawalRequest,
codecForAcceptExchangeTosRequest,
codecForAcceptManualWithdrawalRequet,
codecForAcceptPeerPullPaymentRequest,
codecForConfirmPeerPushPaymentRequest,
codecForAcceptTipRequest,
codecForAddExchangeRequest,
codecForAddKnownBankAccounts,
@ -39,8 +65,9 @@ import {
codecForApplyRefundFromPurchaseIdRequest,
codecForApplyRefundRequest,
codecForCheckPeerPullPaymentRequest,
codecForPreparePeerPushCreditRequest,
codecForCheckPeerPushDebitRequest,
codecForConfirmPayRequest,
codecForConfirmPeerPushPaymentRequest,
codecForCreateDepositGroupRequest,
codecForDeleteTransactionRequest,
codecForForceRefreshRequest,
@ -54,86 +81,58 @@ import {
codecForInitiatePeerPullPaymentRequest,
codecForInitiatePeerPushPaymentRequest,
codecForIntegrationTestArgs,
codecForIntegrationTestV2Args,
codecForListKnownBankAccounts,
codecForMerchantPostOrderResponse,
codecForPrepareDepositRequest,
codecForPreparePayRequest,
codecForPreparePayTemplateRequest,
codecForPreparePeerPullPaymentRequest,
codecForCheckPeerPushDebitRequest,
codecForPreparePeerPushCreditRequest,
codecForPrepareRefundRequest,
codecForPrepareTipRequest,
codecForResumeTransaction,
codecForRetryTransactionRequest,
codecForSetCoinSuspendedRequest,
codecForSetDevModeRequest,
codecForSetWalletDeviceIdRequest,
codecForSuspendTransaction,
codecForTestPayArgs,
codecForTransactionByIdRequest,
codecForTransactionsRequest,
codecForUserAttentionByIdRequest,
codecForUserAttentionsRequest,
codecForValidateIbanRequest,
codecForWithdrawFakebankRequest,
codecForWithdrawTestBalance,
CoinDumpJson,
CoinRefreshRequest,
CoinStatus,
constructPayUri,
CoreApiResponse,
DenominationInfo,
DenomOperationMap,
Duration,
durationFromSpec,
durationMin,
ExchangeDetailedResponse,
ExchangeListItem,
ExchangesListResponse,
ExchangeTosStatusDetails,
FeeDescription,
GetExchangeTosResult,
InitResponse,
getErrorDetailFromException,
j2s,
KnownBankAccounts,
KnownBankAccountsInfo,
Logger,
ManualWithdrawalDetails,
MerchantUsingTemplateDetails,
NotificationType,
parsePayTemplateUri,
parsePaytoUri,
RefreshReason,
TalerErrorCode,
URL,
WalletCoreVersion,
WalletNotification,
codecForSuspendTransaction,
codecForResumeTransaction,
validateIban,
codecForValidateIbanRequest,
ValidateIbanResponse,
codecForIntegrationTestV2Args,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import {
CryptoDispatcher,
CryptoWorkerFactory,
} from "./crypto/workers/crypto-dispatcher.js";
import {
AuditorTrustRecord,
clearDatabase,
CoinSourceType,
ConfigRecordKey,
DenominationRecord,
ExchangeDetailsRecord,
WalletStoresV1,
clearDatabase,
exportDb,
importDb,
WalletStoresV1,
} from "./db.js";
import {
applyDevExperiment,
maybeInitDevMode,
setDevMode,
} from "./dev-experiments.js";
import { getErrorDetailFromException, TalerError } from "@gnu-taler/taler-util";
import { DevExperimentHttpLib, applyDevExperiment } from "./dev-experiments.js";
import {
ActiveLongpollInfo,
ExchangeOperations,
@ -198,18 +197,18 @@ import {
processPurchase,
} from "./operations/pay-merchant.js";
import {
confirmPeerPullDebit,
confirmPeerPushCredit,
preparePeerPullDebit,
preparePeerPushCredit,
initiatePeerPullPayment,
initiatePeerPushPayment,
checkPeerPullPaymentInitiation,
checkPeerPushDebit,
confirmPeerPullDebit,
confirmPeerPushCredit,
initiatePeerPullPayment,
initiatePeerPushPayment,
preparePeerPullDebit,
preparePeerPushCredit,
processPeerPullCredit,
processPeerPushInitiation,
processPeerPullDebit,
processPeerPushCredit,
processPeerPushInitiation,
} from "./operations/pay-peer.js";
import { getPendingOperations } from "./operations/pending.js";
import {
@ -252,10 +251,6 @@ import {
selectBestForOverlappingDenominations,
selectMinimumFee,
} from "./util/denominations.js";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "@gnu-taler/taler-util/http";
import { checkDbInvariant } from "./util/invariants.js";
import {
AsyncCondition,
@ -275,22 +270,14 @@ import {
WALLET_MERCHANT_PROTOCOL_VERSION,
} from "./versions.js";
import {
RecursivePartial,
WalletApiOperation,
WalletConfig,
WalletConfigParameter,
WalletCoreApiClient,
WalletCoreResponseType,
} from "./wallet-api-types.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
currency: "KUDOS",
auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
auditorBaseUrl: "https://auditor.demo.taler.net/",
uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"],
},
];
const builtinExchanges: string[] = ["https://exchange.demo.taler.net/"];
const logger = new Logger("wallet.ts");
/**
@ -495,10 +482,10 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
return;
}
logger.info("importing default exchanges and auditors");
for (const c of builtinAuditors) {
for (const c of ws.config.builtin.auditors) {
await tx.auditorTrust.put(c);
}
for (const baseUrl of builtinExchanges) {
for (const baseUrl of ws.config.builtin.exchanges) {
const now = AbsoluteTime.now();
provideExchangeRecordInTx(ws, tx, baseUrl, now);
}
@ -1018,7 +1005,6 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
logger.trace("filling defaults");
await fillDefaults(ws);
}
await maybeInitDevMode(ws);
const resp: InitResponse = {
versionInfo: getVersion(ws),
};
@ -1482,15 +1468,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await applyDevExperiment(ws, req.devExperimentUri);
return {};
}
case WalletApiOperation.SetDevMode: {
const req = codecForSetDevModeRequest().decode(payload);
await setDevMode(ws, req.devModeEnabled);
return {};
}
case WalletApiOperation.GetVersion: {
return getVersion(ws);
}
//default:
// default:
// assertUnreachable(operation);
}
throw TalerError.fromDetail(
@ -1509,7 +1490,7 @@ export function getVersion(ws: InternalWalletState): WalletCoreVersion {
exchange: WALLET_EXCHANGE_PROTOCOL_VERSION,
merchant: WALLET_MERCHANT_PROTOCOL_VERSION,
bank: WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
devMode: ws.devModeActive,
devMode: false,
};
return version;
}
@ -1575,8 +1556,15 @@ export class Wallet {
http: HttpRequestLibrary,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
config?: WalletConfigParameter,
) {
this.ws = new InternalWalletStateImpl(db, http, timer, cryptoWorkerFactory);
this.ws = new InternalWalletStateImpl(
db,
http,
timer,
cryptoWorkerFactory,
config ?? {},
);
}
get client(): WalletCoreApiClient {
@ -1586,29 +1574,22 @@ export class Wallet {
return this._client;
}
/**
* Trust the exchange, do not validate signatures.
* Only used to benchmark the exchange.
*/
setInsecureTrustExchange(): void {
this.ws.insecureTrustExchange = true;
}
setBatchWithdrawal(enable: boolean): void {
this.ws.batchWithdrawal = enable;
}
static async create(
db: DbAccess<typeof WalletStoresV1>,
http: HttpRequestLibrary,
timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
config?: WalletConfigParameter,
): Promise<Wallet> {
const w = new Wallet(db, http, timer, cryptoWorkerFactory);
const w = new Wallet(db, http, timer, cryptoWorkerFactory, config);
w._client = await getClientFromWalletState(w.ws);
return w;
}
static getDefaultConfig(): Readonly<WalletConfig> {
return InternalWalletStateImpl.defaultConfig;
}
addNotificationListener(f: (n: WalletNotification) => void): void {
return this.ws.addNotificationListener(f);
}
@ -1650,10 +1631,6 @@ class InternalWalletStateImpl implements InternalWalletState {
merchantInfoCache: Record<string, MerchantInfo> = {};
insecureTrustExchange = false;
batchWithdrawal = false;
readonly timerGroup: TimerGroup;
latch = new AsyncCondition();
stopped = false;
@ -1662,8 +1639,6 @@ class InternalWalletStateImpl implements InternalWalletState {
initCalled = false;
devModeActive = false;
exchangeOps: ExchangeOperations = {
getExchangeDetails,
getExchangeTrust,
@ -1696,6 +1671,31 @@ class InternalWalletStateImpl implements InternalWalletState {
*/
private resourceLocks: Set<string> = new Set();
config: Readonly<WalletConfig>;
public static defaultConfig: Readonly<WalletConfig> = {
builtin: {
exchanges: ["https://exchange.demo.taler.net/"],
auditors: [
{
currency: "KUDOS",
auditorPub: "BW9DC48PHQY4NH011SHHX36DZZ3Q22Y6X7FZ1VD1CMZ2PTFZ6PN0",
auditorBaseUrl: "https://auditor.demo.taler.net/",
uids: ["5P25XF8TVQP9AW6VYGY2KV47WT5Y3ZXFSJAA570GJPX5SVJXKBVG"],
},
],
},
features: {
batchWithdrawal: false,
allowHttp: false,
},
testing: {
devModeActive: false,
insecureTrustExchange: false,
denomselAllowLate: false,
},
};
constructor(
// FIXME: Make this a getter and make
// the actual value nullable.
@ -1705,10 +1705,15 @@ class InternalWalletStateImpl implements InternalWalletState {
public http: HttpRequestLibrary,
public timer: TimerAPI,
cryptoWorkerFactory: CryptoWorkerFactory,
config: WalletConfigParameter,
) {
this.cryptoDispatcher = new CryptoDispatcher(cryptoWorkerFactory);
this.cryptoApi = this.cryptoDispatcher.cryptoApi;
this.timerGroup = new TimerGroup(timer);
this.config = deepMerge(InternalWalletStateImpl.defaultConfig, config);
if (this.config.testing.devModeActive) {
this.http = new DevExperimentHttpLib(this.http);
}
}
async getDenomInfo(
@ -1808,3 +1813,29 @@ class InternalWalletStateImpl implements InternalWalletState {
}
}
}
/**
* Take the full object as template, create a new result with all the values.
* Use the override object to change the values in the result
* return result
* @param full
* @param override
* @returns
*/
function deepMerge<T extends object>(
full: T,
override: RecursivePartial<T>,
): T {
const keys = Object.keys(full);
const result = { ...full };
for (const k of keys) {
// @ts-ignore
const newVal = override[k];
if (newVal === undefined) continue;
// @ts-ignore
result[k] =
// @ts-ignore
typeof newVal === "object" ? deepMerge(full[k], newVal) : newVal;
}
return result;
}

View File

@ -26,7 +26,7 @@
*/
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { JustInDevMode } from "./components/JustInDevMode.js";
import { EnabledBySettings } from "./components/EnabledBySettings.js";
import {
NavigationHeader,
NavigationHeaderHolder,
@ -183,11 +183,11 @@ export function PopupNavBar({ path }: { path?: PopupNavBarOptions }): VNode {
<a href={Pages.balance} class={path === "balance" ? "active" : ""}>
<i18n.Translate>Balance</i18n.Translate>
</a>
<JustInDevMode>
<EnabledBySettings name="backup">
<a href={Pages.backup} class={path === "backup" ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate>
</a>
</JustInDevMode>
</EnabledBySettings>
<div style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}>
{attentionCount > 0 ? (
<a href={Pages.notifications}>
@ -238,11 +238,11 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<a href={Pages.balance} class={path === "balance" ? "active" : ""}>
<i18n.Translate>Balance</i18n.Translate>
</a>
<JustInDevMode>
<EnabledBySettings name="backup">
<a href={Pages.backup} class={path === "backup" ? "active" : ""}>
<i18n.Translate>Backup</i18n.Translate>
</a>
</JustInDevMode>
</EnabledBySettings>
{attentionCount > 0 ? (
<a href={Pages.notifications}>
@ -252,11 +252,11 @@ export function WalletNavBar({ path }: { path?: WalletNavBarOptions }): VNode {
<Fragment />
)}
<JustInDevMode>
<EnabledBySettings name="advanceMode">
<a href={Pages.dev} class={path === "dev" ? "active" : ""}>
<i18n.Translate>Dev</i18n.Translate>
<i18n.Translate>Dev tools</i18n.Translate>
</a>
</JustInDevMode>
</EnabledBySettings>
<div
style={{ display: "flex", paddingTop: 4, justifyContent: "right" }}

View File

@ -27,7 +27,6 @@ import { platform, setupPlatform } from "./platform/background.js";
import devAPI from "./platform/dev.js";
import { wxMain } from "./wxBackend.js";
console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
async function start() {

View File

@ -35,10 +35,8 @@ const isFirefox =
// FIXME: create different entry point for any platform instead of
// switching in runtime
if (isFirefox) {
console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}

View File

@ -14,14 +14,25 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useDevContext } from "../context/devContext.js";
import { useSettings } from "../hooks/useSettings.js";
import { Settings } from "../platform/api.js";
export function JustInDevMode({
export function EnabledBySettings<K extends keyof Settings>({
children,
value,
name,
}: {
name: K;
value?: Settings[K];
children: ComponentChildren;
}): VNode {
const { devMode } = useDevContext();
if (!devMode) return <Fragment />;
const [settings] = useSettings();
if (value === undefined) {
if (!settings[name]) return <Fragment />;
return <Fragment>{children}</Fragment>;
}
if (settings[name] !== value) {
return <Fragment />;
}
return <Fragment>{children}</Fragment>;
}

View File

@ -17,8 +17,8 @@ import { TalerErrorDetail, TranslatedString } from "@gnu-taler/taler-util";
import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import arrowDown from "../svg/chevron-down.svg";
import { useDevContext } from "../context/devContext.js";
import { ErrorBox } from "./styled/index.js";
import { EnabledBySettings } from "./EnabledBySettings.js";
export function ErrorTalerOperation({
title,
@ -27,7 +27,6 @@ export function ErrorTalerOperation({
title?: TranslatedString;
error?: TalerErrorDetail;
}): VNode | null {
const { devMode } = useDevContext();
const [showErrorDetail, setShowErrorDetail] = useState(false);
if (!title || !error) return null;
@ -62,11 +61,11 @@ export function ErrorTalerOperation({
<b>{error.hint}</b> {!errorHint ? "" : `: ${errorHint}`}{" "}
</div>
</div>
{devMode && (
<EnabledBySettings name="showJsonOnError">
<div style={{ textAlign: "left", overflowX: "auto" }}>
<pre>{JSON.stringify(error, undefined, 2)}</pre>
</div>
)}
</EnabledBySettings>
</Fragment>
)}
</ErrorBox>

View File

@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { ExchangeTosStatus, GetExchangeTosResult } from "@gnu-taler/taler-util";
import {
ExchangeTosStatus,
GetExchangeTosResult,
Logger,
} from "@gnu-taler/taler-util";
export function buildTermsOfServiceState(
tos: GetExchangeTosResult,
@ -27,6 +31,8 @@ export function buildTermsOfServiceState(
return { content, status: tos.tosStatus, version: tos.currentEtag };
}
const logger = new Logger("termsofservice");
function parseTermsOfServiceContent(
type: string,
text: string,
@ -36,28 +42,28 @@ function parseTermsOfServiceContent(
const document = new DOMParser().parseFromString(text, "text/xml");
return { type: "xml", document };
} catch (e) {
console.log(e);
logger.error("error parsing xml", e);
}
} else if (type === "text/html") {
try {
const href = new URL(text);
return { type: "html", href };
} catch (e) {
console.log(e);
logger.error("error parsing url", e);
}
} else if (type === "text/json") {
try {
const data = JSON.parse(text);
return { type: "json", data };
} catch (e) {
console.log(e);
logger.error("error parsing json", e);
}
} else if (type === "text/pdf") {
try {
const location = new URL(text);
return { type: "pdf", location };
} catch (e) {
console.log(e);
logger.error("error parsing url", e);
}
}
const content = text;

View File

@ -1,70 +0,0 @@
/*
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/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { useWalletDevMode } from "../hooks/useWalletDevMode.js";
interface Type {
devMode: boolean;
toggle: () => Promise<void>;
}
const Context = createContext<Type>({
devMode: false,
toggle: async () => {
null;
},
});
export const useDevContext = (): Type => useContext(Context);
export const DevContextProviderForTesting = ({
value,
children,
}: {
value?: boolean;
children: any;
}): VNode => {
return h(Context.Provider, {
value: {
devMode: !!value,
toggle: async () => {
null;
},
},
children,
});
};
export const DevContextProvider = ({ children }: { children: any }): VNode => {
const devModeToggle = useWalletDevMode();
const value: Type = {
devMode: !!devModeToggle.value,
toggle: devModeToggle.toggle,
};
//support for function as children, useful for getting the value right away
children =
children.length === 1 && typeof children === "function"
? children(value)
: children;
return h(Context.Provider, { value, children });
};

View File

@ -167,8 +167,6 @@ export function useComponentState({
if (fu) {
if (typeof window !== "undefined") {
document.location.href = fu;
} else {
console.log(`should d to ${fu}`);
}
}
onSuccess(res.transactionId);

View File

@ -30,7 +30,6 @@ export function ReadyView({
}: State.FillTemplate): VNode {
const { i18n } = useTranslationContext();
console.log("is summary", !!summary);
return (
<Fragment>
<section style={{ textAlign: "left" }}>

View File

@ -51,7 +51,6 @@ export function useClipboardPermissions(): ToggleHandler {
// .call("toggleHeaderListener", false)
// .then((r) => setEnabled(r.newValue));
// } catch (e) {
// console.log(e);
// }
}
return;

View File

@ -36,7 +36,6 @@ export function useSettings(): [
function updateField<T extends keyof Settings>(k: T, v: Settings[T]) {
const newValue = { ...parsed, [k]: v };
const json = JSON.stringify(newValue);
console.log(json);
update(json);
}
return [parsed, updateField];

View File

@ -1,52 +0,0 @@
/*
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 { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useEffect, useState } from "preact/hooks";
import { useBackendContext } from "../context/backend.js";
type Result = {
value: boolean | undefined;
toggle: () => Promise<void>;
};
export function useWalletDevMode(): Result {
const [enabled, setEnabled] = useState<undefined | boolean>(undefined);
const api = useBackendContext();
// const { pushAlertOnError } = useAlertContext();
async function handleOpen(): Promise<void> {
const nextValue = !enabled;
await api.wallet.call(WalletApiOperation.SetDevMode, {
devModeEnabled: nextValue,
});
setEnabled(nextValue);
return;
}
useEffect(() => {
async function getValue(): Promise<void> {
const res = await api.wallet.call(WalletApiOperation.GetVersion, {});
setEnabled(res.devMode);
}
getValue();
}, []);
return {
value: enabled,
toggle: handleOpen,
};
}

View File

@ -15,7 +15,11 @@
*/
import { CoreApiResponse, NotificationType } from "@gnu-taler/taler-util";
import { WalletOperations } from "@gnu-taler/taler-wallet-core";
import {
WalletConfig,
WalletConfigParameter,
WalletOperations,
} from "@gnu-taler/taler-wallet-core";
import { BackgroundOperations } from "../wxApi.js";
import {
ExtensionOperations,
@ -87,12 +91,35 @@ export interface WalletWebExVersion {
version: string;
}
export interface Settings {
type F = WalletConfig["features"];
type kf = keyof F;
type WebexWalletConfig = {
[P in keyof F as `wallet${Capitalize<P>}`]: F[P];
};
export interface Settings extends WebexWalletConfig {
injectTalerSupport: boolean;
advanceMode: boolean;
backup: boolean;
langSelector: boolean;
showJsonOnError: boolean;
extendedAccountTypes: boolean;
//WORKAROUND
//Able to delete tx in dev mode
//FIXME: remove this when DD37 is implemented
deleteActiveTransactions: boolean;
}
export const defaultSettings: Settings = {
injectTalerSupport: false,
advanceMode: false,
backup: false,
langSelector: false,
deleteActiveTransactions: false,
showJsonOnError: false,
extendedAccountTypes: false,
walletAllowHttp: false,
walletBatchWithdrawal: false,
};
/**

View File

@ -45,7 +45,7 @@ function isFirefox(): boolean {
}
function addPermissionsListener(callback: (p: Permissions) => void): void {
console.log("addPermissionListener is not supported for Firefox");
throw Error("addPermissionListener is not supported for Firefox");
}
function getPermissionsApi(): CrossBrowserPermissionsApi {

View File

@ -20,6 +20,10 @@
* @author sebasjm
*/
import {
TranslationProvider,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { createHashHistory } from "history";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { route, Route, Router } from "preact-router";
@ -27,29 +31,22 @@ import { useEffect, useState } from "preact/hooks";
import PendingTransactions from "../components/PendingTransactions.js";
import { PopupBox } from "../components/styled/index.js";
import { AlertProvider } from "../context/alert.js";
import { DevContextProvider } from "../context/devContext.js";
import { IoCProviderForRuntime } from "../context/iocContext.js";
import {
TranslationProvider,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { useTalerActionURL } from "../hooks/useTalerActionURL.js";
import { strings } from "../i18n/strings.js";
import { Pages, PopupNavBar, PopupNavBarOptions } from "../NavigationBar.js";
import { platform } from "../platform/foreground.js";
import { BackupPage } from "../wallet/BackupPage.js";
import { ProviderDetailPage } from "../wallet/ProviderDetailPage.js";
import { BalancePage } from "./BalancePage.js";
import { TalerActionFound } from "./TalerActionFound.js";
import { strings } from "../i18n/strings.js";
export function Application(): VNode {
return (
<TranslationProvider source={strings}>
<DevContextProvider>
<IoCProviderForRuntime>
<ApplicationView />
</IoCProviderForRuntime>
</DevContextProvider>
<IoCProviderForRuntime>
<ApplicationView />
</IoCProviderForRuntime>
</TranslationProvider>
);
}

View File

@ -27,7 +27,6 @@ import { setupPlatform } from "./platform/foreground.js";
import devAPI from "./platform/dev.js";
import { Application } from "./popup/Application.js";
console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
function main(): void {

View File

@ -32,10 +32,8 @@ import { Application } from "./popup/Application.js";
//switching in runtime
const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
if (isFirefox) {
console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}

View File

@ -41,7 +41,6 @@ import { strings } from "./i18n/strings.js";
// When doing tests we want the requestAnimationFrame to be as fast as possible.
// without this option the RAF will timeout after 100ms making the tests slower
options.requestAnimationFrame = (fn: () => void) => {
// console.log("RAF called")
return fn();
};

View File

@ -20,15 +20,21 @@
* @author sebasjm
*/
import { TranslatedString } from "@gnu-taler/taler-util";
import {
TalerUriAction,
TranslatedString,
parseTalerUri,
} from "@gnu-taler/taler-util";
TranslationProvider,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { createHashHistory } from "history";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { route, Route, Router } from "preact-router";
import { ComponentChildren, Fragment, VNode, h } from "preact";
import { Route, Router, route } from "preact-router";
import { useEffect } from "preact/hooks";
import {
Pages,
WalletNavBar,
WalletNavBarOptions,
getPathnameForTalerURI,
} from "../NavigationBar.js";
import { AlertView, CurrentAlerts } from "../components/CurrentAlerts.js";
import { LogoHeader } from "../components/LogoHeader.js";
import PendingTransactions from "../components/PendingTransactions.js";
@ -39,12 +45,7 @@ import {
WalletBox,
} from "../components/styled/index.js";
import { AlertProvider } from "../context/alert.js";
import { DevContextProvider } from "../context/devContext.js";
import { IoCProviderForRuntime } from "../context/iocContext.js";
import {
TranslationProvider,
useTranslationContext,
} from "@gnu-taler/web-util/lib/index.browser";
import { DepositPage as DepositPageCTA } from "../cta/Deposit/index.js";
import { InvoiceCreatePage } from "../cta/InvoiceCreate/index.js";
import { InvoicePayPage } from "../cta/InvoicePay/index.js";
@ -59,12 +60,7 @@ import {
WithdrawPageFromParams,
WithdrawPageFromURI,
} from "../cta/Withdraw/index.js";
import {
Pages,
WalletNavBar,
WalletNavBarOptions,
getPathnameForTalerURI,
} from "../NavigationBar.js";
import { strings } from "../i18n/strings.js";
import { platform } from "../platform/foreground.js";
import CloseIcon from "../svg/close_24px.svg";
import { AddBackupProviderPage } from "./AddBackupProvider/index.js";
@ -80,7 +76,6 @@ import { QrReaderPage } from "./QrReader.js";
import { SettingsPage } from "./Settings.js";
import { TransactionPage } from "./Transaction.js";
import { WelcomePage } from "./Welcome.js";
import { strings } from "../i18n/strings.js";
export function Application(): VNode {
const { i18n } = useTranslationContext();
@ -91,433 +86,421 @@ export function Application(): VNode {
}
return (
<TranslationProvider source={strings}>
<DevContextProvider>
<IoCProviderForRuntime>
<Router history={hash_history}>
<Route
path={Pages.welcome}
component={() => (
<WalletTemplate>
<WelcomePage />
</WalletTemplate>
)}
/>
<IoCProviderForRuntime>
<Router history={hash_history}>
<Route
path={Pages.welcome}
component={() => (
<WalletTemplate>
<WelcomePage />
</WalletTemplate>
)}
/>
<Route
path={Pages.qr}
component={() => (
<WalletTemplate goToTransaction={redirectToTxInfo}>
<QrReaderPage
onDetected={(talerActionUrl: string) => {
platform.openWalletURIFromPopup(talerActionUrl);
}}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.qr}
component={() => (
<WalletTemplate goToTransaction={redirectToTxInfo}>
<QrReaderPage
onDetected={(talerActionUrl: string) => {
platform.openWalletURIFromPopup(talerActionUrl);
}}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.settings}
component={() => (
<WalletTemplate goToTransaction={redirectToTxInfo}>
<SettingsPage />
</WalletTemplate>
)}
/>
<Route
path={Pages.notifications}
component={() => (
<WalletTemplate>
<NotificationsPage />
</WalletTemplate>
)}
/>
{/**
* SETTINGS
*/}
<Route
path={Pages.settingsExchangeAdd.pattern}
component={() => (
<WalletTemplate>
<ExchangeAddPage onBack={() => redirectTo(Pages.balance)} />
</WalletTemplate>
)}
/>
<Route
path={Pages.settings}
component={() => (
<WalletTemplate goToTransaction={redirectToTxInfo}>
<SettingsPage />
</WalletTemplate>
)}
/>
<Route
path={Pages.notifications}
component={() => (
<WalletTemplate>
<NotificationsPage />
</WalletTemplate>
)}
/>
{/**
* SETTINGS
*/}
<Route
path={Pages.settingsExchangeAdd.pattern}
component={() => (
<WalletTemplate>
<ExchangeAddPage onBack={() => redirectTo(Pages.balance)} />
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceHistory.pattern}
component={({ currency }: { currency?: string }) => (
<WalletTemplate
path="balance"
goToTransaction={redirectToTxInfo}
>
<HistoryPage
currency={currency}
goToWalletDeposit={(currency: string) =>
redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
}
goToWalletManualWithdraw={(currency?: string) =>
redirectTo(
Pages.receiveCash({
amount: !currency ? undefined : `${currency}:0`,
}),
)
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.sendCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance">
<DestinationSelectionPage
type="send"
amount={amount}
goToWalletBankDeposit={(amount: string) =>
redirectTo(Pages.balanceDeposit({ amount }))
}
goToWalletWalletSend={(amount: string) =>
redirectTo(Pages.ctaTransferCreate({ amount }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.receiveCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance">
<DestinationSelectionPage
type="get"
amount={amount}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.ctaWithdrawManual({ amount }))
}
goToWalletWalletInvoice={(amount?: string) =>
redirectTo(Pages.ctaInvoiceCreate({ amount }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceHistory.pattern}
component={({ currency }: { currency?: string }) => (
<WalletTemplate path="balance" goToTransaction={redirectToTxInfo}>
<HistoryPage
currency={currency}
goToWalletDeposit={(currency: string) =>
redirectTo(Pages.sendCash({ amount: `${currency}:0` }))
}
goToWalletManualWithdraw={(currency?: string) =>
redirectTo(
Pages.receiveCash({
amount: !currency ? undefined : `${currency}:0`,
}),
)
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.sendCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance">
<DestinationSelectionPage
type="send"
amount={amount}
goToWalletBankDeposit={(amount: string) =>
redirectTo(Pages.balanceDeposit({ amount }))
}
goToWalletWalletSend={(amount: string) =>
redirectTo(Pages.ctaTransferCreate({ amount }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.receiveCash.pattern}
component={({ amount }: { amount?: string }) => (
<WalletTemplate path="balance">
<DestinationSelectionPage
type="get"
amount={amount}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.ctaWithdrawManual({ amount }))
}
goToWalletWalletInvoice={(amount?: string) =>
redirectTo(Pages.ctaInvoiceCreate({ amount }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceTransaction.pattern}
component={({ tid }: { tid: string }) => (
<WalletTemplate path="balance">
<TransactionPage
tid={tid}
goToWalletHistory={(currency?: string) =>
redirectTo(Pages.balanceHistory({ currency }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceTransaction.pattern}
component={({ tid }: { tid: string }) => (
<WalletTemplate path="balance">
<TransactionPage
tid={tid}
goToWalletHistory={(currency?: string) =>
redirectTo(Pages.balanceHistory({ currency }))
}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceDeposit.pattern}
component={({ amount }: { amount: string }) => (
<WalletTemplate path="balance">
<DepositPage
amount={amount}
onCancel={(currency: string) => {
redirectTo(Pages.balanceHistory({ currency }));
}}
onSuccess={(currency: string) => {
redirectTo(Pages.balanceHistory({ currency }));
}}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.balanceDeposit.pattern}
component={({ amount }: { amount: string }) => (
<WalletTemplate path="balance">
<DepositPage
amount={amount}
onCancel={(currency: string) => {
redirectTo(Pages.balanceHistory({ currency }));
}}
onSuccess={(currency: string) => {
redirectTo(Pages.balanceHistory({ currency }));
}}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backup}
component={() => (
<WalletTemplate
path="backup"
goToTransaction={redirectToTxInfo}
>
<BackupPage
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backupProviderDetail.pattern}
component={({ pid }: { pid: string }) => (
<WalletTemplate>
<ProviderDetailPage
pid={pid}
onPayProvider={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
onWithdraw={(amount: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
onBack={() => redirectTo(Pages.backup)}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backupProviderAdd}
component={() => (
<WalletTemplate>
<AddBackupProviderPage
onPaymentRequired={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
onComplete={(pid: string) =>
redirectTo(Pages.backupProviderDetail({ pid }))
}
onBack={() => redirectTo(Pages.backup)}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backup}
component={() => (
<WalletTemplate path="backup" goToTransaction={redirectToTxInfo}>
<BackupPage
onAddProvider={() => redirectTo(Pages.backupProviderAdd)}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backupProviderDetail.pattern}
component={({ pid }: { pid: string }) => (
<WalletTemplate>
<ProviderDetailPage
pid={pid}
onPayProvider={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
onWithdraw={(amount: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
onBack={() => redirectTo(Pages.backup)}
/>
</WalletTemplate>
)}
/>
<Route
path={Pages.backupProviderAdd}
component={() => (
<WalletTemplate>
<AddBackupProviderPage
onPaymentRequired={(uri: string) =>
redirectTo(`${Pages.ctaPay}?talerPayUri=${uri}`)
}
onComplete={(pid: string) =>
redirectTo(Pages.backupProviderDetail({ pid }))
}
onBack={() => redirectTo(Pages.backup)}
/>
</WalletTemplate>
)}
/>
{/**
* DEV
*/}
<Route
path={Pages.dev}
component={() => (
<WalletTemplate path="dev" goToTransaction={redirectToTxInfo}>
<DeveloperPage />
</WalletTemplate>
)}
/>
{/**
* DEV
*/}
<Route
path={Pages.dev}
component={() => (
<WalletTemplate path="dev" goToTransaction={redirectToTxInfo}>
<DeveloperPage />
</WalletTemplate>
)}
/>
{/**
* CALL TO ACTION
*/}
<Route
path={Pages.defaultCta.pattern}
component={({ uri }: { uri: string }) => {
const path = getPathnameForTalerURI(uri);
if (!path) {
return (
<CallToActionTemplate title={i18n.str`Taler URI handler`}>
<AlertView
alert={{
type: "warning",
message: i18n.str`Could not found a handler for the Taler URI`,
description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
}}
/>
</CallToActionTemplate>
);
}
return <Redirect to={path} />;
}}
/>
<Route
path={Pages.ctaPay}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentPage
talerPayUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaPayTemplate}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentTemplatePage
talerTemplateUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaRefund}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash refund`}>
<RefundPage
talerRefundUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTips}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash tip`}>
<TipPage
talerTipUri={decodeURIComponent(talerUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaWithdraw}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromURI
talerWithdrawUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaWithdrawManual.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromParams
amount={amount}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaDeposit}
component={({
amount,
talerUri,
}: {
amount: string;
talerUri: string;
}) => (
<CallToActionTemplate title={i18n.str`Digital cash deposit`}>
<DepositPageCTA
amountStr={amount}
talerDepositUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaInvoiceCreate.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoiceCreatePage
amount={amount}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTransferCreate.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferCreatePage
amount={amount}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaInvoicePay}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoicePayPage
talerPayPullUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTransferPickup}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferPickupPage
talerPayPushUri={decodeURIComponent(talerUri)}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaRecovery}
component={({
talerRecoveryUri,
}: {
talerRecoveryUri: string;
}) => (
<CallToActionTemplate title={i18n.str`Digital cash recovery`}>
<RecoveryPage
talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={() => redirectTo(Pages.backup)}
/>
</CallToActionTemplate>
)}
/>
{/**
* CALL TO ACTION
*/}
<Route
path={Pages.defaultCta.pattern}
component={({ uri }: { uri: string }) => {
const path = getPathnameForTalerURI(uri);
if (!path) {
return (
<CallToActionTemplate title={i18n.str`Taler URI handler`}>
<AlertView
alert={{
type: "warning",
message: i18n.str`Could not found a handler for the Taler URI`,
description: i18n.str`The uri read in the path parameter is not valid: "${uri}"`,
}}
/>
</CallToActionTemplate>
);
}
return <Redirect to={path} />;
}}
/>
<Route
path={Pages.ctaPay}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentPage
talerPayUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaPayTemplate}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash payment`}>
<PaymentTemplatePage
talerTemplateUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaRefund}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash refund`}>
<RefundPage
talerRefundUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTips}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash tip`}>
<TipPage
talerTipUri={decodeURIComponent(talerUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaWithdraw}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromURI
talerWithdrawUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaWithdrawManual.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash withdrawal`}>
<WithdrawPageFromParams
amount={amount}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaDeposit}
component={({
amount,
talerUri,
}: {
amount: string;
talerUri: string;
}) => (
<CallToActionTemplate title={i18n.str`Digital cash deposit`}>
<DepositPageCTA
amountStr={amount}
talerDepositUri={decodeURIComponent(talerUri)}
cancel={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaInvoiceCreate.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoiceCreatePage
amount={amount}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTransferCreate.pattern}
component={({ amount }: { amount: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferCreatePage
amount={amount}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaInvoicePay}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash invoice`}>
<InvoicePayPage
talerPayPullUri={decodeURIComponent(talerUri)}
goToWalletManualWithdraw={(amount?: string) =>
redirectTo(Pages.receiveCash({ amount }))
}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaTransferPickup}
component={({ talerUri }: { talerUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash transfer`}>
<TransferPickupPage
talerPayPushUri={decodeURIComponent(talerUri)}
onClose={() => redirectTo(Pages.balance)}
onSuccess={(tid: string) =>
redirectTo(Pages.balanceTransaction({ tid }))
}
/>
</CallToActionTemplate>
)}
/>
<Route
path={Pages.ctaRecovery}
component={({ talerRecoveryUri }: { talerRecoveryUri: string }) => (
<CallToActionTemplate title={i18n.str`Digital cash recovery`}>
<RecoveryPage
talerRecoveryUri={decodeURIComponent(talerRecoveryUri)}
onCancel={() => redirectTo(Pages.balance)}
onSuccess={() => redirectTo(Pages.backup)}
/>
</CallToActionTemplate>
)}
/>
{/**
* NOT FOUND
* all redirects should be at the end
*/}
<Route
path={Pages.balance}
component={() => <Redirect to={Pages.balanceHistory({})} />}
/>
{/**
* NOT FOUND
* all redirects should be at the end
*/}
<Route
path={Pages.balance}
component={() => <Redirect to={Pages.balanceHistory({})} />}
/>
<Route
default
component={() => <Redirect to={Pages.balanceHistory({})} />}
/>
</Router>
</IoCProviderForRuntime>
</DevContextProvider>
<Route
default
component={() => <Redirect to={Pages.balanceHistory({})} />}
/>
</Router>
</IoCProviderForRuntime>
</TranslationProvider>
);
}

View File

@ -17,7 +17,7 @@
import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js";
import { JustInDevMode } from "../../components/JustInDevMode.js";
import { EnabledBySettings } from "../../components/EnabledBySettings.js";
import { SelectList } from "../../components/SelectList.js";
import {
Input,
@ -300,11 +300,11 @@ export function ReadySendView({
required
handler={amountHandler}
/>
<JustInDevMode>
<EnabledBySettings name="advanceMode">
<Button onClick={sendAll.onClick}>
<i18n.Translate>Send all</i18n.Translate>
</Button>
</JustInDevMode>
</EnabledBySettings>
</Grid>
<Grid container spacing={1} columns={1}>

View File

@ -23,10 +23,10 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useState } from "preact/hooks";
import { alertFromError, useAlertContext } from "../../context/alert.js";
import { useBackendContext } from "../../context/backend.js";
import { useDevContext } from "../../context/devContext.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js";
import { AccountByType, Props, State } from "./index.js";
import { useSettings } from "../../hooks/useSettings.js";
export function useComponentState({
currency,
@ -39,12 +39,11 @@ export function useComponentState({
const hook = useAsyncAsHook(() =>
api.wallet.call(WalletApiOperation.ListKnownBankAccounts, { currency }),
);
const { devMode } = useDevContext();
const accountType: Record<string, string> = {
iban: "IBAN",
// "x-taler-bank": "Taler Bank",
};
if (devMode) {
const [settings] = useSettings();
if (settings.extendedAccountTypes) {
accountType["bitcoin"] = "Bitcoin";
accountType["x-taler-bank"] = "Taler Bank";
}

View File

@ -47,27 +47,27 @@ const version = {
export const AllOff = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
devModeToggle: { value: false, button: {} },
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
});
export const OneChecked = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
devModeToggle: { value: false, button: {} },
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
...version,
});
export const WithOneExchange = tests.createExample(TestedComponent, {
deviceName: "this-is-the-device-name",
devModeToggle: { value: false, button: {} },
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
knownExchanges: [
{
@ -89,9 +89,9 @@ export const WithExchangeInDifferentState = tests.createExample(
TestedComponent,
{
deviceName: "this-is-the-device-name",
devModeToggle: { value: false, button: {} },
advanceToggle: { value: false, button: {} },
autoOpenToggle: { value: false, button: {} },
clipboardToggle: { value: false, button: {} },
langToggle: { value: false, button: {} },
setDeviceName: () => Promise.resolve(),
knownExchanges: [
{

View File

@ -17,12 +17,15 @@
import {
ExchangeListItem,
ExchangeTosStatus,
TranslatedString,
WalletCoreVersion,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { Fragment, h, VNode } from "preact";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { Fragment, VNode, h } from "preact";
import { Pages } from "../NavigationBar.js";
import { Checkbox } from "../components/Checkbox.js";
import { JustInDevMode } from "../components/JustInDevMode.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
import { Part } from "../components/Part.js";
import { SelectList } from "../components/SelectList.js";
import {
@ -35,25 +38,20 @@ import {
} from "../components/styled/index.js";
import { useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useDevContext } from "../context/devContext.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { useSettings } from "../hooks/useSettings.js";
import { useBackupDeviceName } from "../hooks/useBackupDeviceName.js";
import { useClipboardPermissions } from "../hooks/useClipboardPermissions.js";
import { useSettings } from "../hooks/useSettings.js";
import { ToggleHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
import { platform } from "../platform/foreground.js";
import { Settings } from "../platform/api.js";
const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
export function SettingsPage(): VNode {
const [settings, updateSettings] = useSettings();
const clipboardToggle = useClipboardPermissions();
const { devMode, toggle } = useDevContext();
const { safely } = useAlertContext();
const { name, update } = useBackupDeviceName();
const { pushAlertOnError } = useAlertContext();
const webex = platform.getWalletWebExVersion();
const api = useBackendContext();
@ -76,16 +74,24 @@ export function SettingsPage(): VNode {
value: settings.injectTalerSupport,
button: {
onClick: safely("update support injection", async () => {
console.log("click", settings.injectTalerSupport);
updateSettings("injectTalerSupport", !settings.injectTalerSupport);
}),
},
}}
clipboardToggle={clipboardToggle}
devModeToggle={{
value: devMode,
advanceToggle={{
value: settings.advanceMode,
button: {
onClick: pushAlertOnError(toggle),
onClick: safely("update advance mode", async () => {
updateSettings("advanceMode", !settings.advanceMode);
}),
},
}}
langToggle={{
value: settings.langSelector,
button: {
onClick: safely("update lang selector", async () => {
updateSettings("langSelector", !settings.langSelector);
}),
},
}}
webexVersion={{
@ -101,8 +107,8 @@ export interface ViewProps {
deviceName: string;
setDeviceName: (s: string) => Promise<void>;
autoOpenToggle: ToggleHandler;
clipboardToggle: ToggleHandler;
devModeToggle: ToggleHandler;
advanceToggle: ToggleHandler;
langToggle: ToggleHandler;
knownExchanges: Array<ExchangeListItem>;
coreVersion: WalletCoreVersion | undefined;
webexVersion: {
@ -114,8 +120,8 @@ export interface ViewProps {
export function SettingsView({
knownExchanges,
autoOpenToggle,
clipboardToggle,
devModeToggle,
advanceToggle,
langToggle,
coreVersion,
webexVersion,
}: ViewProps): VNode {
@ -204,45 +210,19 @@ export function SettingsView({
</LinkPrimary>
</div>
<JustInDevMode>
<SubTitle>
<i18n.Translate>Display</i18n.Translate>
</SubTitle>
<Input>
<SelectList
label={<i18n.Translate>Current Language</i18n.Translate>}
list={supportedLang}
name="lang"
value={lang}
onChange={(v) => changeLanguage(v)}
/>
</Input>
</JustInDevMode>
<SubTitle>
<i18n.Translate>Version</i18n.Translate>
</SubTitle>
{coreVersion && (
<Part
title={i18n.str`Wallet Core`}
text={
<span>
{coreVersion.version}{" "}
<JustInDevMode>{coreVersion.hash}</JustInDevMode>
</span>
}
/>
)}
<Part
title={i18n.str`Web Extension`}
text={
<span>
{webexVersion.version}{" "}
<JustInDevMode>{webexVersion.hash}</JustInDevMode>
<EnabledBySettings name="advanceMode">
{webexVersion.hash}
</EnabledBySettings>
</span>
}
/>
{coreVersion && (
<JustInDevMode>
<EnabledBySettings name="advanceMode">
<Part
title={i18n.str`Exchange compatibility`}
text={<span>{coreVersion.exchange}</span>}
@ -255,18 +235,42 @@ export function SettingsView({
title={i18n.str`Bank compatibility`}
text={<span>{coreVersion.bank}</span>}
/>
</JustInDevMode>
</EnabledBySettings>
)}
<SubTitle>
<i18n.Translate>Troubleshooting</i18n.Translate>
<i18n.Translate>Advance mode</i18n.Translate>
</SubTitle>
<Checkbox
label={i18n.str`Developer mode`}
label={i18n.str`Enable advance mode`}
name="devMode"
description={i18n.str`More options and information useful for debugging`}
enabled={devModeToggle.value!}
onToggle={devModeToggle.button.onClick!}
description={i18n.str`Show more information and options in the UI`}
enabled={advanceToggle.value!}
onToggle={advanceToggle.button.onClick!}
/>
<EnabledBySettings name="advanceMode">
<AdvanceSettings />
</EnabledBySettings>
<Checkbox
label={i18n.str`Lang selector`}
name="langSelector"
description={i18n.str`Allows to manually change the language of the UI. Otherwise it will be automatically selected by your browser configuration.`}
enabled={langToggle.value!}
onToggle={langToggle.button.onClick!}
/>
<EnabledBySettings name="langSelector">
<SubTitle>
<i18n.Translate>Display</i18n.Translate>
</SubTitle>
<Input>
<SelectList
label={<i18n.Translate>Current Language</i18n.Translate>}
list={supportedLang}
name="lang"
value={lang}
onChange={(v) => changeLanguage(v)}
/>
</Input>
</EnabledBySettings>
<SubTitle>
<i18n.Translate>Navigator</i18n.Translate>
</SubTitle>
@ -283,6 +287,77 @@ export function SettingsView({
enabled={autoOpenToggle.value!}
onToggle={autoOpenToggle.button.onClick!}
/>
<SubTitle>
<i18n.Translate>Version</i18n.Translate>
</SubTitle>
{coreVersion && (
<Part
title={i18n.str`Wallet Core`}
text={
<span>
{coreVersion.version}{" "}
<EnabledBySettings name="advanceMode">
{coreVersion.hash}
</EnabledBySettings>
</span>
}
/>
)}
</section>
</Fragment>
);
}
type Info = { label: TranslatedString; description: TranslatedString };
type Options = {
[k in keyof Settings]?: Info;
};
function AdvanceSettings(): VNode {
const [settings, updateSettings] = useSettings();
const { i18n } = useTranslationContext();
const o: Options = {
backup: {
label: i18n.str`Show backup feature`,
description: i18n.str`Backup integration still in beta.`,
},
deleteActiveTransactions: {
label: i18n.str`Show delete active transaction`,
description: i18n.str`Deleting active transaction is not safe and you may loose your coins.`,
},
extendedAccountTypes: {
label: i18n.str`Show more account types on deposit`,
description: i18n.str`Extends the UI to more payment target types.`,
},
showJsonOnError: {
label: i18n.str`Show JSON on error`,
description: i18n.str`Print more information about the error. Useful for debugging.`,
},
walletAllowHttp: {
label: i18n.str`Allow HTTP connections`,
description: i18n.str`Using HTTP connection may be faster but unsafe (wallet restart required)`,
},
walletBatchWithdrawal: {
label: i18n.str`Allow batch withdrawals`,
description: i18n.str`Using the batch withdrawal API allows faster withdrawals (wallet restart required)`,
},
};
return (
<Fragment>
<section>
{Object.entries(o).map(([name, { label, description }]) => {
const settingsName = name as keyof Settings;
return (
<Checkbox
label={label}
name={name}
description={description}
enabled={settings[settingsName]}
onToggle={async () => {
updateSettings(settingsName, !settings[settingsName]);
}}
/>
);
})}
</section>
</Fragment>
);

View File

@ -34,6 +34,7 @@ import {
WithdrawalType,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { styled } from "@linaria/react";
import { differenceInSeconds, isPast } from "date-fns";
import { ComponentChildren, Fragment, h, VNode } from "preact";
@ -43,7 +44,6 @@ import { Amount } from "../components/Amount.js";
import { BankDetailsByPaytoType } from "../components/BankDetailsByPaytoType.js";
import { CopyButton } from "../components/CopyButton.js";
import { AlertView, ErrorAlertView } from "../components/CurrentAlerts.js";
import { JustInDevMode } from "../components/JustInDevMode.js";
import { Loading } from "../components/Loading.js";
import { Kind, Part, PartCollapsible, PartPayto } from "../components/Part.js";
import { QR } from "../components/QR.js";
@ -61,12 +61,12 @@ import {
import { Time } from "../components/Time.js";
import { alertFromError, useAlertContext } from "../context/alert.js";
import { useBackendContext } from "../context/backend.js";
import { useTranslationContext } from "@gnu-taler/web-util/lib/index.browser";
import { useAsyncAsHook } from "../hooks/useAsyncAsHook.js";
import { Button } from "../mui/Button.js";
import { SafeHandler } from "../mui/handlers.js";
import { Pages } from "../NavigationBar.js";
import { assertUnreachable } from "../utils/index.js";
import { EnabledBySettings } from "../components/EnabledBySettings.js";
interface Props {
tid: string;
@ -359,18 +359,15 @@ function TransactionTemplate({
<i18n.Translate>Cancel</i18n.Translate>
</Button>
) : (
//WORKAROUND
//Able to delete tx in dev mode
//FIXME: remove this when DD37 is implemented
<JustInDevMode>
<EnabledBySettings name="deleteActiveTransactions">
<Button
variant="contained"
color="error"
onClick={doCheckBeforeForget as SafeHandler<void>}
>
<i18n.Translate>Forget</i18n.Translate>
<i18n.Translate>Delete</i18n.Translate>
</Button>
</JustInDevMode>
</EnabledBySettings>
)
) : (
<Button
@ -378,7 +375,7 @@ function TransactionTemplate({
color="error"
onClick={doCheckBeforeForget as SafeHandler<void>}
>
<i18n.Translate>Forget</i18n.Translate>
<i18n.Translate>Delete</i18n.Translate>
</Button>
)}
</div>

View File

@ -27,7 +27,6 @@ import { setupPlatform } from "./platform/foreground.js";
import devAPI from "./platform/dev.js";
import { Application } from "./wallet/Application.js";
console.log("Wallet setup for Dev API");
setupPlatform(devAPI);
function main(): void {

View File

@ -33,10 +33,8 @@ const isFirefox = typeof (window as any)["InstallTrigger"] !== "undefined";
//FIXME: create different entry point for any platform instead of
//switching in runtime
if (isFirefox) {
console.log("Wallet setup for Firefox API");
setupPlatform(firefoxAPI);
} else {
console.log("Wallet setup for Chrome API");
setupPlatform(chromeAPI);
}

View File

@ -156,7 +156,7 @@ class WalletApiClientImpl implements WalletCoreApiClient {
};
response = await platform.sendMessageToBackground(message);
} catch (e) {
console.log("Error calling backend");
logger.error("Error calling backend", e);
throw new Error(`Error contacting backend: ${e}`);
}
if (response.type === "error") {

View File

@ -319,12 +319,19 @@ async function reinitWallet(): Promise<void> {
timer = new SetTimeoutTimerAPI();
}
const settings = await platform.getSettingsFromStorage();
logger.info("Setting up wallet");
const wallet = await Wallet.create(
currentDatabase,
httpLib,
timer,
cryptoWorker,
{
features: {
allowHttp: settings.walletAllowHttp,
batchWithdrawal: settings.walletBatchWithdrawal,
},
},
);
try {
await wallet.handleCoreApiRequest("initWallet", "native-init", {});