fix performance and UI issues with tipping

This commit is contained in:
Florian Dold 2018-02-01 07:19:03 +01:00
parent 97f6e68ce3
commit d9683861f9
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 126 additions and 49 deletions

View File

@ -697,6 +697,31 @@ export class QueryRoot {
return this;
}
/**
* Put an object into a store or return an existing record.
*/
putOrGetExisting<T>(store: Store<T>, val: T, key: IDBValidKey): Promise<T> {
this.checkFinished();
const {resolve, promise} = openPromise();
const doPutOrGet = (tx: IDBTransaction) => {
const objstore = tx.objectStore(store.name);
const req = objstore.get(key);
req.onsuccess = () => {
if (req.result !== undefined) {
resolve(req.result);
} else {
const req2 = objstore.add(val);
req2.onsuccess = () => {
resolve(val);
};
}
};
};
this.scheduleFinish();
this.addWork(doPutOrGet, store.name, true);
return promise;
}
putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
this.checkFinished();
@ -892,8 +917,12 @@ export class QueryRoot {
resolve();
};
tx.onabort = () => {
console.warn(`aborted ${mode} transaction on stores [${[... this.stores]}]`);
reject(Error("transaction aborted"));
};
tx.onerror = (e) => {
console.warn(`error in transaction`, (e.target as any).error);
};
for (const w of this.work) {
w(tx);
}

View File

@ -316,6 +316,7 @@ export class Wallet {
private timerGroup: TimerGroup;
private speculativePayData: SpeculativePayData | undefined;
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
private activeTipOperations: { [s: string]: Promise<TipRecord> } = {};
/**
* Set of identifiers for running operations.
@ -2744,20 +2745,34 @@ export class Wallet {
return feeAcc;
}
/**
* Workaround for merchant bug (#5258)
*/
private tipPickupWorkaround: { [tipId: string]: boolean } = {};
async processTip(tipToken: TipToken): Promise<TipRecord> {
const merchantDomain = new URI(tipToken.pickup_url).origin();
const key = tipToken.tip_id + merchantDomain;
if (this.activeTipOperations[key]) {
return this.activeTipOperations[key];
}
const p = this.processTipImpl(tipToken);
this.activeTipOperations[key] = p
try {
return await p;
} finally {
delete this.activeTipOperations[key];
}
}
private async processTipImpl(tipToken: TipToken): Promise<TipRecord> {
console.log("got tip token", tipToken);
const merchantDomain = new URI(tipToken.pickup_url).origin();
const deadlineSec = getTalerStampSec(tipToken.expiration);
if (!deadlineSec) {
throw Error("tipping failed (invalid expiration)");
}
const merchantDomain = new URI(tipToken.pickup_url).origin();
let tipRecord = await this.q().get(Stores.tips, [tipToken.tip_id, merchantDomain]);
if (tipRecord && tipRecord.pickedUp) {
@ -2783,21 +2798,16 @@ export class Wallet {
tipId: tipToken.tip_id,
};
let merchantResp;
tipRecord = await this.q().putOrGetExisting(Stores.tips, tipRecord, [tipRecord.tipId, merchantDomain]);
// Planchets in the form that the merchant expects
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
coin_ev: p.coinEv,
denom_pub_hash: p.denomPubHash,
}));
let merchantResp;
await this.q().put(Stores.tips, tipRecord).finish();
if (this.tipPickupWorkaround[tipRecord.tipId]) {
// Be careful to not accidentally download twice (#5258)
return tipRecord;
}
try {
const config = {
validateStatus: (s: number) => s === 200,
@ -2809,8 +2819,6 @@ export class Wallet {
throw e;
}
this.tipPickupWorkaround[tipToken.tip_id] = true;
const response = TipResponse.checked(merchantResp.data);
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
@ -2880,11 +2888,20 @@ export class Wallet {
async getTipStatus(tipToken: TipToken): Promise<TipStatus> {
const tipRecord = await this.processTip(tipToken);
const rci = await this.getReserveCreationInfo(tipRecord.exchangeUrl, tipRecord.amount);
const tipId = tipToken.tip_id;
const merchantDomain = new URI(tipToken.pickup_url).origin();
let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
const amount = Amounts.parseOrThrow(tipToken.amount);
const exchangeUrl = tipToken.exchange_url;
this.processTip(tipToken);
const nextUrl = tipToken.next_url;
const tipStatus: TipStatus = {
rci,
tip: tipRecord,
accepted: !!tipRecord && tipRecord.accepted,
amount,
exchangeUrl,
merchantDomain,
nextUrl,
tipRecord,
};
return tipStatus;
}

View File

@ -436,8 +436,12 @@ export interface CoinWithDenom {
* Status of processing a tip.
*/
export interface TipStatus {
tip: TipRecord;
rci?: ReserveCreationInfo;
accepted: boolean;
amount: AmountJson;
nextUrl: string;
merchantDomain: string;
exchangeUrl: string;
tipRecord?: TipRecord;
}

View File

@ -260,7 +260,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
return;
}
console.log("payResult", payResult);
document.location.href = payResult.nextUrl;
document.location.replace(payResult.nextUrl);
this.setState({ holdCheck: true });
}

View File

@ -31,6 +31,7 @@ import * as i18n from "../../i18n";
import {
acceptTip,
getTipStatus,
getReserveCreationInfo,
} from "../wxApi";
import {
@ -40,7 +41,7 @@ import {
import * as Amounts from "../../amounts";
import { TipToken } from "../../talerTypes";
import { TipStatus } from "../../walletTypes";
import { ReserveCreationInfo, TipStatus } from "../../walletTypes";
interface TipDisplayProps {
tipToken: TipToken;
@ -48,18 +49,22 @@ interface TipDisplayProps {
interface TipDisplayState {
tipStatus?: TipStatus;
rci?: ReserveCreationInfo;
working: boolean;
discarded: boolean;
}
class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
constructor(props: TipDisplayProps) {
super(props);
this.state = { working: false };
this.state = { working: false, discarded: false };
}
async update() {
const tipStatus = await getTipStatus(this.props.tipToken);
this.setState({ tipStatus });
const rci = await getReserveCreationInfo(tipStatus.exchangeUrl, tipStatus.amount);
this.setState({ rci });
}
componentDidMount() {
@ -74,8 +79,8 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
this.update();
}
renderExchangeInfo(ts: TipStatus) {
const rci = ts.rci;
renderExchangeInfo() {
const rci = this.state.rci;
if (!rci) {
return <p>Waiting for info about exchange ...</p>;
}
@ -99,22 +104,8 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
acceptTip(this.props.tipToken);
}
renderButtons() {
return (
<form className="pure-form">
<button
className="pure-button pure-button-primary"
type="button"
onClick={() => this.accept()}>
{ this.state.working
? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
: null }
Accept tip
</button>
{" "}
<button className="pure-button" type="button" onClick={() => { window.close(); }}>Discard tip</button>
</form>
);
discard() {
this.setState({ discarded: true });
}
render(): JSX.Element {
@ -122,16 +113,52 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
if (!ts) {
return <p>Processing ...</p>;
}
const renderAccepted = () => (
<>
<p>You've accepted this tip! <a href={ts.nextUrl}>Go back to merchant</a></p>
{this.renderExchangeInfo()}
</>
);
const renderButtons = () => (
<>
<form className="pure-form">
<button
className="pure-button pure-button-primary"
type="button"
disabled={!(this.state.rci && this.state.tipStatus)}
onClick={() => this.accept()}>
{ this.state.working
? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
: null }
Accept tip
</button>
{" "}
<button className="pure-button" type="button" onClick={() => this.discard()}>
Discard tip
</button>
</form>
{ this.renderExchangeInfo() }
</>
);
const renderDiscarded = () => (
<p>You've discarded this tip. <a href={ts.nextUrl}>Go back to merchant.</a></p>
);
return (
<div>
<h2>Tip Received!</h2>
<p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <span> </span>
<strong>{ts.tip.merchantDomain}</strong>.</p>
{ts.tip.accepted
? <p>You've accepted this tip! <a href={ts.tip.nextUrl}>Go back to merchant</a></p>
: this.renderButtons()
<p>You received a tip of <strong>{renderAmount(ts.amount)}</strong> from <span> </span>
<strong>{ts.merchantDomain}</strong>.</p>
{
this.state.discarded
? renderDiscarded()
: ts.accepted
? renderAccepted()
: renderButtons()
}
{this.renderExchangeInfo(ts)}
</div>
);
}