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();
});
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
.command("pay-uri <pay-uri")
.option("-y, --yes", "automatically answer yes to prompts")

View File

@ -26,6 +26,10 @@ export interface WithdrawUriResult {
statusUrl: string;
}
export interface RefundUriResult {
refundUrl: string;
}
export interface TipUriResult {
tipPickupUrl: string;
tipId: string;
@ -155,3 +159,52 @@ export function parseTipUri(s: string): TipUriResult | undefined {
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,
} from "./walletTypes";
import { openPromise } from "./promiseUtils";
import { parsePayUri, parseWithdrawUri, parseTipUri } from "./taleruri";
import { parsePayUri, parseWithdrawUri, parseTipUri, parseRefundUri } from "./taleruri";
interface SpeculativePayData {
payCoinInfo: PayCoinInfo;
@ -3109,7 +3109,7 @@ export class Wallet {
}
}
async acceptRefundResponse(
private async acceptRefundResponse(
refundResponse: MerchantRefundResponse,
): Promise<string> {
const refundPermissions = refundResponse.refund_permissions;
@ -3149,8 +3149,7 @@ export class Wallet {
.finish();
this.notifier.notify();
// Start submitting it but don't wait for it here.
this.submitRefunds(hc);
await this.submitRefunds(hc);
return hc;
}
@ -3159,7 +3158,15 @@ export class Wallet {
* Accept a refund, return the contract hash for the contract
* 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");
let resp;
try {

View File

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

View File

@ -188,8 +188,12 @@ async function main() {
return;
}
const contractTermsHash = query.contractTermsHash;
const refundUrl = query.refundUrl;
const talerRefundUri = query.talerRefundUri;
if (!talerRefundUri) {
console.error("taler refund URI requred");
return;
}
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 { WithdrawDetailView, renderAmount } from "../renderHtml";
import { WithdrawDetailView, renderAmount, ProgressButton } from "../renderHtml";
import * as Amounts from "../../amounts";
import { useState, useEffect } from "react";
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 }) {
const [tipStatus, setTipStatus] = useState<TipStatus | undefined>(undefined);
@ -110,9 +87,9 @@ function TipDisplay(props: { talerTipUri: string }) {
operation.
</p>
<form className="pure-form">
<LoadingButton loading={loading} onClick={() => accept()}>
<ProgressButton loading={loading} onClick={() => accept()}>
AcceptTip
</LoadingButton>
</ProgressButton>
{" "}
<button className="pure-button" type="button" onClick={() => discard()}>
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":
return needsWallet().getFullRefundFees(detail.refundPermissions);
case "accept-refund":
return needsWallet().acceptRefund(detail.refundUrl);
return needsWallet().applyRefund(detail.refundUrl);
case "get-tip-status": {
return needsWallet().getTipStatus(detail.talerTipUri);
}