refactor tipping, adjust to new redirect-based API
This commit is contained in:
parent
2f68e9e50e
commit
1671d9a508
@ -574,6 +574,11 @@ export interface TipRecord {
|
||||
*/
|
||||
accepted: boolean;
|
||||
|
||||
/**
|
||||
* Have we picked up the tip record from the merchant already?
|
||||
*/
|
||||
pickedUp: boolean;
|
||||
|
||||
/**
|
||||
* The tipped amount.
|
||||
*/
|
||||
|
@ -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"
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
@ -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 ""
|
||||
|
184
src/wallet.ts
184
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,
|
||||
};
|
||||
return undefined;
|
||||
}
|
||||
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,
|
||||
};
|
||||
}
|
||||
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 merchantDomain = new URI(tipToken.pickup_url).origin();
|
||||
let tipRecord = await this.q().get(Stores.tips, [tipToken.tip_id, merchantDomain]);
|
||||
|
||||
const planchets = walletResp;
|
||||
|
||||
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,
|
||||
|
@ -41,7 +41,6 @@ import {
|
||||
CoinPaySig,
|
||||
ContractTerms,
|
||||
PayReq,
|
||||
TipResponse,
|
||||
} from "./talerTypes";
|
||||
|
||||
|
||||
@ -280,12 +279,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.
|
||||
*/
|
||||
@ -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.
|
||||
*/
|
||||
@ -448,138 +441,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.
|
||||
*/
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<i18n.Translate wrap="p">
|
||||
The merchant <span>{merchantName}</span> {" "}
|
||||
offers you to purchase:
|
||||
</i18n.Translate>
|
||||
|
||||
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>
|
||||
{(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>
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<i18n.Translate wrap="p">
|
||||
The merchant <span>{merchantName}</span> {" "}
|
||||
offers you to purchase:
|
||||
</i18n.Translate>
|
||||
<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(
|
||||
|
@ -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) {
|
||||
|
@ -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";
|
||||
@ -221,6 +219,13 @@ export function confirmPay(proposalId: number, sessionId: string | undefined): P
|
||||
return callBackend("confirm-pay", { proposalId, sessionId });
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -323,38 +328,20 @@ 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.
|
||||
|
@ -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) {
|
||||
|
Loading…
Reference in New Issue
Block a user