diff options
| author | Florian Dold <florian.dold@gmail.com> | 2018-01-19 01:27:27 +0100 | 
|---|---|---|
| committer | Florian Dold <florian.dold@gmail.com> | 2018-01-19 01:27:27 +0100 | 
| commit | 1671d9a508b803af31762bcd9508e70eb40e7b48 (patch) | |
| tree | 24d79103d0661c9edafd1d6371692b726b2f0ef3 | |
| parent | 2f68e9e50e83c55ca46e9d4d72956d6525d0fa8c (diff) | |
refactor tipping, adjust to new redirect-based API
| -rw-r--r-- | src/dbTypes.ts | 5 | ||||
| -rw-r--r-- | src/i18n/de.po | 8 | ||||
| -rw-r--r-- | src/i18n/en-US.po | 8 | ||||
| -rw-r--r-- | src/i18n/fr.po | 8 | ||||
| -rw-r--r-- | src/i18n/it.po | 8 | ||||
| -rw-r--r-- | src/i18n/taler-wallet-webex.pot | 8 | ||||
| -rw-r--r-- | src/wallet.ts | 184 | ||||
| -rw-r--r-- | src/walletTypes.ts | 141 | ||||
| -rw-r--r-- | src/webex/messages.ts | 16 | ||||
| -rw-r--r-- | src/webex/pages/confirm-contract.tsx | 73 | ||||
| -rw-r--r-- | src/webex/pages/tip.tsx | 16 | ||||
| -rw-r--r-- | src/webex/wxApi.ts | 39 | ||||
| -rw-r--r-- | src/webex/wxBackend.ts | 67 | 
13 files changed, 191 insertions, 390 deletions
| diff --git a/src/dbTypes.ts b/src/dbTypes.ts index 609c85265..035c100a9 100644 --- a/src/dbTypes.ts +++ b/src/dbTypes.ts @@ -575,6 +575,11 @@ export interface TipRecord {    accepted: boolean;    /** +   * Have we picked up the tip record from the merchant already? +   */ +  pickedUp: boolean; + +  /**     * The tipped amount.     */    amount: AmountJson; diff --git a/src/i18n/de.po b/src/i18n/de.po index 39f1f56e6..1a003c17d 100644 --- a/src/i18n/de.po +++ b/src/i18n/de.po @@ -42,13 +42,13 @@ msgstr ""  msgid "Exchanges in the wallet:"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:188 +#: src/webex/pages/confirm-contract.tsx:200  #, c-format  msgid "You have insufficient funds of the requested currency in your wallet."  msgstr ""  #. tslint:disable-next-line:max-line-length -#: src/webex/pages/confirm-contract.tsx:190 +#: src/webex/pages/confirm-contract.tsx:202  #, c-format  msgid ""  "You do not have any funds from an exchange that is accepted by this " @@ -56,12 +56,12 @@ msgid ""  "wallet."  msgstr "" -#: src/webex/pages/confirm-contract.tsx:251 +#: src/webex/pages/confirm-contract.tsx:280  #, c-format  msgid "The merchant%1$s offers you to purchase:\n"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:272 +#: src/webex/pages/confirm-contract.tsx:301  #, fuzzy, c-format  msgid "Confirm payment"  msgstr "Bezahlung bestätigen" diff --git a/src/i18n/en-US.po b/src/i18n/en-US.po index 2fdb451db..3d3fd4332 100644 --- a/src/i18n/en-US.po +++ b/src/i18n/en-US.po @@ -42,13 +42,13 @@ msgstr ""  msgid "Exchanges in the wallet:"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:188 +#: src/webex/pages/confirm-contract.tsx:200  #, c-format  msgid "You have insufficient funds of the requested currency in your wallet."  msgstr ""  #. tslint:disable-next-line:max-line-length -#: src/webex/pages/confirm-contract.tsx:190 +#: src/webex/pages/confirm-contract.tsx:202  #, c-format  msgid ""  "You do not have any funds from an exchange that is accepted by this " @@ -56,12 +56,12 @@ msgid ""  "wallet."  msgstr "" -#: src/webex/pages/confirm-contract.tsx:251 +#: src/webex/pages/confirm-contract.tsx:280  #, c-format  msgid "The merchant%1$s offers you to purchase:\n"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:272 +#: src/webex/pages/confirm-contract.tsx:301  #, c-format  msgid "Confirm payment"  msgstr "" diff --git a/src/i18n/fr.po b/src/i18n/fr.po index 5d47a1f74..08f4a9d0c 100644 --- a/src/i18n/fr.po +++ b/src/i18n/fr.po @@ -42,13 +42,13 @@ msgstr ""  msgid "Exchanges in the wallet:"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:188 +#: src/webex/pages/confirm-contract.tsx:200  #, c-format  msgid "You have insufficient funds of the requested currency in your wallet."  msgstr ""  #. tslint:disable-next-line:max-line-length -#: src/webex/pages/confirm-contract.tsx:190 +#: src/webex/pages/confirm-contract.tsx:202  #, c-format  msgid ""  "You do not have any funds from an exchange that is accepted by this " @@ -56,12 +56,12 @@ msgid ""  "wallet."  msgstr "" -#: src/webex/pages/confirm-contract.tsx:251 +#: src/webex/pages/confirm-contract.tsx:280  #, c-format  msgid "The merchant%1$s offers you to purchase:\n"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:272 +#: src/webex/pages/confirm-contract.tsx:301  #, c-format  msgid "Confirm payment"  msgstr "" diff --git a/src/i18n/it.po b/src/i18n/it.po index 5d47a1f74..08f4a9d0c 100644 --- a/src/i18n/it.po +++ b/src/i18n/it.po @@ -42,13 +42,13 @@ msgstr ""  msgid "Exchanges in the wallet:"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:188 +#: src/webex/pages/confirm-contract.tsx:200  #, c-format  msgid "You have insufficient funds of the requested currency in your wallet."  msgstr ""  #. tslint:disable-next-line:max-line-length -#: src/webex/pages/confirm-contract.tsx:190 +#: src/webex/pages/confirm-contract.tsx:202  #, c-format  msgid ""  "You do not have any funds from an exchange that is accepted by this " @@ -56,12 +56,12 @@ msgid ""  "wallet."  msgstr "" -#: src/webex/pages/confirm-contract.tsx:251 +#: src/webex/pages/confirm-contract.tsx:280  #, c-format  msgid "The merchant%1$s offers you to purchase:\n"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:272 +#: src/webex/pages/confirm-contract.tsx:301  #, c-format  msgid "Confirm payment"  msgstr "" diff --git a/src/i18n/taler-wallet-webex.pot b/src/i18n/taler-wallet-webex.pot index 5d47a1f74..08f4a9d0c 100644 --- a/src/i18n/taler-wallet-webex.pot +++ b/src/i18n/taler-wallet-webex.pot @@ -42,13 +42,13 @@ msgstr ""  msgid "Exchanges in the wallet:"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:188 +#: src/webex/pages/confirm-contract.tsx:200  #, c-format  msgid "You have insufficient funds of the requested currency in your wallet."  msgstr ""  #. tslint:disable-next-line:max-line-length -#: src/webex/pages/confirm-contract.tsx:190 +#: src/webex/pages/confirm-contract.tsx:202  #, c-format  msgid ""  "You do not have any funds from an exchange that is accepted by this " @@ -56,12 +56,12 @@ msgid ""  "wallet."  msgstr "" -#: src/webex/pages/confirm-contract.tsx:251 +#: src/webex/pages/confirm-contract.tsx:280  #, c-format  msgid "The merchant%1$s offers you to purchase:\n"  msgstr "" -#: src/webex/pages/confirm-contract.tsx:272 +#: src/webex/pages/confirm-contract.tsx:301  #, c-format  msgid "Confirm payment"  msgstr "" diff --git a/src/wallet.ts b/src/wallet.ts index 7c2914926..9498fe820 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -99,7 +99,6 @@ import {    NextUrlResult,    Notifier,    PayCoinInfo, -  QueryPaymentResult,    ReserveCreationInfo,    ReturnCoinsRequest,    SenderWireInfos, @@ -652,8 +651,8 @@ export class Wallet {        contractTerms: proposal.contractTerms,        contractTermsHash: proposal.contractTermsHash,        finished: false, -      lastSessionSig: undefined,        lastSessionId: undefined, +      lastSessionSig: undefined,        merchantSig: proposal.merchantSig,        payReq,        refundsDone: {}, @@ -717,7 +716,11 @@ export class Wallet {      return id;    } -  private async submitPay(purchase: PurchaseRecord, sessionId: string | undefined): Promise<ConfirmPayResult> { +  async submitPay(contractTermsHash: string, sessionId: string | undefined): Promise<ConfirmPayResult> { +    const purchase = await this.q().get(Stores.purchases, contractTermsHash); +    if (!purchase) { +      throw Error("Purchase not found: " + contractTermsHash); +    }      let resp;      const payReq = { ...purchase.payReq, session_id: sessionId };      try { @@ -764,7 +767,7 @@ export class Wallet {      let purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);      if (purchase) { -      return this.submitPay(purchase, sessionId); +      return this.submitPay(purchase.contractTermsHash, sessionId);      }      const res = await this.getCoinsForPayment({ @@ -796,7 +799,7 @@ export class Wallet {        purchase = await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);      } -    return this.submitPay(purchase, sessionId); +    return this.submitPay(purchase.contractTermsHash, sessionId);    } @@ -885,52 +888,17 @@ export class Wallet {     * Retrieve information required to pay for a contract, where the     * contract is identified via the fulfillment url.     */ -  async queryPaymentByFulfillmentUrl(url: string): Promise<QueryPaymentResult> { +  async queryPaymentByFulfillmentUrl(url: string): Promise<PurchaseRecord | undefined> {      console.log("query for payment", url);      const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url);      if (!t) {        console.log("query for payment failed"); -      return { -        found: false, -      }; -    } -    console.log("query for payment succeeded:", t); -    return { -      contractTerms: t.contractTerms, -      contractTermsHash: t.contractTermsHash, -      found: true, -      lastSessionId: t.lastSessionId, -      lastSessionSig: t.lastSessionSig, -      payReq: t.payReq, -    }; -  } - -  /** -   * Retrieve information required to pay for a contract, where the -   * contract is identified via the contract terms hash. -   */ -  async queryPaymentByContractTermsHash(contractTermsHash: string): Promise<QueryPaymentResult> { -    console.log("query for payment", contractTermsHash); - -    const t = await this.q().get(Stores.purchases, contractTermsHash); - -    if (!t) { -      console.log("query for payment failed"); -      return { -        found: false, -      }; +      return undefined;      }      console.log("query for payment succeeded:", t); -    return { -      contractTerms: t.contractTerms, -      contractTermsHash: t.contractTermsHash, -      found: true, -      lastSessionSig: t.lastSessionSig, -      lastSessionId: t.lastSessionId, -      payReq: t.payReq, -    }; +    return t;    } @@ -2723,46 +2691,11 @@ export class Wallet {    }    /** -   * Get planchets for a tip.  Creates new planchets if they don't exist already -   * for this tip.  The tip is uniquely identified by the merchant's domain and the tip id. +   * Workaround for merchant bug (#5258)     */ -  async getTipPlanchets(merchantDomain: string, -                        tipId: string, -                        amount: AmountJson, -                        deadline: number, -                        exchangeUrl: string, -                        nextUrl: string): Promise<TipPlanchetDetail[]> { -    let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); -    if (!tipRecord) { -      await this.updateExchangeFromUrl(exchangeUrl); -      const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(exchangeUrl, amount); -      const planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d))); -      const coinPubs: string[] = planchets.map(x => x.coinPub); -      const now = (new Date()).getTime(); -      tipRecord = { -        accepted: false, -        amount, -        coinPubs, -        deadline, -        exchangeUrl, -        merchantDomain, -        nextUrl, -        planchets, -        timestamp: now, -        tipId, -      }; -      await this.q().put(Stores.tips, tipRecord).finish(); -    } -    // Planchets in the form that the merchant expects -    const planchetDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({ -      coin_ev: p.coinEv, -      denom_pub_hash: p.denomPubHash, -    })); -    return planchetDetail; -  } +  private tipPickupWorkaround: { [tipId: string]: boolean } = {}; - -  async processTip(tipToken: TipToken): Promise<void> { +  async processTip(tipToken: TipToken): Promise<TipRecord> {      console.log("got tip token", tipToken);      const deadlineSec = getTalerStampSec(tipToken.expiration); @@ -2770,55 +2703,61 @@ export class Wallet {        throw Error("tipping failed (invalid expiration)");      } -    const merchantDomain = new URI(document.location.href).origin(); -    let walletResp; -    walletResp = await this.getTipPlanchets(merchantDomain, -                                              tipToken.tip_id, -                                              tipToken.amount, -                                              deadlineSec, -                                              tipToken.exchange_url, -                                              tipToken.next_url); - -    const planchets = walletResp; +    const merchantDomain = new URI(tipToken.pickup_url).origin(); +    let tipRecord = await this.q().get(Stores.tips, [tipToken.tip_id, merchantDomain]); -    if (!planchets) { -      console.log("failed tip", walletResp); -      throw Error("processing tip failed"); +    if (tipRecord && tipRecord.pickedUp) { +      return tipRecord;      } +    await this.updateExchangeFromUrl(tipToken.exchange_url); +    const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(tipToken.exchange_url, tipToken.amount); +    const planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d))); +    const coinPubs: string[] = planchets.map(x => x.coinPub); +    const now = (new Date()).getTime(); +    tipRecord = { +      accepted: false, +      amount: tipToken.amount, +      coinPubs, +      deadline: deadlineSec, +      exchangeUrl: tipToken.exchange_url, +      merchantDomain, +      nextUrl: tipToken.next_url, +      pickedUp: false, +      planchets, +      timestamp: now, +      tipId: tipToken.tip_id, +    }; + +    // 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,        }; -      const req = { planchets, tip_id: tipToken.tip_id }; +      const req = { planchets: planchetsDetail, tip_id: tipToken.tip_id };        merchantResp = await axios.post(tipToken.pickup_url, req, config);      } catch (e) {        console.log("tipping failed", e);        throw e;      } -    try { -      this.processTipResponse(merchantDomain, tipToken.tip_id, merchantResp.data); -    } catch (e) { -      console.log("processTipResponse failed", e); -      throw e; -    } +    this.tipPickupWorkaround[tipToken.tip_id] = true; -    return; -  } +    const response = TipResponse.checked(merchantResp.data); -  /** -   * Accept a merchant's response to a tip pickup and start withdrawing the coins. -   * These coins will not appear in the wallet yet. -   */ -  async processTipResponse(merchantDomain: string, tipId: string, response: TipResponse): Promise<void> { -    const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); -    if (!tipRecord) { -      throw Error("tip not found"); -    } -    console.log("processing tip response", response);      if (response.reserve_sigs.length !== tipRecord.planchets.length) {        throw Error("number of tip responses does not match requested planchets");      } @@ -2840,12 +2779,21 @@ export class Wallet {        await this.q().put(Stores.precoins, preCoin);        this.processPreCoin(preCoin);      } + +    tipRecord.pickedUp = true; + +    await this.q().put(Stores.tips, tipRecord).finish(); + +    return tipRecord;    } +    /**     * Start using the coins from a tip.     */ -  async acceptTip(merchantDomain: string, tipId: string): Promise<void> { +  async acceptTip(tipToken: TipToken): Promise<void> { +    const tipId = tipToken.tip_id; +    const merchantDomain = new URI(tipToken.pickup_url).origin();      const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);      if (!tipRecord) {        throw Error("tip not found"); @@ -2875,11 +2823,9 @@ export class Wallet {      this.notifier.notify();    } -  async getTipStatus(merchantDomain: string, tipId: string): Promise<TipStatus> { -    const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]); -    if (!tipRecord) { -      throw Error("tip not found"); -    } + +  async getTipStatus(tipToken: TipToken): Promise<TipStatus> { +    const tipRecord = await this.processTip(tipToken);      const rci = await this.getReserveCreationInfo(tipRecord.exchangeUrl, tipRecord.amount);      const tipStatus: TipStatus = {        rci, diff --git a/src/walletTypes.ts b/src/walletTypes.ts index c98717ac2..aba7dbfba 100644 --- a/src/walletTypes.ts +++ b/src/walletTypes.ts @@ -41,7 +41,6 @@ import {    CoinPaySig,    ContractTerms,    PayReq, -  TipResponse,  } from "./talerTypes"; @@ -281,12 +280,6 @@ export interface HistoryRecord {  /** - * Response to a query payment request.  Tagged union over the 'found' field. - */ -export type QueryPaymentResult = QueryPaymentNotFound | QueryPaymentFound; - - -/**   * Query payment response when the payment was found.   */  export interface QueryPaymentNotFound { @@ -304,6 +297,7 @@ export interface QueryPaymentFound {    lastSessionSig?: string;    lastSessionId?: string;    payReq: PayReq; +  proposalId: number;  } @@ -438,7 +432,6 @@ export interface CoinWithDenom {    denom: DenominationRecord;  } -  /**   * Status of processing a tip.   */ @@ -449,138 +442,6 @@ export interface TipStatus {  /** - * Request to the wallet for the status of processing a tip. - */ -@Checkable.Class() -export class TipStatusRequest { -  /** -   * Identifier of the tip. -   */ -  @Checkable.String -  tipId: string; - -  /** -   * Merchant domain.  Within each merchant domain, the tip identifier -   * uniquely identifies a tip. -   */ -  @Checkable.String -  merchantDomain: string; - -  /** -   * Create a TipStatusRequest from untyped JSON. -   */ -  static checked: (obj: any) => TipStatusRequest; -} - -/** - * Request to the wallet to accept a tip. - */ -@Checkable.Class() -export class AcceptTipRequest { -  /** -   * Identifier of the tip. -   */ -  @Checkable.String -  tipId: string; - -  /** -   * Merchant domain.  Within each merchant domain, the tip identifier -   * uniquely identifies a tip. -   */ -  @Checkable.String -  merchantDomain: string; - -  /** -   * Create an AcceptTipRequest from untyped JSON. -   * Validates the schema and throws on error. -   */ -  static checked: (obj: any) => AcceptTipRequest; -} - - -/** - * Request for the wallet to process a tip response from a merchant. - */ -@Checkable.Class() -export class ProcessTipResponseRequest { -  /** -   * Identifier of the tip. -   */ -  @Checkable.String -  tipId: string; - -  /** -   * Merchant domain.  Within each merchant domain, the tip identifier -   * uniquely identifies a tip. -   */ -  @Checkable.String -  merchantDomain: string; - -  /** -   * Tip response from the merchant. -   */ -  @Checkable.Value(() => TipResponse) -  tipResponse: TipResponse; - -  /** -   * Create an AcceptTipRequest from untyped JSON. -   * Validates the schema and throws on error. -   */ -  static checked: (obj: any) => ProcessTipResponseRequest; -} - - -/** - * Request for the wallet to generate tip planchets. - */ -@Checkable.Class() -export class GetTipPlanchetsRequest { -  /** -   * Identifier of the tip. -   */ -  @Checkable.String -  tipId: string; - -  /** -   * Merchant domain.  Within each merchant domain, the tip identifier -   * uniquely identifies a tip. -   */ -  @Checkable.String -  merchantDomain: string; - -  /** -   * Amount of the tip. -   */ -  @Checkable.Optional(Checkable.Value(() => AmountJson)) -  amount: AmountJson; - -  /** -   * Deadline for picking up the tip. -   */ -  @Checkable.Number -  deadline: number; - -  /** -   * Exchange URL that must be used to pick up the tip. -   */ -  @Checkable.String -  exchangeUrl: string; - -  /** -   * URL to nagivate to after processing the tip. -   */ -  @Checkable.String -  nextUrl: string; - -  /** -   * Create an AcceptTipRequest from untyped JSON. -   * Validates the schema and throws on error. -   */ -  static checked: (obj: any) => GetTipPlanchetsRequest; -} - - -/**   * Badge that shows activity for the wallet.   */  export interface Badge { diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 0fcd6047e..e1bd6f12c 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -171,20 +171,12 @@ export interface MessageMap {      request: { refundPermissions: talerTypes.RefundPermission[] };      response: void;    }; -  "get-tip-planchets": { -    request: walletTypes.GetTipPlanchetsRequest; -    response: void; -  }; -  "process-tip-response": { -    request: walletTypes.ProcessTipResponseRequest; -    response: void; -  };    "accept-tip": { -    request: walletTypes.AcceptTipRequest; +    request: { tipToken: talerTypes.TipToken };      response: void;    };    "get-tip-status": { -    request: walletTypes.TipStatusRequest; +    request: { tipToken: talerTypes.TipToken };      response: void;    };    "clear-notification": { @@ -199,6 +191,10 @@ export interface MessageMap {      request: any;      response: void;    }; +  "submit-pay": { +    request: { contractTermsHash: string, sessionId: string | undefined }; +    response: void; +  };  }  /** diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx index cd58d712a..2ec131052 100644 --- a/src/webex/pages/confirm-contract.tsx +++ b/src/webex/pages/confirm-contract.tsx @@ -122,6 +122,7 @@ interface ContractPromptState {     */    holdCheck: boolean;    payStatus?: CheckPayResult; +  replaying: boolean;  }  class ContractPrompt extends React.Component<ContractPromptProps, ContractPromptState> { @@ -135,6 +136,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt        payDisabled: true,        proposal: null,        proposalId: props.proposalId, +      replaying: false,      };    } @@ -150,13 +152,23 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt      if (this.props.resourceUrl) {        const p = await wxApi.queryPaymentByFulfillmentUrl(this.props.resourceUrl);        console.log("query for resource url", this.props.resourceUrl, "result", p); -      if (p.found && (p.lastSessionSig === undefined || p.lastSessionSig === this.props.sessionId)) { -        const nextUrl = new URI(p.contractTerms.fulfillment_url); -        nextUrl.addSearch("order_id", p.contractTerms.order_id); -        if (p.lastSessionSig) { -          nextUrl.addSearch("session_sig", p.lastSessionSig); +      if (p) { +        if (p.lastSessionSig === undefined || p.lastSessionSig === this.props.sessionId) { +          const nextUrl = new URI(p.contractTerms.fulfillment_url); +          nextUrl.addSearch("order_id", p.contractTerms.order_id); +          if (p.lastSessionSig) { +            nextUrl.addSearch("session_sig", p.lastSessionSig); +          } +          location.replace(nextUrl.href()); +          return; +        } else { +          // We're in a new session +          this.setState({ replaying: true }); +          const payResult = await wxApi.submitPay(p.contractTermsHash, this.props.sessionId); +          console.log("payResult", payResult); +          location.replace(payResult.nextUrl); +          return;          } -        location.href = nextUrl.href();        }      }      let proposalId = this.props.proposalId; @@ -230,6 +242,9 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt      if (this.props.contractUrl === undefined && this.props.proposalId === undefined) {        return <span>Error: either contractUrl or proposalId must be given</span>;      } +    if (this.state.replaying) { +      return <span>Re-submitting existing payment</span>; +    }      if (this.state.proposalId === undefined) {        return <span>Downloading contract terms</span>;      } @@ -245,26 +260,40 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt      }      const amount = <strong>{renderAmount(c.amount)}</strong>;      console.log("payStatus", this.state.payStatus); + +    let products = null; +    if (c.products.length) { +      products = ( +        <> +          <span>The following items are included:</span> +          <ul> +            {c.products.map( +              (p: any, i: number) => (<li key={i}>{p.description}: {renderAmount(p.price)}</li>)) +            } +          </ul> +      </> +      ); +    }      return ( -      <div> +      <>          <div>            <i18n.Translate wrap="p">              The merchant <span>{merchantName}</span> {" "}              offers you to purchase:            </i18n.Translate> -          <ul> -            {c.products.map( -              (p: any, i: number) => (<li key={i}>{p.description}: {renderAmount(p.price)}</li>)) -            } -          </ul> -            {(this.state.payStatus && this.state.payStatus.coinSelection) -              ? <p> -                  The total price is <span>{amount}</span>{" "} -                  (plus <span>{renderAmount(this.state.payStatus.coinSelection.totalFees)}</span> fees). -                </p> -              : -              <p>The total price is <span>{amount}</span>.</p> -            } +          <div style={{"text-align": "center"}}> +            <strong>{c.summary}</strong> +          </div> +          <strong></strong> +          {products} +          {(this.state.payStatus && this.state.payStatus.coinSelection) +            ? <p> +                The total price is <span>{amount}</span>{" "} +                (plus <span>{renderAmount(this.state.payStatus.coinSelection.totalFees)}</span> fees). +              </p> +            : +            <p>The total price is <span>{amount}</span>.</p> +          }          </div>          <button className="pure-button button-success"                  disabled={this.state.payDisabled} @@ -280,7 +309,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt            {(this.state.error ? <p className="errorbox">{this.state.error}</p> : <p />)}          </div>          <Details exchanges={this.state.exchanges} contractTerms={c} collapsed={!this.state.error}/> -      </div> +      </>      );    }  } @@ -296,10 +325,8 @@ document.addEventListener("DOMContentLoaded", () => {    } catch  {      // ignore error    } -    const sessionId = query.sessionId;    const contractUrl = query.contractUrl; -    const resourceUrl = query.resourceUrl;    ReactDOM.render( diff --git a/src/webex/pages/tip.tsx b/src/webex/pages/tip.tsx index 7f96401c5..578ae6aa4 100644 --- a/src/webex/pages/tip.tsx +++ b/src/webex/pages/tip.tsx @@ -39,11 +39,11 @@ import {  } from "../renderHtml";  import * as Amounts from "../../amounts"; +import { TipToken } from "../../talerTypes";  import { TipStatus } from "../../walletTypes";  interface TipDisplayProps { -  merchantDomain: string; -  tipId: string; +  tipToken: TipToken;  }  interface TipDisplayState { @@ -58,7 +58,7 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {    }    async update() { -    const tipStatus = await getTipStatus(this.props.merchantDomain, this.props.tipId); +    const tipStatus = await getTipStatus(this.props.tipToken);      this.setState({ tipStatus });    } @@ -96,7 +96,7 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {    accept() {      this.setState({ working: true}); -    acceptTip(this.props.merchantDomain, this.props.tipId); +    acceptTip(this.props.tipToken);    }    renderButtons() { @@ -126,7 +126,7 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {        <div>          <h2>Tip Received!</h2>          <p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <span> </span> -        <strong>{this.props.merchantDomain}</strong>.</p> +        <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() @@ -142,11 +142,9 @@ async function main() {      const url = new URI(document.location.href);      const query: any = URI.parseQuery(url.query()); -    const merchantDomain = query.merchant_domain; -    const tipId = query.tip_id; -    const props: TipDisplayProps = { tipId, merchantDomain }; +    const tipToken = TipToken.checked(JSON.parse(query.tip_token)); -    ReactDOM.render(<TipDisplay {...props} />, +    ReactDOM.render(<TipDisplay tipToken={tipToken} />,                      document.getElementById("container")!);    } catch (e) { diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 84c44dbaa..566f45265 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -35,7 +35,6 @@ import {  import {    CheckPayResult,    ConfirmPayResult, -  QueryPaymentResult,    ReserveCreationInfo,    SenderWireInfos,    TipStatus, @@ -44,8 +43,7 @@ import {  import {    RefundPermission, -  TipPlanchetDetail, -  TipResponse, +  TipToken,  } from "../talerTypes";  import { MessageMap, MessageType } from "./messages"; @@ -222,6 +220,13 @@ export function confirmPay(proposalId: number, sessionId: string | undefined): P  }  /** + * Replay paying for a purchase. + */ +export function submitPay(contractTermsHash: string, sessionId: string | undefined): Promise<ConfirmPayResult> { +  return callBackend("submit-pay", { contractTermsHash, sessionId }); +} + +/**   * Hash a contract.  Throws if its not a valid contract.   */  export function hashContract(contract: object): Promise<string> { @@ -238,7 +243,7 @@ export function confirmReserve(reservePub: string): Promise<void> {  /**   * Query for a payment by fulfillment URL.   */ -export function queryPaymentByFulfillmentUrl(url: string): Promise<QueryPaymentResult> { +export function queryPaymentByFulfillmentUrl(url: string): Promise<PurchaseRecord> {    return callBackend("query-payment", { url });  } @@ -324,37 +329,19 @@ export function getFullRefundFees(args: { refundPermissions: RefundPermission[]  /** - * Get or generate planchets to give the merchant that wants to tip us. - */ -export function getTipPlanchets(merchantDomain: string, -                                tipId: string, -                                amount: AmountJson, -                                deadline: number, -                                exchangeUrl: string, -                                nextUrl: string): Promise<TipPlanchetDetail[]> { -  return callBackend("get-tip-planchets", { merchantDomain, tipId, amount, deadline, exchangeUrl, nextUrl }); -} - -/**   * Get the status of processing a tip.   */ -export function getTipStatus(merchantDomain: string, tipId: string): Promise<TipStatus> { -  return callBackend("get-tip-status", { merchantDomain, tipId }); +export function getTipStatus(tipToken: TipToken): Promise<TipStatus> { +  return callBackend("get-tip-status", { tipToken });  }  /**   * Mark a tip as accepted by the user.   */ -export function acceptTip(merchantDomain: string, tipId: string): Promise<TipStatus> { -  return callBackend("accept-tip", { merchantDomain, tipId }); +export function acceptTip(tipToken: TipToken): Promise<TipStatus> { +  return callBackend("accept-tip", { tipToken });  } -/** - * Process a response from the merchant for a tip request. - */ -export function processTipResponse(merchantDomain: string, tipId: string, tipResponse: TipResponse): Promise<void> { -  return callBackend("process-tip-response", { merchantDomain, tipId, tipResponse }); -}  /**   * Clear notifications that the wallet shows to the user. diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index a4f534af9..26b8ff2cf 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -34,15 +34,10 @@ import {  import { AmountJson } from "../amounts";  import { -  AcceptTipRequest,    ConfirmReserveRequest,    CreateReserveRequest, -  GetTipPlanchetsRequest,    Notifier, -  ProcessTipResponseRequest, -  QueryPaymentFound,    ReturnCoinsRequest, -  TipStatusRequest,  } from "../walletTypes";  import { @@ -50,6 +45,7 @@ import {  } from "../wallet";  import { +  PurchaseRecord,    Stores,    WALLET_DB_VERSION,  } from "../dbTypes"; @@ -136,6 +132,12 @@ function handleMessage(sender: MessageSender,        }        return needsWallet().confirmPay(detail.proposalId, detail.sessionId);      } +    case "submit-pay": { +      if (typeof detail.contractTermsHash !== "string") { +        throw Error("contractTermsHash must be a string"); +      } +      return needsWallet().submitPay(detail.contractTermsHash, detail.sessionId); +    }      case "check-pay": {        if (typeof detail.proposalId !== "number") {          throw Error("proposalId must be number"); @@ -291,25 +293,12 @@ function handleMessage(sender: MessageSender,      case "get-full-refund-fees":        return needsWallet().getFullRefundFees(detail.refundPermissions);      case "get-tip-status": { -      const req = TipStatusRequest.checked(detail); -      return needsWallet().getTipStatus(req.merchantDomain, req.tipId); +      const tipToken = TipToken.checked(detail.tipToken); +      return needsWallet().getTipStatus(tipToken);      }      case "accept-tip": { -      const req = AcceptTipRequest.checked(detail); -      return needsWallet().acceptTip(req.merchantDomain, req.tipId); -    } -    case "process-tip-response": { -      const req = ProcessTipResponseRequest.checked(detail); -      return needsWallet().processTipResponse(req.merchantDomain, req.tipId, req.tipResponse); -    } -    case "get-tip-planchets": { -      const req = GetTipPlanchetsRequest.checked(detail); -      return needsWallet().getTipPlanchets(req.merchantDomain, -                                           req.tipId, -                                           req.amount, -                                           req.deadline, -                                           req.exchangeUrl, -                                           req.nextUrl); +      const tipToken = TipToken.checked(detail.tipToken); +      return needsWallet().acceptTip(tipToken);      }      case "clear-notification": {        return needsWallet().clearNotification(); @@ -410,7 +399,7 @@ async function talerPay(fields: any, url: string, tabId: number): Promise<string    const w = currentWallet; -  const goToPayment = (p: QueryPaymentFound): string => { +  const goToPayment = (p: PurchaseRecord): string => {      const nextUrl = new URI(p.contractTerms.fulfillment_url);      nextUrl.addSearch("order_id", p.contractTerms.order_id);      if (p.lastSessionSig) { @@ -422,14 +411,7 @@ async function talerPay(fields: any, url: string, tabId: number): Promise<string    if (fields.resource_url) {      const p = await w.queryPaymentByFulfillmentUrl(fields.resource_url);      console.log("query for resource url", fields.resource_url, "result", p); -    if (p.found && (fields.session_id === undefined || fields.session_id === p.lastSessionId)) { -      return goToPayment(p); -    } -  } -  if (fields.contract_hash) { -    const p = await w.queryPaymentByContractTermsHash(fields.contract_hash); -    if (p.found) { -      goToPayment(p); +    if (p && (fields.session_id === undefined || fields.session_id === p.lastSessionId)) {        return goToPayment(p);      }    } @@ -452,15 +434,8 @@ async function talerPay(fields: any, url: string, tabId: number): Promise<string      return chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);    }    if (fields.tip) { -    const tipToken = TipToken.checked(fields.tip); -    w.processTip(tipToken); -    // Go to tip dialog page, where the user can confirm the tip or -    // decline if they are not happy with the exchange. -    const merchantDomain = new URI(url).origin();      const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html")); -    const params = { tip_id: tipToken.tip_id, merchant_domain: merchantDomain }; -    const redirectUrl = uri.query(params).href(); -    return redirectUrl; +    return uri.query({ tip_token: fields.tip }).href();    }    return undefined;  } @@ -486,7 +461,6 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri    }    const fields = { -    contract_hash: headers["x-taler-contract-hash"],      contract_url: headers["x-taler-contract-url"],      offer_url: headers["x-taler-offer-url"],      refund_url: headers["x-taler-refund-url"], @@ -506,15 +480,15 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri    console.log("got pay detail", fields); -  // Fast path for existing payment +  // Synchronous fast path for existing payment    if (fields.resource_url) {      const result = currentWallet.getNextUrlFromResourceUrl(fields.resource_url);      if (result && (fields.session_id === undefined || fields.session_id === result.lastSessionId)) {        return { redirectUrl: result.nextUrl };      }    } -  // Fast path for new contract -  if (!fields.contract_hash && fields.contract_url) { +  // Synchronous fast path for new contract +  if (fields.contract_url) {      const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));      uri.addSearch("contractUrl", fields.contract_url);      if (fields.session_id) { @@ -526,6 +500,13 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri      return { redirectUrl: uri.href() };    } +  // Synchronous fast path for tip +  if (fields.tip) { +    const uri = new URI(chrome.extension.getURL("/src/webex/pages/tip.html")); +    uri.query({ tip_token: fields.tip }); +    return { redirectUrl: uri.href() }; +  } +    // We need to do some asynchronous operation, we can't directly redirect    talerPay(fields, url, tabId).then((nextUrl) => {      if (nextUrl) { | 
