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>      );    } | 
