cli refunds

This commit is contained in:
Florian Dold 2019-08-31 11:49:36 +02:00
parent 5ec344290e
commit 5a7269b20d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 121 additions and 40 deletions

View File

@ -179,6 +179,20 @@ program
wallet.stop(); wallet.stop();
}); });
program
.command("refund-uri <refund-uri>")
.action(async (refundUri, cmdObj) => {
applyVerbose(program.verbose);
console.log("getting refund", refundUri);
const wallet = await getDefaultNodeWallet({
persistentStoragePath: walletDbPath,
});
await wallet.applyRefund(refundUri);
wallet.stop();
});
program program
.command("pay-uri <pay-uri") .command("pay-uri <pay-uri")
.option("-y, --yes", "automatically answer yes to prompts") .option("-y, --yes", "automatically answer yes to prompts")

View File

@ -26,6 +26,10 @@ export interface WithdrawUriResult {
statusUrl: string; statusUrl: string;
} }
export interface RefundUriResult {
refundUrl: string;
}
export interface TipUriResult { export interface TipUriResult {
tipPickupUrl: string; tipPickupUrl: string;
tipId: string; tipId: string;
@ -155,3 +159,52 @@ export function parseTipUri(s: string): TipUriResult | undefined {
merchantOrigin: new URI(tipPickupUrl).origin(), merchantOrigin: new URI(tipPickupUrl).origin(),
}; };
} }
export function parseRefundUri(s: string): RefundUriResult | undefined {
const parsedUri = new URI(s);
if (parsedUri.scheme() != "taler") {
return undefined;
}
if (parsedUri.authority() != "refund") {
return undefined;
}
let [
_,
host,
maybePath,
maybeInstance,
orderId,
] = parsedUri.path().split("/");
if (!host) {
return undefined;
}
if (!maybePath) {
return undefined;
}
if (!orderId) {
return undefined;
}
if (maybePath === "-") {
maybePath = "public/refund";
} else {
maybePath = decodeURIComponent(maybePath);
}
if (maybeInstance === "-") {
maybeInstance = "default";
}
const refundUrl = new URI(
"https://" + host + "/" + decodeURIComponent(maybePath),
)
.addQuery({ instance: maybeInstance, order_id: orderId })
.href();
return {
refundUrl,
};
}

View File

@ -109,7 +109,7 @@ import {
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
} from "./walletTypes"; } from "./walletTypes";
import { openPromise } from "./promiseUtils"; import { openPromise } from "./promiseUtils";
import { parsePayUri, parseWithdrawUri, parseTipUri } from "./taleruri"; import { parsePayUri, parseWithdrawUri, parseTipUri, parseRefundUri } from "./taleruri";
interface SpeculativePayData { interface SpeculativePayData {
payCoinInfo: PayCoinInfo; payCoinInfo: PayCoinInfo;
@ -3109,7 +3109,7 @@ export class Wallet {
} }
} }
async acceptRefundResponse( private async acceptRefundResponse(
refundResponse: MerchantRefundResponse, refundResponse: MerchantRefundResponse,
): Promise<string> { ): Promise<string> {
const refundPermissions = refundResponse.refund_permissions; const refundPermissions = refundResponse.refund_permissions;
@ -3149,8 +3149,7 @@ export class Wallet {
.finish(); .finish();
this.notifier.notify(); this.notifier.notify();
// Start submitting it but don't wait for it here. await this.submitRefunds(hc);
this.submitRefunds(hc);
return hc; return hc;
} }
@ -3159,7 +3158,15 @@ export class Wallet {
* Accept a refund, return the contract hash for the contract * Accept a refund, return the contract hash for the contract
* that was involved in the refund. * that was involved in the refund.
*/ */
async acceptRefund(refundUrl: string): Promise<string> { async applyRefund(talerRefundUri: string): Promise<string> {
const parseResult = parseRefundUri(talerRefundUri);
if (!parseResult) {
throw Error("invalid refund URI");
}
const refundUrl = parseResult.refundUrl;
Wallet.enableTracing && console.log("processing refund"); Wallet.enableTracing && console.log("processing refund");
let resp; let resp;
try { try {

View File

@ -30,7 +30,7 @@ import { ExchangeRecord, ProposalDownloadRecord } from "../../dbTypes";
import { ContractTerms } from "../../talerTypes"; import { ContractTerms } from "../../talerTypes";
import { CheckPayResult, PreparePayResult } from "../../walletTypes"; import { CheckPayResult, PreparePayResult } from "../../walletTypes";
import { renderAmount } from "../renderHtml"; import { renderAmount, ProgressButton } from "../renderHtml";
import * as wxApi from "../wxApi"; import * as wxApi from "../wxApi";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
@ -44,6 +44,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>(); const [payStatus, setPayStatus] = useState<PreparePayResult | undefined>();
const [payErrMsg, setPayErrMsg] = useState<string | undefined>(""); const [payErrMsg, setPayErrMsg] = useState<string | undefined>("");
const [numTries, setNumTries] = useState(0); const [numTries, setNumTries] = useState(0);
const [loading, setLoading] = useState(false);
let totalFees: Amounts.AmountJson | undefined = undefined; let totalFees: Amounts.AmountJson | undefined = undefined;
useEffect(() => { useEffect(() => {
@ -99,6 +100,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
const doPayment = async () => { const doPayment = async () => {
setNumTries(numTries + 1); setNumTries(numTries + 1);
try { try {
setLoading(true);
const res = await wxApi.confirmPay(payStatus!.proposalId!, undefined); const res = await wxApi.confirmPay(payStatus!.proposalId!, undefined);
document.location.href = res.nextUrl; document.location.href = res.nextUrl;
} catch (e) { } catch (e) {
@ -140,12 +142,11 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
</div> </div>
) : ( ) : (
<div> <div>
<button <ProgressButton
className="pure-button button-success" loading={loading}
onClick={() => doPayment()} onClick={() => doPayment()}>
>
{i18n.str`Confirm payment`} {i18n.str`Confirm payment`}
</button> </ProgressButton>
</div> </div>
)} )}
</div> </div>

View File

@ -188,8 +188,12 @@ async function main() {
return; return;
} }
const contractTermsHash = query.contractTermsHash; const talerRefundUri = query.talerRefundUri;
const refundUrl = query.refundUrl; if (!talerRefundUri) {
console.error("taler refund URI requred");
return;
}
ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} refundUrl={refundUrl} />, container); ReactDOM.render(<RefundStatusView contractTermsHash={contractTermsHash} refundUrl={refundUrl} />, container);
} }

View File

@ -29,35 +29,12 @@ import * as i18n from "../../i18n";
import { acceptTip, getReserveCreationInfo, getTipStatus } from "../wxApi"; import { acceptTip, getReserveCreationInfo, getTipStatus } from "../wxApi";
import { WithdrawDetailView, renderAmount } from "../renderHtml"; import { WithdrawDetailView, renderAmount, ProgressButton } from "../renderHtml";
import * as Amounts from "../../amounts"; import * as Amounts from "../../amounts";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { TipStatus } from "../../walletTypes"; import { TipStatus } from "../../walletTypes";
interface LoadingButtonProps {
loading: boolean;
}
function LoadingButton(
props:
& React.PropsWithChildren<LoadingButtonProps>
& React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>,
) {
return (
<button
className="pure-button pure-button-primary"
type="button"
{...props}
>
{props.loading ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /></span> : null}
{props.children}
</button>
);
}
function TipDisplay(props: { talerTipUri: string }) { function TipDisplay(props: { talerTipUri: string }) {
const [tipStatus, setTipStatus] = useState<TipStatus | undefined>(undefined); const [tipStatus, setTipStatus] = useState<TipStatus | undefined>(undefined);
@ -110,9 +87,9 @@ function TipDisplay(props: { talerTipUri: string }) {
operation. operation.
</p> </p>
<form className="pure-form"> <form className="pure-form">
<LoadingButton loading={loading} onClick={() => accept()}> <ProgressButton loading={loading} onClick={() => accept()}>
AcceptTip AcceptTip
</LoadingButton> </ProgressButton>
{" "} {" "}
<button className="pure-button" type="button" onClick={() => discard()}> <button className="pure-button" type="button" onClick={() => discard()}>
Discard tip Discard tip

View File

@ -316,3 +316,28 @@ export class ExpanderText extends ImplicitStateComponent<ExpanderTextProps> {
} }
} }
export interface LoadingButtonProps {
loading: boolean;
}
export function ProgressButton(
props:
& React.PropsWithChildren<LoadingButtonProps>
& React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>,
) {
return (
<button
className="pure-button pure-button-primary"
type="button"
{...props}
>
{props.loading ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /></span> : null}
{" "}
{props.children}
</button>
);
}

View File

@ -292,7 +292,7 @@ function handleMessage(
case "get-full-refund-fees": case "get-full-refund-fees":
return needsWallet().getFullRefundFees(detail.refundPermissions); return needsWallet().getFullRefundFees(detail.refundPermissions);
case "accept-refund": case "accept-refund":
return needsWallet().acceptRefund(detail.refundUrl); return needsWallet().applyRefund(detail.refundUrl);
case "get-tip-status": { case "get-tip-status": {
return needsWallet().getTipStatus(detail.talerTipUri); return needsWallet().getTipStatus(detail.talerTipUri);
} }