fix performance and UI issues with tipping
This commit is contained in:
parent
97f6e68ce3
commit
d9683861f9
29
src/query.ts
29
src/query.ts
@ -697,6 +697,31 @@ export class QueryRoot {
|
|||||||
return this;
|
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> {
|
putWithResult<T>(store: Store<T>, val: T): Promise<IDBValidKey> {
|
||||||
this.checkFinished();
|
this.checkFinished();
|
||||||
@ -892,8 +917,12 @@ export class QueryRoot {
|
|||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
tx.onabort = () => {
|
tx.onabort = () => {
|
||||||
|
console.warn(`aborted ${mode} transaction on stores [${[... this.stores]}]`);
|
||||||
reject(Error("transaction aborted"));
|
reject(Error("transaction aborted"));
|
||||||
};
|
};
|
||||||
|
tx.onerror = (e) => {
|
||||||
|
console.warn(`error in transaction`, (e.target as any).error);
|
||||||
|
};
|
||||||
for (const w of this.work) {
|
for (const w of this.work) {
|
||||||
w(tx);
|
w(tx);
|
||||||
}
|
}
|
||||||
|
@ -316,6 +316,7 @@ export class Wallet {
|
|||||||
private timerGroup: TimerGroup;
|
private timerGroup: TimerGroup;
|
||||||
private speculativePayData: SpeculativePayData | undefined;
|
private speculativePayData: SpeculativePayData | undefined;
|
||||||
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
|
private cachedNextUrl: { [fulfillmentUrl: string]: NextUrlResult } = {};
|
||||||
|
private activeTipOperations: { [s: string]: Promise<TipRecord> } = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set of identifiers for running operations.
|
* Set of identifiers for running operations.
|
||||||
@ -2744,20 +2745,34 @@ export class Wallet {
|
|||||||
return feeAcc;
|
return feeAcc;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround for merchant bug (#5258)
|
|
||||||
*/
|
|
||||||
private tipPickupWorkaround: { [tipId: string]: boolean } = {};
|
|
||||||
|
|
||||||
async processTip(tipToken: TipToken): Promise<TipRecord> {
|
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);
|
console.log("got tip token", tipToken);
|
||||||
|
|
||||||
|
const merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||||
|
|
||||||
const deadlineSec = getTalerStampSec(tipToken.expiration);
|
const deadlineSec = getTalerStampSec(tipToken.expiration);
|
||||||
if (!deadlineSec) {
|
if (!deadlineSec) {
|
||||||
throw Error("tipping failed (invalid expiration)");
|
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]);
|
let tipRecord = await this.q().get(Stores.tips, [tipToken.tip_id, merchantDomain]);
|
||||||
|
|
||||||
if (tipRecord && tipRecord.pickedUp) {
|
if (tipRecord && tipRecord.pickedUp) {
|
||||||
@ -2783,21 +2798,16 @@ export class Wallet {
|
|||||||
tipId: tipToken.tip_id,
|
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
|
// Planchets in the form that the merchant expects
|
||||||
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
|
const planchetsDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
|
||||||
coin_ev: p.coinEv,
|
coin_ev: p.coinEv,
|
||||||
denom_pub_hash: p.denomPubHash,
|
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 {
|
try {
|
||||||
const config = {
|
const config = {
|
||||||
validateStatus: (s: number) => s === 200,
|
validateStatus: (s: number) => s === 200,
|
||||||
@ -2809,8 +2819,6 @@ export class Wallet {
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tipPickupWorkaround[tipToken.tip_id] = true;
|
|
||||||
|
|
||||||
const response = TipResponse.checked(merchantResp.data);
|
const response = TipResponse.checked(merchantResp.data);
|
||||||
|
|
||||||
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
|
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
|
||||||
@ -2880,11 +2888,20 @@ export class Wallet {
|
|||||||
|
|
||||||
|
|
||||||
async getTipStatus(tipToken: TipToken): Promise<TipStatus> {
|
async getTipStatus(tipToken: TipToken): Promise<TipStatus> {
|
||||||
const tipRecord = await this.processTip(tipToken);
|
const tipId = tipToken.tip_id;
|
||||||
const rci = await this.getReserveCreationInfo(tipRecord.exchangeUrl, tipRecord.amount);
|
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 = {
|
const tipStatus: TipStatus = {
|
||||||
rci,
|
accepted: !!tipRecord && tipRecord.accepted,
|
||||||
tip: tipRecord,
|
amount,
|
||||||
|
exchangeUrl,
|
||||||
|
merchantDomain,
|
||||||
|
nextUrl,
|
||||||
|
tipRecord,
|
||||||
};
|
};
|
||||||
return tipStatus;
|
return tipStatus;
|
||||||
}
|
}
|
||||||
|
@ -436,8 +436,12 @@ export interface CoinWithDenom {
|
|||||||
* Status of processing a tip.
|
* Status of processing a tip.
|
||||||
*/
|
*/
|
||||||
export interface TipStatus {
|
export interface TipStatus {
|
||||||
tip: TipRecord;
|
accepted: boolean;
|
||||||
rci?: ReserveCreationInfo;
|
amount: AmountJson;
|
||||||
|
nextUrl: string;
|
||||||
|
merchantDomain: string;
|
||||||
|
exchangeUrl: string;
|
||||||
|
tipRecord?: TipRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -260,7 +260,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log("payResult", payResult);
|
console.log("payResult", payResult);
|
||||||
document.location.href = payResult.nextUrl;
|
document.location.replace(payResult.nextUrl);
|
||||||
this.setState({ holdCheck: true });
|
this.setState({ holdCheck: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@ import * as i18n from "../../i18n";
|
|||||||
import {
|
import {
|
||||||
acceptTip,
|
acceptTip,
|
||||||
getTipStatus,
|
getTipStatus,
|
||||||
|
getReserveCreationInfo,
|
||||||
} from "../wxApi";
|
} from "../wxApi";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -40,7 +41,7 @@ import {
|
|||||||
|
|
||||||
import * as Amounts from "../../amounts";
|
import * as Amounts from "../../amounts";
|
||||||
import { TipToken } from "../../talerTypes";
|
import { TipToken } from "../../talerTypes";
|
||||||
import { TipStatus } from "../../walletTypes";
|
import { ReserveCreationInfo, TipStatus } from "../../walletTypes";
|
||||||
|
|
||||||
interface TipDisplayProps {
|
interface TipDisplayProps {
|
||||||
tipToken: TipToken;
|
tipToken: TipToken;
|
||||||
@ -48,18 +49,22 @@ interface TipDisplayProps {
|
|||||||
|
|
||||||
interface TipDisplayState {
|
interface TipDisplayState {
|
||||||
tipStatus?: TipStatus;
|
tipStatus?: TipStatus;
|
||||||
|
rci?: ReserveCreationInfo;
|
||||||
working: boolean;
|
working: boolean;
|
||||||
|
discarded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
||||||
constructor(props: TipDisplayProps) {
|
constructor(props: TipDisplayProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { working: false };
|
this.state = { working: false, discarded: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const tipStatus = await getTipStatus(this.props.tipToken);
|
const tipStatus = await getTipStatus(this.props.tipToken);
|
||||||
this.setState({ tipStatus });
|
this.setState({ tipStatus });
|
||||||
|
const rci = await getReserveCreationInfo(tipStatus.exchangeUrl, tipStatus.amount);
|
||||||
|
this.setState({ rci });
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -74,8 +79,8 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
|||||||
this.update();
|
this.update();
|
||||||
}
|
}
|
||||||
|
|
||||||
renderExchangeInfo(ts: TipStatus) {
|
renderExchangeInfo() {
|
||||||
const rci = ts.rci;
|
const rci = this.state.rci;
|
||||||
if (!rci) {
|
if (!rci) {
|
||||||
return <p>Waiting for info about exchange ...</p>;
|
return <p>Waiting for info about exchange ...</p>;
|
||||||
}
|
}
|
||||||
@ -99,22 +104,8 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
|||||||
acceptTip(this.props.tipToken);
|
acceptTip(this.props.tipToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderButtons() {
|
discard() {
|
||||||
return (
|
this.setState({ discarded: true });
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
@ -122,16 +113,52 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
|
|||||||
if (!ts) {
|
if (!ts) {
|
||||||
return <p>Processing ...</p>;
|
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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2>Tip Received!</h2>
|
<h2>Tip Received!</h2>
|
||||||
<p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <span> </span>
|
<p>You received a tip of <strong>{renderAmount(ts.amount)}</strong> from <span> </span>
|
||||||
<strong>{ts.tip.merchantDomain}</strong>.</p>
|
<strong>{ts.merchantDomain}</strong>.</p>
|
||||||
{ts.tip.accepted
|
{
|
||||||
? <p>You've accepted this tip! <a href={ts.tip.nextUrl}>Go back to merchant</a></p>
|
this.state.discarded
|
||||||
: this.renderButtons()
|
? renderDiscarded()
|
||||||
|
: ts.accepted
|
||||||
|
? renderAccepted()
|
||||||
|
: renderButtons()
|
||||||
}
|
}
|
||||||
{this.renderExchangeInfo(ts)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user