summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-01-19 01:27:27 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-01-19 01:27:27 +0100
commit1671d9a508b803af31762bcd9508e70eb40e7b48 (patch)
tree24d79103d0661c9edafd1d6371692b726b2f0ef3
parent2f68e9e50e83c55ca46e9d4d72956d6525d0fa8c (diff)
downloadwallet-core-1671d9a508b803af31762bcd9508e70eb40e7b48.tar.gz
wallet-core-1671d9a508b803af31762bcd9508e70eb40e7b48.tar.bz2
wallet-core-1671d9a508b803af31762bcd9508e70eb40e7b48.zip
refactor tipping, adjust to new redirect-based API
-rw-r--r--src/dbTypes.ts5
-rw-r--r--src/i18n/de.po8
-rw-r--r--src/i18n/en-US.po8
-rw-r--r--src/i18n/fr.po8
-rw-r--r--src/i18n/it.po8
-rw-r--r--src/i18n/taler-wallet-webex.pot8
-rw-r--r--src/wallet.ts184
-rw-r--r--src/walletTypes.ts141
-rw-r--r--src/webex/messages.ts16
-rw-r--r--src/webex/pages/confirm-contract.tsx73
-rw-r--r--src/webex/pages/tip.tsx16
-rw-r--r--src/webex/wxApi.ts39
-rw-r--r--src/webex/wxBackend.ts67
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) {