diff options
19 files changed, 440 insertions, 124 deletions
diff --git a/packages/taler-util/src/payto.ts b/packages/taler-util/src/payto.ts index 3073b991c..8eb0b88a8 100644 --- a/packages/taler-util/src/payto.ts +++ b/packages/taler-util/src/payto.ts @@ -139,12 +139,13 @@ export function parsePaytoUri(s: string): PaytoUri | undefined { let iban: string | undefined = undefined; let bic: string | undefined = undefined; if (parts.length === 1) { - iban = parts[0] - } if (parts.length === 2) { - bic = parts[0] - iban = parts[1] + iban = parts[0]; + } + if (parts.length === 2) { + bic = parts[0]; + iban = parts[1]; } else { - iban = targetPath + iban = targetPath; } return { isKnown: true, diff --git a/packages/taler-util/src/taler-types.ts b/packages/taler-util/src/taler-types.ts index d4f96f5cd..292ace94b 100644 --- a/packages/taler-util/src/taler-types.ts +++ b/packages/taler-util/src/taler-types.ts @@ -1297,7 +1297,7 @@ export const codecForProduct = (): Codec<Product> => .property("price", codecOptional(codecForString())) .build("Tax"); -export const codecForContractTerms = (): Codec<MerchantContractTerms> => +export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => buildCodecForObject<MerchantContractTerms>() .property("order_id", codecForString()) .property("fulfillment_url", codecOptional(codecForString())) @@ -1329,7 +1329,14 @@ export const codecForContractTerms = (): Codec<MerchantContractTerms> => .property("products", codecOptional(codecForList(codecForProduct()))) .property("extra", codecForAny()) .property("minimum_age", codecOptional(codecForNumber())) - .build("ContractTerms"); + .build("MerchantContractTerms"); + +export const codecForPeerContractTerms = (): Codec<PeerContractTerms> => + buildCodecForObject<PeerContractTerms>() + .property("summary", codecForString()) + .property("amount", codecForString()) + .property("purse_expiration", codecForTimestamp) + .build("PeerContractTerms"); export const codecForMerchantRefundPermission = (): Codec<MerchantAbortPayRefundDetails> => diff --git a/packages/taler-util/src/types-test.ts b/packages/taler-util/src/types-test.ts index 2915106c2..6acd2c26e 100644 --- a/packages/taler-util/src/types-test.ts +++ b/packages/taler-util/src/types-test.ts @@ -15,7 +15,7 @@ */ import test from "ava"; -import { codecForContractTerms } from "./taler-types.js"; +import { codecForMerchantContractTerms as codecForContractTerms } from "./taler-types.js"; test("contract terms validation", (t) => { const c = { diff --git a/packages/taler-util/src/wallet-types.ts b/packages/taler-util/src/wallet-types.ts index daeac73fd..4e1563e27 100644 --- a/packages/taler-util/src/wallet-types.ts +++ b/packages/taler-util/src/wallet-types.ts @@ -53,13 +53,15 @@ import { TalerErrorCode } from "./taler-error-codes.js"; import { AmountString, AuditorDenomSig, - codecForContractTerms, + codecForMerchantContractTerms, CoinEnvelope, MerchantContractTerms, + PeerContractTerms, DenominationPubKey, DenomKeyType, ExchangeAuditor, UnblindedSignature, + codecForPeerContractTerms, } from "./taler-types.js"; import { AbsoluteTime, @@ -253,7 +255,7 @@ export const codecForConfirmPayResultDone = (): Codec<ConfirmPayResultDone> => buildCodecForObject<ConfirmPayResultDone>() .property("type", codecForConstString(ConfirmPayResultType.Done)) .property("transactionId", codecForString()) - .property("contractTerms", codecForContractTerms()) + .property("contractTerms", codecForMerchantContractTerms()) .build("ConfirmPayResultDone"); export const codecForConfirmPayResult = (): Codec<ConfirmPayResult> => @@ -383,7 +385,7 @@ export const codecForPreparePayResultPaymentPossible = buildCodecForObject<PreparePayResultPaymentPossible>() .property("amountEffective", codecForAmountString()) .property("amountRaw", codecForAmountString()) - .property("contractTerms", codecForContractTerms()) + .property("contractTerms", codecForMerchantContractTerms()) .property("proposalId", codecForString()) .property("contractTermsHash", codecForString()) .property("noncePriv", codecForString()) @@ -1738,9 +1740,26 @@ export interface PayCoinSelection { customerDepositFees: AmountString; } -export interface InitiatePeerPushPaymentRequest { +export interface PreparePeerPushPaymentRequest { + exchangeBaseUrl?: string; amount: AmountString; - partialContractTerms: any; +} + +export const codecForPreparePeerPushPaymentRequest = + (): Codec<PreparePeerPushPaymentRequest> => + buildCodecForObject<PreparePeerPushPaymentRequest>() + .property("exchangeBaseUrl", codecOptional(codecForString())) + .property("amount", codecForAmountString()) + .build("InitiatePeerPushPaymentRequest"); + +export interface PreparePeerPushPaymentResponse { + amountRaw: AmountString; + amountEffective: AmountString; +} + +export interface InitiatePeerPushPaymentRequest { + exchangeBaseUrl?: string; + partialContractTerms: PeerContractTerms; } export interface InitiatePeerPushPaymentResponse { @@ -1755,8 +1774,7 @@ export interface InitiatePeerPushPaymentResponse { export const codecForInitiatePeerPushPaymentRequest = (): Codec<InitiatePeerPushPaymentRequest> => buildCodecForObject<InitiatePeerPushPaymentRequest>() - .property("amount", codecForAmountString()) - .property("partialContractTerms", codecForAny()) + .property("partialContractTerms", codecForPeerContractTerms()) .build("InitiatePeerPushPaymentRequest"); export interface CheckPeerPushPaymentRequest { @@ -1768,13 +1786,13 @@ export interface CheckPeerPullPaymentRequest { } export interface CheckPeerPushPaymentResponse { - contractTerms: any; + contractTerms: PeerContractTerms; amount: AmountString; peerPushPaymentIncomingId: string; } export interface CheckPeerPullPaymentResponse { - contractTerms: any; + contractTerms: PeerContractTerms; amount: AmountString; peerPullPaymentIncomingId: string; } @@ -1843,21 +1861,34 @@ export const codecForAcceptPeerPullPaymentRequest = .property("peerPullPaymentIncomingId", codecForString()) .build("AcceptPeerPllPaymentRequest"); +export interface PreparePeerPullPaymentRequest { + exchangeBaseUrl: string; + amount: AmountString; +} +export const codecForPreparePeerPullPaymentRequest = + (): Codec<PreparePeerPullPaymentRequest> => + buildCodecForObject<PreparePeerPullPaymentRequest>() + .property("amount", codecForAmountString()) + .property("exchangeBaseUrl", codecForString()) + .build("PreparePeerPullPaymentRequest"); + +export interface PreparePeerPullPaymentResponse { + amountRaw: AmountString; + amountEffective: AmountString; +} export interface InitiatePeerPullPaymentRequest { /** * FIXME: Make this optional? */ exchangeBaseUrl: string; - amount: AmountString; - partialContractTerms: any; + partialContractTerms: PeerContractTerms; } export const codecForInitiatePeerPullPaymentRequest = (): Codec<InitiatePeerPullPaymentRequest> => buildCodecForObject<InitiatePeerPullPaymentRequest>() - .property("partialContractTerms", codecForAny()) - .property("amount", codecForAmountString()) - .property("exchangeBaseUrl", codecForAmountString()) + .property("partialContractTerms", codecForPeerContractTerms()) + .property("exchangeBaseUrl", codecForString()) .build("InitiatePeerPullPaymentRequest"); export interface InitiatePeerPullPaymentResponse { diff --git a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts index 6ce21572e..e9d67eec6 100644 --- a/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts +++ b/packages/taler-wallet-core/src/crypto/workers/synchronousWorkerFactory.ts @@ -27,7 +27,6 @@ import { SynchronousCryptoWorker } from "./synchronousWorkerNode.js"; */ export class SynchronousCryptoWorkerFactory implements CryptoWorkerFactory { startWorker(): CryptoWorker { - return new SynchronousCryptoWorker(); } diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts index 3159c60af..5fd220113 100644 --- a/packages/taler-wallet-core/src/operations/backup/import.ts +++ b/packages/taler-wallet-core/src/operations/backup/import.ts @@ -26,7 +26,7 @@ import { BackupRefreshReason, BackupRefundState, BackupWgType, - codecForContractTerms, + codecForMerchantContractTerms, CoinStatus, DenomKeyType, DenomSelectionState, @@ -638,7 +638,7 @@ export async function importBackup( break; } } - const parsedContractTerms = codecForContractTerms().decode( + const parsedContractTerms = codecForMerchantContractTerms().decode( backupPurchase.contract_terms_raw, ); const amount = Amounts.parseOrThrow(parsedContractTerms.amount); diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts index 4483a57c0..bb391d468 100644 --- a/packages/taler-wallet-core/src/operations/pay-merchant.ts +++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts @@ -34,7 +34,7 @@ import { Amounts, ApplyRefundResponse, codecForAbortResponse, - codecForContractTerms, + codecForMerchantContractTerms, codecForMerchantOrderRefundPickupResponse, codecForMerchantOrderStatusPaid, codecForMerchantPayResponse, @@ -456,7 +456,7 @@ export async function processDownloadProposal( let parsedContractTerms: MerchantContractTerms; try { - parsedContractTerms = codecForContractTerms().decode( + parsedContractTerms = codecForMerchantContractTerms().decode( proposalResp.contract_terms, ); } catch (e) { @@ -1584,7 +1584,7 @@ export async function runPayForConfirmPay( const numRetry = opRetry?.retryInfo.retryCounter ?? 0; if ( res.errorDetail.code === - TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && + TalerErrorCode.WALLET_PAY_MERCHANT_SERVER_ERROR && numRetry < maxRetry ) { // Pretend the operation is pending instead of reporting diff --git a/packages/taler-wallet-core/src/operations/pay-peer.ts b/packages/taler-wallet-core/src/operations/pay-peer.ts index b6acef2dc..f31a7f37c 100644 --- a/packages/taler-wallet-core/src/operations/pay-peer.ts +++ b/packages/taler-wallet-core/src/operations/pay-peer.ts @@ -57,6 +57,10 @@ import { parsePayPullUri, parsePayPushUri, PeerContractTerms, + PreparePeerPullPaymentRequest, + PreparePeerPullPaymentResponse, + PreparePeerPushPaymentRequest, + PreparePeerPushPaymentResponse, RefreshReason, strcmp, TalerProtocolTimestamp, @@ -218,28 +222,30 @@ export async function selectPeerCoins( return undefined; } +export async function preparePeerPushPayment( + ws: InternalWalletState, + req: PreparePeerPushPaymentRequest, +): Promise<PreparePeerPushPaymentResponse> { + // FIXME: look up for the exchange and calculate fee + return { + amountEffective: req.amount, + amountRaw: req.amount, + }; +} + export async function initiatePeerToPeerPush( ws: InternalWalletState, req: InitiatePeerPushPaymentRequest, ): Promise<InitiatePeerPushPaymentResponse> { - const instructedAmount = Amounts.parseOrThrow(req.amount); + const instructedAmount = Amounts.parseOrThrow( + req.partialContractTerms.amount, + ); + const purseExpiration = req.partialContractTerms.purse_expiration; + const contractTerms = req.partialContractTerms; const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromSpec({ days: 2 }), - ), - ); - - const contractTerms = { - ...req.partialContractTerms, - purse_expiration: purseExpiration, - amount: req.amount, - }; - const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); const econtractResp = await ws.cryptoApi.encryptContractForMerge({ @@ -751,6 +757,16 @@ export async function checkPeerPullPayment( }; } +export async function preparePeerPullPayment( + ws: InternalWalletState, + req: PreparePeerPullPaymentRequest, +): Promise<PreparePeerPullPaymentResponse> { + //FIXME: look up for exchange details and use purse fee + return { + amountEffective: req.amount, + amountRaw: req.amount, + }; +} /** * Initiate a peer pull payment. */ @@ -769,24 +785,17 @@ export async function initiatePeerPullPayment( const pursePair = await ws.cryptoApi.createEddsaKeypair({}); const mergePair = await ws.cryptoApi.createEddsaKeypair({}); - const purseExpiration: TalerProtocolTimestamp = AbsoluteTime.toTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - Duration.fromSpec({ days: 2 }), - ), + const instructedAmount = Amounts.parseOrThrow( + req.partialContractTerms.amount, ); + const purseExpiration = req.partialContractTerms.purse_expiration; + const contractTerms = req.partialContractTerms; const reservePayto = talerPaytoFromExchangeReserve( req.exchangeBaseUrl, mergeReserveInfo.reservePub, ); - const contractTerms = { - ...req.partialContractTerms, - amount: req.amount, - purse_expiration: purseExpiration, - }; - const econtractResp = await ws.cryptoApi.encryptContractForDeposit({ contractTerms, pursePriv: pursePair.priv, @@ -796,7 +805,7 @@ export async function initiatePeerPullPayment( const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms); const purseFee = Amounts.stringify( - Amounts.zeroOfCurrency(Amounts.parseOrThrow(req.amount).currency), + Amounts.zeroOfCurrency(instructedAmount.currency), ); const sigRes = await ws.cryptoApi.signReservePurseCreate({ @@ -804,7 +813,7 @@ export async function initiatePeerPullPayment( flags: WalletAccountMergeFlags.CreateWithPurseFee, mergePriv: mergePair.priv, mergeTimestamp: mergeTimestamp, - purseAmount: req.amount, + purseAmount: req.partialContractTerms.amount, purseExpiration: purseExpiration, purseFee: purseFee, pursePriv: pursePair.priv, @@ -817,7 +826,7 @@ export async function initiatePeerPullPayment( .mktx((x) => [x.peerPullPaymentInitiations, x.contractTerms]) .runReadWrite(async (tx) => { await tx.peerPullPaymentInitiations.put({ - amount: req.amount, + amount: req.partialContractTerms.amount, contractTermsHash: hContractTerms, exchangeBaseUrl: req.exchangeBaseUrl, pursePriv: pursePair.priv, @@ -840,7 +849,7 @@ export async function initiatePeerPullPayment( purse_fee: purseFee, purse_pub: pursePair.pub, purse_sig: sigRes.purseSig, - purse_value: req.amount, + purse_value: req.partialContractTerms.amount, reserve_sig: sigRes.accountSig, econtract: econtractResp.econtract, }; @@ -862,7 +871,7 @@ export async function initiatePeerPullPayment( logger.info(`reserve merge response: ${j2s(resp)}`); const wg = await internalCreateWithdrawalGroup(ws, { - amount: Amounts.parseOrThrow(req.amount), + amount: instructedAmount, wgInfo: { withdrawalType: WithdrawalRecordType.PeerPullCredit, contractTerms, diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts index e36e630f4..b7d0ada3d 100644 --- a/packages/taler-wallet-core/src/wallet-api-types.ts +++ b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -75,6 +75,10 @@ import { PrepareDepositResponse, PreparePayRequest, PreparePayResult, + PreparePeerPullPaymentRequest, + PreparePeerPullPaymentResponse, + PreparePeerPushPaymentRequest, + PreparePeerPushPaymentResponse, PrepareRefundRequest, PrepareRefundResult, PrepareTipRequest, @@ -164,9 +168,11 @@ export enum WalletApiOperation { WithdrawFakebank = "withdrawFakebank", ImportDb = "importDb", ExportDb = "exportDb", + PreparePeerPushPayment = "preparePeerPushPayment", InitiatePeerPushPayment = "initiatePeerPushPayment", CheckPeerPushPayment = "checkPeerPushPayment", AcceptPeerPushPayment = "acceptPeerPushPayment", + PreparePeerPullPayment = "preparePeerPullPayment", InitiatePeerPullPayment = "initiatePeerPullPayment", CheckPeerPullPayment = "checkPeerPullPayment", AcceptPeerPullPayment = "acceptPeerPullPayment", @@ -556,6 +562,15 @@ export type ExportBackupPlainOp = { /** * Initiate an outgoing peer push payment. */ +export type PreparePeerPushPaymentOp = { + op: WalletApiOperation.PreparePeerPushPayment; + request: PreparePeerPushPaymentRequest; + response: PreparePeerPushPaymentResponse; +}; + +/** + * Initiate an outgoing peer push payment. + */ export type InitiatePeerPushPaymentOp = { op: WalletApiOperation.InitiatePeerPushPayment; request: InitiatePeerPushPaymentRequest; @@ -583,6 +598,15 @@ export type AcceptPeerPushPaymentOp = { /** * Initiate an outgoing peer pull payment. */ +export type PreparePeerPullPaymentOp = { + op: WalletApiOperation.PreparePeerPullPayment; + request: PreparePeerPullPaymentRequest; + response: PreparePeerPullPaymentResponse; +}; + +/** + * Initiate an outgoing peer pull payment. + */ export type InitiatePeerPullPaymentOp = { op: WalletApiOperation.InitiatePeerPullPayment; request: InitiatePeerPullPaymentRequest; @@ -815,9 +839,11 @@ export type WalletOperations = { [WalletApiOperation.TestPay]: TestPayOp; [WalletApiOperation.ExportDb]: ExportDbOp; [WalletApiOperation.ImportDb]: ImportDbOp; + [WalletApiOperation.PreparePeerPushPayment]: PreparePeerPushPaymentOp; [WalletApiOperation.InitiatePeerPushPayment]: InitiatePeerPushPaymentOp; [WalletApiOperation.CheckPeerPushPayment]: CheckPeerPushPaymentOp; [WalletApiOperation.AcceptPeerPushPayment]: AcceptPeerPushPaymentOp; + [WalletApiOperation.PreparePeerPullPayment]: PreparePeerPullPaymentOp; [WalletApiOperation.InitiatePeerPullPayment]: InitiatePeerPullPaymentOp; [WalletApiOperation.CheckPeerPullPayment]: CheckPeerPullPaymentOp; [WalletApiOperation.AcceptPeerPullPayment]: AcceptPeerPullPaymentOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 9339b2f8e..caaf6d410 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -57,6 +57,8 @@ import { codecForListKnownBankAccounts, codecForPrepareDepositRequest, codecForPreparePayRequest, + codecForPreparePeerPullPaymentRequest, + codecForPreparePeerPushPaymentRequest, codecForPrepareRefundRequest, codecForPrepareTipRequest, codecForRetryTransactionRequest, @@ -186,6 +188,8 @@ import { checkPeerPushPayment, initiatePeerPullPayment, initiatePeerToPeerPush, + preparePeerPullPayment, + preparePeerPushPayment, } from "./operations/pay-peer.js"; import { getPendingOperations } from "./operations/pending.js"; import { @@ -659,7 +663,9 @@ async function getExchanges( const opRetryRecord = await tx.operationRetries.get( RetryTags.forExchangeUpdate(r), ); - exchanges.push(makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError)); + exchanges.push( + makeExchangeListItem(r, exchangeDetails, opRetryRecord?.lastError), + ); } }); return { exchanges }; @@ -927,9 +933,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> { ageCommitmentProof: c.ageCommitmentProof, spend_allocation: c.spendAllocation ? { - amount: c.spendAllocation.amount, - id: c.spendAllocation.id, - } + amount: c.spendAllocation.amount, + id: c.spendAllocation.id, + } : undefined, }); } @@ -1340,6 +1346,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( await importDb(ws.db.idbHandle(), req.dump); return []; } + case WalletApiOperation.PreparePeerPushPayment: { + const req = codecForPreparePeerPushPaymentRequest().decode(payload); + return await preparePeerPushPayment(ws, req); + } case WalletApiOperation.InitiatePeerPushPayment: { const req = codecForInitiatePeerPushPaymentRequest().decode(payload); return await initiatePeerToPeerPush(ws, req); @@ -1352,6 +1362,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( const req = codecForAcceptPeerPushPaymentRequest().decode(payload); return await acceptPeerPushPayment(ws, req); } + case WalletApiOperation.PreparePeerPullPayment: { + const req = codecForPreparePeerPullPaymentRequest().decode(payload); + return await preparePeerPullPayment(ws, req); + } case WalletApiOperation.InitiatePeerPullPayment: { const req = codecForInitiatePeerPullPaymentRequest().decode(payload); return await initiatePeerPullPayment(ws, req); diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts index 0389a17fb..01dbb6d6d 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/index.ts @@ -59,10 +59,10 @@ export namespace State { doSelectExchange: ButtonHandler; create: ButtonHandler; subject: TextFieldHandler; + expiration: TextFieldHandler; toBeReceived: AmountJson; - chosenAmount: AmountJson; + requestAmount: AmountJson; exchangeUrl: string; - invalid: boolean; error: undefined; operationError?: TalerErrorDetail; } diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts index d845e121a..27f05ce03 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/state.ts @@ -15,8 +15,9 @@ */ /* eslint-disable react-hooks/rules-of-hooks */ -import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { isFuture, parse } from "date-fns"; import { useState } from "preact/hooks"; import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { useSelectedExchange } from "../../hooks/useSelectedExchange.js"; @@ -49,7 +50,8 @@ export function useComponentState( const exchangeList = hook.response.exchanges; return () => { - const [subject, setSubject] = useState(""); + const [subject, setSubject] = useState<string | undefined>(); + const [timestamp, setTimestamp] = useState<string | undefined>() const [operationError, setOperationError] = useState< TalerErrorDetail | undefined @@ -67,13 +69,59 @@ export function useComponentState( const exchange = selectedExchange.selected; + const hook = useAsyncAsHook(async () => { + const resp = await api.wallet.call(WalletApiOperation.PreparePeerPullPayment, { + amount: amountStr, + exchangeBaseUrl: exchange.exchangeBaseUrl, + }) + return resp + }) + + if (!hook) { + return { + status: "loading", + error: undefined + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook + } + } + + const { amountEffective, amountRaw } = hook.response + const requestAmount = Amounts.parseOrThrow(amountRaw) + const toBeReceived = Amounts.parseOrThrow(amountEffective) + + let purse_expiration: TalerProtocolTimestamp | undefined = undefined + let timestampError: string | undefined = undefined; + + const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) + + if (t !== undefined) { + if (Number.isNaN(t.getTime())) { + timestampError = 'Should have the format "dd/MM/yyyy"' + } else { + if (!isFuture(t)) { + timestampError = 'Should be in the future' + } else { + purse_expiration = { + t_s: t.getTime() / 1000 + } + } + } + } + async function accept(): Promise<void> { + if (!subject || !purse_expiration) return; try { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPullPayment, { - amount: Amounts.stringify(amount), exchangeBaseUrl: exchange.exchangeBaseUrl, partialContractTerms: { + amount: Amounts.stringify(amount), summary: subject, + purse_expiration }, }); @@ -86,25 +134,32 @@ export function useComponentState( throw Error("error trying to accept"); } } + const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration return { status: "ready", subject: { - error: !subject ? "cant be empty" : undefined, - value: subject, + error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, + value: subject ?? "", onInput: async (e) => setSubject(e), }, + expiration: { + error: timestampError, + value: timestamp === undefined ? "" : timestamp, + onInput: async (e) => { + setTimestamp(e) + } + }, doSelectExchange: selectedExchange.doSelect, - invalid: !subject || Amounts.isZero(amount), exchangeUrl: exchange.exchangeBaseUrl, create: { - onClick: accept, + onClick: unableToCreate ? undefined : accept, }, cancel: { onClick: onClose, }, - chosenAmount: amount, - toBeReceived: amount, + requestAmount, + toBeReceived, error: undefined, operationError, }; diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx index 77885b0c1..8d4473d8f 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/stories.tsx @@ -27,11 +27,14 @@ export default { }; export const Ready = createExample(ReadyView, { - chosenAmount: { + requestAmount: { currency: "ARS", value: 1, fraction: 0, }, + expiration: { + value: "2/12/12", + }, cancel: {}, toBeReceived: { currency: "ARS", diff --git a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx index 4970f590f..f15482953 100644 --- a/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/InvoiceCreate/views.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { format } from "date-fns"; import { h, VNode } from "preact"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; @@ -46,18 +47,40 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { } export function ReadyView({ - invalid, exchangeUrl, subject, + expiration, cancel, operationError, create, toBeReceived, - chosenAmount, + requestAmount, doSelectExchange, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + async function oneDayExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"), + ); + } + } + + async function oneWeekExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"), + ); + } + } + async function _20DaysExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"), + ); + } + } return ( <WalletAction> <LogoHeader /> @@ -75,16 +98,6 @@ export function ReadyView({ /> )} <section style={{ textAlign: "left" }}> - <TextField - label="Subject" - variant="filled" - error={subject.error} - required - fullWidth - value={subject.value} - onChange={subject.onInput} - /> - <Part title={ <div @@ -107,6 +120,52 @@ export function ReadyView({ kind="neutral" big /> + <p> + <TextField + label="Subject" + variant="filled" + error={subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + </p> + + <p> + <TextField + label="Expiration" + variant="filled" + error={expiration.error} + required + fullWidth + value={expiration.value} + onChange={expiration.onInput} + /> + <p> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneDayExpiration} + > + 1 day + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneWeekExpiration} + > + 1 week + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={_20DaysExpiration} + > + 20 days + </Button> + </p> + </p> <Part title={<i18n.Translate>Details</i18n.Translate>} @@ -114,19 +173,14 @@ export function ReadyView({ <InvoiceDetails amount={{ effective: toBeReceived, - raw: chosenAmount, + raw: requestAmount, }} /> } /> </section> <section> - <Button - disabled={invalid} - onClick={create.onClick} - variant="contained" - color="success" - > + <Button onClick={create.onClick} variant="contained" color="success"> <i18n.Translate>Create</i18n.Translate> </Button> </section> diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts index 83293438f..8d51ff3e0 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/index.ts @@ -48,11 +48,11 @@ export namespace State { } export interface Ready extends BaseInfo { status: "ready"; - invalid: boolean; create: ButtonHandler; toBeReceived: AmountJson; - chosenAmount: AmountJson; + debitAmount: AmountJson; subject: TextFieldHandler; + expiration: TextFieldHandler; error: undefined; operationError?: TalerErrorDetail; } diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts index b229924b2..089f46047 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/state.ts @@ -14,9 +14,11 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ -import { Amounts, TalerErrorDetail } from "@gnu-taler/taler-util"; +import { Amounts, TalerErrorDetail, TalerProtocolTimestamp } from "@gnu-taler/taler-util"; import { TalerError, WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { format, isFuture, parse } from "date-fns"; import { useState } from "preact/hooks"; +import { useAsyncAsHook } from "../../hooks/useAsyncAsHook.js"; import { wxApi } from "../../wxApi.js"; import { Props, State } from "./index.js"; @@ -26,17 +28,65 @@ export function useComponentState( ): State { const amount = Amounts.parseOrThrow(amountStr); - const [subject, setSubject] = useState(""); + const [subject, setSubject] = useState<string | undefined>(); + const [timestamp, setTimestamp] = useState<string | undefined>() + const [operationError, setOperationError] = useState< TalerErrorDetail | undefined >(undefined); + + const hook = useAsyncAsHook(async () => { + const resp = await api.wallet.call(WalletApiOperation.PreparePeerPushPayment, { + amount: amountStr + }) + return resp + }) + + if (!hook) { + return { + status: "loading", + error: undefined + } + } + if (hook.hasError) { + return { + status: "loading-uri", + error: hook + } + } + + const { amountEffective, amountRaw } = hook.response + const debitAmount = Amounts.parseOrThrow(amountRaw) + const toBeReceived = Amounts.parseOrThrow(amountEffective) + + let purse_expiration: TalerProtocolTimestamp | undefined = undefined + let timestampError: string | undefined = undefined; + + const t = timestamp === undefined ? undefined : parse(timestamp, "dd/MM/yyyy", new Date()) + + if (t !== undefined) { + if (Number.isNaN(t.getTime())) { + timestampError = 'Should have the format "dd/MM/yyyy"' + } else { + if (!isFuture(t)) { + timestampError = 'Should be in the future' + } else { + purse_expiration = { + t_s: t.getTime() / 1000 + } + } + } + } + async function accept(): Promise<void> { + if (!subject || !purse_expiration) return; try { const resp = await api.wallet.call(WalletApiOperation.InitiatePeerPushPayment, { - amount: Amounts.stringify(amount), partialContractTerms: { summary: subject, + amount: amountStr, + purse_expiration }, }); onSuccess(resp.transactionId); @@ -48,22 +98,31 @@ export function useComponentState( throw Error("error trying to accept"); } } + + const unableToCreate = !subject || Amounts.isZero(amount) || !purse_expiration + return { status: "ready", - invalid: !subject || Amounts.isZero(amount), cancel: { onClick: onClose, }, subject: { - error: !subject ? "cant be empty" : undefined, - value: subject, + error: subject === undefined ? undefined : !subject ? "Can't be empty" : undefined, + value: subject ?? "", onInput: async (e) => setSubject(e), }, + expiration: { + error: timestampError, + value: timestamp === undefined ? "" : timestamp, + onInput: async (e) => { + setTimestamp(e) + } + }, create: { - onClick: accept, + onClick: unableToCreate ? undefined : accept, }, - chosenAmount: amount, - toBeReceived: amount, + debitAmount, + toBeReceived, error: undefined, operationError, }; diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx index 2746cc153..de781f008 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/stories.tsx @@ -27,11 +27,14 @@ export default { }; export const Ready = createExample(ReadyView, { - chosenAmount: { + debitAmount: { currency: "ARS", value: 1, fraction: 0, }, + expiration: { + value: "20/1/2022", + }, create: {}, cancel: {}, toBeReceived: { diff --git a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx index bca806c5d..7b1c208b9 100644 --- a/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx +++ b/packages/taler-wallet-webextension/src/cta/TransferCreate/views.tsx @@ -14,6 +14,7 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { format } from "date-fns"; import { h, VNode } from "preact"; import { ErrorTalerOperation } from "../../components/ErrorTalerOperation.js"; import { LoadingError } from "../../components/LoadingError.js"; @@ -40,14 +41,37 @@ export function LoadingUriView({ error }: State.LoadingUriError): VNode { export function ReadyView({ subject, + expiration, toBeReceived, - chosenAmount, + debitAmount, create, operationError, cancel, - invalid, }: State.Ready): VNode { const { i18n } = useTranslationContext(); + + async function oneDayExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24, "dd/MM/yyyy"), + ); + } + } + + async function oneWeekExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 7, "dd/MM/yyyy"), + ); + } + } + async function _20DaysExpiration() { + if (expiration.onInput) { + expiration.onInput( + format(new Date().getTime() + 1000 * 60 * 60 * 24 * 20, "dd/MM/yyyy"), + ); + } + } return ( <WalletAction> <LogoHeader /> @@ -65,34 +89,65 @@ export function ReadyView({ /> )} <section style={{ textAlign: "left" }}> - <TextField - label="Subject" - variant="filled" - error={subject.error} - required - fullWidth - value={subject.value} - onChange={subject.onInput} - /> + <p> + <TextField + label="Subject" + variant="filled" + error={subject.error} + required + fullWidth + value={subject.value} + onChange={subject.onInput} + /> + </p> + <p> + <TextField + label="Expiration" + variant="filled" + error={expiration.error} + required + fullWidth + value={expiration.value} + onChange={expiration.onInput} + /> + <p> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneDayExpiration} + > + 1 day + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={oneWeekExpiration} + > + 1 week + </Button> + <Button + variant="outlined" + disabled={!expiration.onInput} + onClick={_20DaysExpiration} + > + 20 days + </Button> + </p> + </p> <Part title={<i18n.Translate>Details</i18n.Translate>} text={ <TransferDetails amount={{ effective: toBeReceived, - raw: chosenAmount, + raw: debitAmount, }} /> } /> </section> <section> - <Button - disabled={invalid} - onClick={create.onClick} - variant="contained" - color="success" - > + <Button onClick={create.onClick} variant="contained" color="success"> <i18n.Translate>Create</i18n.Translate> </Button> </section> diff --git a/packages/taler-wallet-webextension/src/mui/Button.tsx b/packages/taler-wallet-webextension/src/mui/Button.tsx index 0aaa5ee97..bca0d6231 100644 --- a/packages/taler-wallet-webextension/src/mui/Button.tsx +++ b/packages/taler-wallet-webextension/src/mui/Button.tsx @@ -290,7 +290,7 @@ export function Button({ return ( <ButtonBase - disabled={disabled || running} + disabled={disabled || running || !doClick} class={[ theme.typography.button, theme.shape.roundBorder, |