aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-02-01 07:19:03 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-02-01 07:19:03 +0100
commitd9683861f98277e7121ff47d2bd223c2d0c2cd34 (patch)
treea7357a7ebe80675716b543f1d30f1f1cdf083efa
parent97f6e68ce3a515938228b9a4d3e41b5f4b25a015 (diff)
fix performance and UI issues with tipping
-rw-r--r--src/query.ts29
-rw-r--r--src/wallet.ts57
-rw-r--r--src/walletTypes.ts8
-rw-r--r--src/webex/pages/confirm-contract.tsx2
-rw-r--r--src/webex/pages/tip.tsx65
5 files changed, 119 insertions, 42 deletions
diff --git a/src/query.ts b/src/query.ts
index e45596c66..f21f82020 100644
--- a/src/query.ts
+++ b/src/query.ts
@@ -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);
}
diff --git a/src/wallet.ts b/src/wallet.ts
index c4308b8d1..95e7246fb 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -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;
}
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index edcf65830..562d12dfa 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -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;
}
diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx
index 78e90ee0e..b851bf1d2 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -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 });
}
diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx
index 578ae6aa4..f21bc0eaf 100644
--- a/src/webex/pages/tip.tsx
+++ b/src/webex/pages/tip.tsx
@@ -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,12 +104,30 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
acceptTip(this.props.tipToken);
}
- renderButtons() {
- return (
+ discard() {
+ this.setState({ discarded: true });
+ }
+
+ render(): JSX.Element {
+ const ts = this.state.tipStatus;
+ 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>
@@ -112,26 +135,30 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
Accept tip
</button>
{" "}
- <button className="pure-button" type="button" onClick={() => { window.close(); }}>Discard 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>
);
- }
- render(): JSX.Element {
- const ts = this.state.tipStatus;
- if (!ts) {
- return <p>Processing ...</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>
);
}