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 => .property("price", codecOptional(codecForString())) .build("Tax"); -export const codecForContractTerms = (): Codec => +export const codecForMerchantContractTerms = (): Codec => buildCodecForObject() .property("order_id", codecForString()) .property("fulfillment_url", codecOptional(codecForString())) @@ -1329,7 +1329,14 @@ export const codecForContractTerms = (): Codec => .property("products", codecOptional(codecForList(codecForProduct()))) .property("extra", codecForAny()) .property("minimum_age", codecOptional(codecForNumber())) - .build("ContractTerms"); + .build("MerchantContractTerms"); + +export const codecForPeerContractTerms = (): Codec => + buildCodecForObject() + .property("summary", codecForString()) + .property("amount", codecForString()) + .property("purse_expiration", codecForTimestamp) + .build("PeerContractTerms"); export const codecForMerchantRefundPermission = (): Codec => 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 => buildCodecForObject() .property("type", codecForConstString(ConfirmPayResultType.Done)) .property("transactionId", codecForString()) - .property("contractTerms", codecForContractTerms()) + .property("contractTerms", codecForMerchantContractTerms()) .build("ConfirmPayResultDone"); export const codecForConfirmPayResult = (): Codec => @@ -383,7 +385,7 @@ export const codecForPreparePayResultPaymentPossible = buildCodecForObject() .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 => + buildCodecForObject() + .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 => buildCodecForObject() - .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 => + buildCodecForObject() + .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 => buildCodecForObject() - .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 { + // 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 { - 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 { + //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", @@ -553,6 +559,15 @@ export type ExportBackupPlainOp = { // group: Peer Payments +/** + * Initiate an outgoing peer push payment. + */ +export type PreparePeerPushPaymentOp = { + op: WalletApiOperation.PreparePeerPushPayment; + request: PreparePeerPushPaymentRequest; + response: PreparePeerPushPaymentResponse; +}; + /** * Initiate an outgoing peer push payment. */ @@ -580,6 +595,15 @@ export type AcceptPeerPushPaymentOp = { response: EmptyObject; }; +/** + * Initiate an outgoing peer pull payment. + */ +export type PreparePeerPullPaymentOp = { + op: WalletApiOperation.PreparePeerPullPayment; + request: PreparePeerPullPaymentRequest; + response: PreparePeerPullPaymentResponse; +}; + /** * Initiate an outgoing peer pull payment. */ @@ -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 { 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( 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( 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(); + const [timestamp, setTimestamp] = useState() 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 { + 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 */ +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 ( @@ -75,16 +98,6 @@ export function ReadyView({ /> )}
- - +

+ +

+ +

+ +

+ + + +

+

Details} @@ -114,19 +173,14 @@ export function ReadyView({ } />
-
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 */ -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(); + const [timestamp, setTimestamp] = useState() + 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 { + 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), }, - create: { - onClick: accept, + expiration: { + error: timestampError, + value: timestamp === undefined ? "" : timestamp, + onInput: async (e) => { + setTimestamp(e) + } }, - chosenAmount: amount, - toBeReceived: amount, + create: { + onClick: unableToCreate ? undefined : accept, + }, + 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 */ +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 ( @@ -65,34 +89,65 @@ export function ReadyView({ /> )}
- +

+ +

+

+ +

+ + + +

+

Details} text={ } />
-
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 (