diff options
Diffstat (limited to 'packages')
13 files changed, 183 insertions, 24 deletions
diff --git a/packages/taler-util/src/transactionsTypes.ts b/packages/taler-util/src/transactionsTypes.ts index dcaa56675..f7383f902 100644 --- a/packages/taler-util/src/transactionsTypes.ts +++ b/packages/taler-util/src/transactionsTypes.ts @@ -244,6 +244,11 @@ export interface TransactionPayment extends TransactionCommon { * Amount pending to be picked up */ refundPending: AmountString | undefined; + + /** + * Reference to applied refunds + */ + refunds: RefundInfoShort[]; } export interface OrderShortInfo { @@ -305,6 +310,13 @@ export interface OrderShortInfo { fulfillmentMessage_i18n?: InternationalizedString; } +export interface RefundInfoShort { + transactionId: string, + timestamp: TalerProtocolTimestamp, + amountEffective: AmountString, + amountRaw: AmountString, +} + export interface TransactionRefund extends TransactionCommon { type: TransactionType.Refund; diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts index fa884c414..00a489861 100644 --- a/packages/taler-util/src/walletTypes.ts +++ b/packages/taler-util/src/walletTypes.ts @@ -799,6 +799,15 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> => .property("talerRefundUri", codecForString()) .build("ApplyRefundRequest"); +export interface ApplyRefundFromPurchaseIdRequest { + purchaseId: string; +} + +export const codecForApplyRefundFromPurchaseIdRequest = (): Codec<ApplyRefundFromPurchaseIdRequest> => + buildCodecForObject<ApplyRefundFromPurchaseIdRequest>() + .property("purchaseId", codecForString()) + .build("ApplyRefundFromPurchaseIdRequest"); + export interface GetWithdrawalDetailsForUriRequest { talerWithdrawUri: string; restrictAge?: number; diff --git a/packages/taler-wallet-core/src/operations/refund.ts b/packages/taler-wallet-core/src/operations/refund.ts index 186fbf7d3..28a92286b 100644 --- a/packages/taler-wallet-core/src/operations/refund.ts +++ b/packages/taler-wallet-core/src/operations/refund.ts @@ -573,7 +573,7 @@ export async function applyRefund( throw Error("invalid refund URI"); } - let purchase = await ws.db + const purchase = await ws.db .mktx((x) => ({ purchases: x.purchases, })) @@ -590,7 +590,15 @@ export async function applyRefund( ); } - const proposalId = purchase.proposalId; + return applyRefundFromPurchaseId(ws, purchase.proposalId) +} + +export async function applyRefundFromPurchaseId( + ws: InternalWalletState, + proposalId: string, +): Promise<ApplyRefundResponse> { + + logger.trace("applying refund for purchase", proposalId); logger.info("processing purchase for refund"); const success = await ws.db @@ -620,7 +628,7 @@ export async function applyRefund( }); } - purchase = await ws.db + const purchase = await ws.db .mktx((x) => ({ purchases: x.purchases, })) diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 87b109d98..db282bb68 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -24,6 +24,7 @@ import { Logger, OrderShortInfo, PaymentStatus, + RefundInfoShort, Transaction, TransactionsRequest, TransactionsResponse, @@ -306,6 +307,7 @@ export async function getTransactions( let totalRefundRaw = Amounts.getZero(contractData.amount.currency); let totalRefundEffective = Amounts.getZero(contractData.amount.currency); + const refunds: RefundInfoShort[] = [] for (const groupKey of refundGroupKeys.values()) { const refundTombstoneId = makeEventId( @@ -345,6 +347,13 @@ export async function getTransactions( refund.totalRefreshCostBound, ).amount, ).amount; + + refunds.push({ + transactionId: refundTransactionId, + timestamp: r0.obtainedTime, + amountEffective: Amounts.stringify(amountEffective), + amountRaw: Amounts.stringify(amountRaw), + }) } } if (!r0) { @@ -353,7 +362,6 @@ export async function getTransactions( totalRefundRaw = Amounts.add(totalRefundRaw, amountRaw).amount; totalRefundEffective = Amounts.add(totalRefundEffective, amountEffective).amount; - transactions.push({ type: TransactionType.Refund, info, @@ -382,10 +390,11 @@ export async function getTransactions( pending: !pr.timestampFirstSuccessfulPay && pr.abortStatus === AbortStatus.None, + refunds, timestamp: pr.timestampAccept, transactionId: paymentTransactionId, proposalId: pr.proposalId, - info: info, + info, frozen: pr.payFrozen ?? false, ...(err ? { error: err } : {}), }); diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index ffceec38f..689e45f3c 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -33,6 +33,7 @@ import { codecForAcceptTipRequest, codecForAddExchangeRequest, codecForAny, + codecForApplyRefundFromPurchaseIdRequest, codecForApplyRefundRequest, codecForConfirmPayRequest, codecForCreateDepositGroupRequest, @@ -145,6 +146,7 @@ import { import { abortFailedPayWithRefund, applyRefund, + applyRefundFromPurchaseId, prepareRefund, processPurchaseQueryRefund } from "./operations/refund.js"; @@ -839,6 +841,10 @@ async function dispatchRequestInternal( const req = codecForApplyRefundRequest().decode(payload); return await applyRefund(ws, req.talerRefundUri); } + case "applyRefundFromPurchaseId": { + const req = codecForApplyRefundFromPurchaseIdRequest().decode(payload); + return await applyRefundFromPurchaseId(ws, req.purchaseId); + } case "acceptBankIntegratedWithdrawal": { const req = codecForAcceptBankIntegratedWithdrawalRequest().decode(payload); diff --git a/packages/taler-wallet-webextension/src/components/Part.tsx b/packages/taler-wallet-webextension/src/components/Part.tsx index 58165a349..06e2985cf 100644 --- a/packages/taler-wallet-webextension/src/components/Part.tsx +++ b/packages/taler-wallet-webextension/src/components/Part.tsx @@ -92,6 +92,7 @@ const CollasibleBox = styled.div` } `; import arrowDown from "../svg/chevron-down.svg"; +import { useTranslationContext } from "../context/translation.js"; export function PartCollapsible({ text, title, big, showSign }: Props): VNode { const Text = big ? ExtraLargeText : LargeText; @@ -137,27 +138,37 @@ interface PropsPayto { } export function PartPayto({ payto, kind, big }: PropsPayto): VNode { const Text = big ? ExtraLargeText : LargeText; - let text: string | undefined = undefined; + let text: VNode | undefined = undefined; let title = ""; + const { i18n } = useTranslationContext(); if (payto.isKnown) { if (payto.targetType === "x-taler-bank") { - text = payto.account; - title = "Bank account"; + text = <Fragment>{payto.account}</Fragment>; + title = i18n.str`Bank account`; } else if (payto.targetType === "bitcoin") { - text = payto.targetPath; - title = "Bitcoin addr"; + text = + payto.segwitAddrs && payto.segwitAddrs.length > 0 ? ( + <ul> + <li>{payto.targetPath}</li> + <li>{payto.segwitAddrs[0]}</li> + <li>{payto.segwitAddrs[1]}</li> + </ul> + ) : ( + <Fragment>{payto.targetPath}</Fragment> + ); + title = i18n.str`Bitcoin address`; } else if (payto.targetType === "iban") { - text = payto.targetPath; - title = "IBAN"; + text = <Fragment>{payto.targetPath}</Fragment>; + title = i18n.str`IBAN`; } } if (!text) { - text = stringifyPaytoUri(payto); + text = <Fragment>{stringifyPaytoUri(payto)}</Fragment>; title = "Payto URI"; } return ( <div style={{ margin: "1em" }}> - <SmallLightText style={{ margin: ".5em" }}>{title}</SmallLightText> + <SmallBoldText>{title}</SmallBoldText> <Text style={{ color: diff --git a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx index 985ddf552..bfffa3267 100644 --- a/packages/taler-wallet-webextension/src/components/TransactionItem.tsx +++ b/packages/taler-wallet-webextension/src/components/TransactionItem.tsx @@ -207,7 +207,7 @@ function TransactionAmount(props: TransactionAmountProps): VNode { > <ExtraLargeText> {sign} - {Amounts.stringifyValue(props.amount)} + {Amounts.stringifyValue(props.amount, 2)} </ExtraLargeText> {props.pending && ( <div> diff --git a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx index 3080a866e..004327c94 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.stories.tsx @@ -78,6 +78,7 @@ const exampleData = { summary: "the summary", fulfillmentMessage: "", }, + refunds: [], refundPending: undefined, totalRefundEffective: "USD:0", totalRefundRaw: "USD:0", diff --git a/packages/taler-wallet-webextension/src/wallet/History.tsx b/packages/taler-wallet-webextension/src/wallet/History.tsx index 4a435d0cf..59f245522 100644 --- a/packages/taler-wallet-webextension/src/wallet/History.tsx +++ b/packages/taler-wallet-webextension/src/wallet/History.tsx @@ -193,7 +193,7 @@ export function HistoryView({ margin: 8, }} > - {Amounts.stringifyValue(currencyAmount)} + {Amounts.stringifyValue(currencyAmount, 2)} </CenteredBoldText> )} </div> diff --git a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx index 587e24e98..895c301c2 100644 --- a/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/ReserveCreated.stories.tsx @@ -45,7 +45,7 @@ export const TalerBank = createExample(TestedComponent, { export const IBAN = createExample(TestedComponent, { reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", paytoURI: parsePaytoUri( - "payto://iban/ASDQWEASDZXCASDQWE?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", + "payto://iban/ES8877998399652238?amount=COL%3A1&message=Taler+Withdrawal+A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", ), amount: { currency: "USD", diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx index 493cdd1d7..83848d005 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.stories.tsx @@ -93,6 +93,7 @@ const exampleData = { // address_lines: [""], // }, }, + refunds: [], refundPending: undefined, totalRefundEffective: "KUDOS:0", totalRefundRaw: "KUDOS:0", @@ -199,7 +200,7 @@ export const WithdrawPendingManual = createExample(TestedComponent, () => ({ ...exampleData.withdraw, withdrawalDetails: { type: WithdrawalType.ManualTransfer, - exchangePaytoUris: ["payto://iban/asdasdasd"], + exchangePaytoUris: ["payto://iban/ES8877998399652238"], reservePub: "A05AJGMFNSK4Q62NXR2FKNDB1J4EXTYQTE7VA4M9GZQ4TR06YBNG", } as WithdrawalDetails, pending: true, @@ -254,6 +255,14 @@ export const PaymentWithRefund = createExample(TestedComponent, { amountRaw: "KUDOS:12", totalRefundEffective: "KUDOS:1", totalRefundRaw: "KUDOS:1", + refunds: [ + { + transactionId: "1123123", + amountRaw: "KUDOS:1", + amountEffective: "KUDOS:1", + timestamp: TalerProtocolTimestamp.fromSeconds(1546546544), + }, + ], }, }); @@ -410,6 +419,25 @@ export const PaymentWithLongSummary = createExample(TestedComponent, { export const Deposit = createExample(TestedComponent, { transaction: exampleData.deposit, }); +export const DepositTalerBank = createExample(TestedComponent, { + transaction: { + ...exampleData.deposit, + targetPaytoUri: "payto://x-taler-bank/bank.demo.taler.net/Exchange", + }, +}); +export const DepositBitcoin = createExample(TestedComponent, { + transaction: { + ...exampleData.deposit, + targetPaytoUri: + "payto://bitcoin/bcrt1q6ps8qs6v8tkqrnru4xqqqa6rfwcx5ufpdfqht4?amount=BTC:0.1&subject=0ZSX8SH0M30KHX8K3Y1DAMVGDQV82XEF9DG1HC4QMQ3QWYT4AF00", + }, +}); +export const DepositIBAN = createExample(TestedComponent, { + transaction: { + ...exampleData.deposit, + targetPaytoUri: "payto://iban/ES8877998399652238", + }, +}); export const DepositError = createExample(TestedComponent, { transaction: { diff --git a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx index 9ccb353a9..8165953ab 100644 --- a/packages/taler-wallet-webextension/src/wallet/Transaction.tsx +++ b/packages/taler-wallet-webextension/src/wallet/Transaction.tsx @@ -22,6 +22,8 @@ import { NotificationType, parsePaytoUri, parsePayUri, + PaytoUri, + stringifyPaytoUri, TalerProtocolTimestamp, Transaction, TransactionDeposit, @@ -50,6 +52,7 @@ import { ButtonDestructive, ButtonPrimary, CenteredDialog, + HistoryRow, InfoBox, ListOfProducts, Overlay, @@ -83,7 +86,7 @@ async function getTransaction(tid: string): Promise<Transaction> { export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { const { i18n } = useTranslationContext(); - const state = useAsyncAsHook(() => getTransaction(tid)); + const state = useAsyncAsHook(() => getTransaction(tid), [tid]); useEffect(() => { wxApi.onUpdateNotification([NotificationType.WithdrawGroupFinished], () => { @@ -119,6 +122,7 @@ export function TransactionPage({ tid, goToWalletHistory }: Props): VNode { onRetry={() => wxApi.retryTransaction(tid).then(() => goToWalletHistory(currency)) } + onRefund={(id) => wxApi.applyRefundFromPurchaseId(id)} onBack={() => goToWalletHistory(currency)} /> ); @@ -128,6 +132,7 @@ export interface WalletTransactionProps { transaction: Transaction; onDelete: () => void; onRetry: () => void; + onRefund: (id: string) => void; onBack: () => void; } @@ -143,7 +148,7 @@ export function TransactionView({ transaction, onDelete, onRetry, - onBack, + onRefund, }: WalletTransactionProps): VNode { const [confirmBeforeForget, setConfirmBeforeForget] = useState(false); @@ -334,6 +339,40 @@ export function TransactionView({ )} </Header> <br /> + {transaction.refunds.length > 0 ? ( + <Part + title={<i18n.Translate>Refunds</i18n.Translate>} + text={ + <table> + {transaction.refunds.map((r, i) => { + return ( + <tr key={i}> + <td> + {<Amount value={r.amountEffective} />}{" "} + <a + href={Pages.balance_transaction.replace( + ":tid", + r.transactionId, + )} + > + was refunded + </a>{" "} + on{" "} + { + <Time + timestamp={AbsoluteTime.fromTimestamp(r.timestamp)} + format="dd MMMM yyyy" + /> + } + </td> + </tr> + ); + })} + </table> + } + kind="neutral" + /> + ) : undefined} {pendingRefund !== undefined && Amounts.isNonZero(pendingRefund) && ( <InfoBox> <i18n.Translate> @@ -348,7 +387,7 @@ export function TransactionView({ <div> <div /> <div> - <ButtonPrimary> + <ButtonPrimary onClick={() => onRefund(transaction.proposalId)}> <i18n.Translate>Accept</i18n.Translate> </ButtonPrimary> </div> @@ -385,9 +424,9 @@ export function TransactionView({ total={total} kind="negative" > - {transaction.targetPaytoUri} + {!payto ? transaction.targetPaytoUri : <NicePayto payto={payto} />} </Header> - {payto && <PartPayto big payto={payto} kind="neutral" />} + {payto && <PartPayto payto={payto} kind="neutral" />} <Part title={<i18n.Translate>Details</i18n.Translate>} text={<DepositDetails transaction={transaction} />} @@ -669,7 +708,7 @@ function PurchaseDetails({ <tr> <td>Refunded</td> <td> - <Amount value={transaction.totalRefundEffective} /> + <Amount value={transaction.totalRefundRaw} /> </td> </tr> )} @@ -988,3 +1027,30 @@ function Header({ </div> ); } + +function NicePayto({ payto }: { payto: PaytoUri }): VNode { + if (payto.isKnown) { + switch (payto.targetType) { + case "bitcoin": { + return <div>{payto.targetPath.substring(0, 20)}...</div>; + } + case "x-taler-bank": { + const url = new URL("/", `https://${payto.host}`); + return ( + <Fragment> + <div>{payto.account}</div> + <SmallLightText> + <a href={url.href} target="_bank" rel="noreferrer"> + {url.toString()} + </a> + </SmallLightText> + </Fragment> + ); + } + case "iban": { + return <div>{payto.targetPath.substring(0, 20)}</div>; + } + } + } + return <Fragment>{stringifyPaytoUri(payto)}</Fragment>; +} diff --git a/packages/taler-wallet-webextension/src/wxApi.ts b/packages/taler-wallet-webextension/src/wxApi.ts index dd4eb2cf4..63840017b 100644 --- a/packages/taler-wallet-webextension/src/wxApi.ts +++ b/packages/taler-wallet-webextension/src/wxApi.ts @@ -312,6 +312,15 @@ export function applyRefund( } /** + * Do refund for purchase. + */ +export function applyRefundFromPurchaseId( + purchaseId: string, +): Promise<ApplyRefundResponse> { + return callBackend("applyRefundFromPurchaseId", { purchaseId }); +} + +/** * Get details about a pay operation. */ export function preparePay(talerPayUri: string): Promise<PreparePayResult> { |