diff options
-rw-r--r-- | src/query.ts | 29 | ||||
-rw-r--r-- | src/wallet.ts | 57 | ||||
-rw-r--r-- | src/walletTypes.ts | 8 | ||||
-rw-r--r-- | src/webex/pages/confirm-contract.tsx | 2 | ||||
-rw-r--r-- | src/webex/pages/tip.tsx | 65 |
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> ); } |