implement new mobile-compatible payment logic
This commit is contained in:
parent
894a09a51c
commit
c62ba4986f
@ -15,8 +15,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
"use strict";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decorators for validating JSON objects and converting them to a typed
|
* Decorators for validating JSON objects and converting them to a typed
|
||||||
* object.
|
* object.
|
||||||
@ -55,6 +53,7 @@ export namespace Checkable {
|
|||||||
propertyKey: any;
|
propertyKey: any;
|
||||||
checker: any;
|
checker: any;
|
||||||
type?: any;
|
type?: any;
|
||||||
|
typeThunk?: () => any;
|
||||||
elementChecker?: any;
|
elementChecker?: any;
|
||||||
elementProp?: any;
|
elementProp?: any;
|
||||||
keyProp?: any;
|
keyProp?: any;
|
||||||
@ -167,11 +166,18 @@ export namespace Checkable {
|
|||||||
|
|
||||||
|
|
||||||
function checkValue(target: any, prop: Prop, path: Path): any {
|
function checkValue(target: any, prop: Prop, path: Path): any {
|
||||||
const type = prop.type;
|
let type;
|
||||||
const typeName = type.name || "??";
|
if (prop.type) {
|
||||||
if (!type) {
|
type = prop.type;
|
||||||
throw Error(`assertion failed (prop is ${JSON.stringify(prop)})`);
|
} else if (prop.typeThunk) {
|
||||||
|
type = prop.typeThunk();
|
||||||
|
if (!type) {
|
||||||
|
throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`);
|
||||||
}
|
}
|
||||||
|
const typeName = type.name || "??";
|
||||||
const v = target;
|
const v = target;
|
||||||
if (!v || typeof v !== "object") {
|
if (!v || typeof v !== "object") {
|
||||||
throw new SchemaError(
|
throw new SchemaError(
|
||||||
@ -236,16 +242,13 @@ export namespace Checkable {
|
|||||||
/**
|
/**
|
||||||
* Target property must be a Checkable object of the given type.
|
* Target property must be a Checkable object of the given type.
|
||||||
*/
|
*/
|
||||||
export function Value(type: any) {
|
export function Value(typeThunk: () => any) {
|
||||||
if (!type) {
|
|
||||||
throw Error("Type does not exist yet (wrong order of definitions?)");
|
|
||||||
}
|
|
||||||
function deco(target: object, propertyKey: string | symbol): void {
|
function deco(target: object, propertyKey: string | symbol): void {
|
||||||
const chk = getCheckableInfo(target);
|
const chk = getCheckableInfo(target);
|
||||||
chk.props.push({
|
chk.props.push({
|
||||||
checker: checkValue,
|
checker: checkValue,
|
||||||
propertyKey,
|
propertyKey,
|
||||||
type,
|
typeThunk,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ import {
|
|||||||
* In the future we might consider adding migration functions for
|
* In the future we might consider adding migration functions for
|
||||||
* each version increment.
|
* each version increment.
|
||||||
*/
|
*/
|
||||||
export const WALLET_DB_VERSION = 24;
|
export const WALLET_DB_VERSION = 25;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -206,7 +206,7 @@ export class DenominationRecord {
|
|||||||
/**
|
/**
|
||||||
* Value of one coin of the denomination.
|
* Value of one coin of the denomination.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
value: AmountJson;
|
value: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -225,25 +225,25 @@ export class DenominationRecord {
|
|||||||
/**
|
/**
|
||||||
* Fee for withdrawing.
|
* Fee for withdrawing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
feeWithdraw: AmountJson;
|
feeWithdraw: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for depositing.
|
* Fee for depositing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
feeDeposit: AmountJson;
|
feeDeposit: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refreshing.
|
* Fee for refreshing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
feeRefresh: AmountJson;
|
feeRefresh: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refunding.
|
* Fee for refunding.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
feeRefund: AmountJson;
|
feeRefund: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -491,15 +491,22 @@ export interface CoinRecord {
|
|||||||
status: CoinStatus;
|
status: CoinStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Proposal record, stored in the wallet's database.
|
* Proposal record, stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
@Checkable.Class()
|
||||||
export class ProposalRecord {
|
export class ProposalDownloadRecord {
|
||||||
|
/**
|
||||||
|
* URL where the proposal was downloaded.
|
||||||
|
*/
|
||||||
|
@Checkable.String
|
||||||
|
url: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The contract that was offered by the merchant.
|
* The contract that was offered by the merchant.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(ContractTerms)
|
@Checkable.Value(() => ContractTerms)
|
||||||
contractTerms: ContractTerms;
|
contractTerms: ContractTerms;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -527,11 +534,17 @@ export class ProposalRecord {
|
|||||||
@Checkable.Number
|
@Checkable.Number
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Private key for the nonce.
|
||||||
|
*/
|
||||||
|
@Checkable.String
|
||||||
|
noncePriv: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
* member.
|
* member.
|
||||||
*/
|
*/
|
||||||
static checked: (obj: any) => ProposalRecord;
|
static checked: (obj: any) => ProposalDownloadRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -788,15 +801,6 @@ export interface SenderWireRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Nonce record as stored in the wallet's database.
|
|
||||||
*/
|
|
||||||
export interface NonceRecord {
|
|
||||||
priv: string;
|
|
||||||
pub: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration key/value entries to configure
|
* Configuration key/value entries to configure
|
||||||
* the wallet.
|
* the wallet.
|
||||||
@ -869,12 +873,6 @@ export namespace Stores {
|
|||||||
pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey");
|
pubKeyIndex = new Index<string, ExchangeRecord>(this, "pubKeyIndex", "masterPublicKey");
|
||||||
}
|
}
|
||||||
|
|
||||||
class NonceStore extends Store<NonceRecord> {
|
|
||||||
constructor() {
|
|
||||||
super("nonces", { keyPath: "pub" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class CoinsStore extends Store<CoinRecord> {
|
class CoinsStore extends Store<CoinRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("coins", { keyPath: "coinPub" });
|
super("coins", { keyPath: "coinPub" });
|
||||||
@ -884,14 +882,14 @@ export namespace Stores {
|
|||||||
denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub");
|
denomPubIndex = new Index<string, CoinRecord>(this, "denomPubIndex", "denomPub");
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProposalsStore extends Store<ProposalRecord> {
|
class ProposalsStore extends Store<ProposalDownloadRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("proposals", {
|
super("proposals", {
|
||||||
autoIncrement: true,
|
autoIncrement: true,
|
||||||
keyPath: "id",
|
keyPath: "id",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
timestampIndex = new Index<string, ProposalRecord>(this, "timestampIndex", "timestamp");
|
timestampIndex = new Index<string, ProposalDownloadRecord>(this, "timestampIndex", "timestamp");
|
||||||
}
|
}
|
||||||
|
|
||||||
class PurchasesStore extends Store<PurchaseRecord> {
|
class PurchasesStore extends Store<PurchaseRecord> {
|
||||||
@ -965,7 +963,6 @@ export namespace Stores {
|
|||||||
export const denominations = new DenominationsStore();
|
export const denominations = new DenominationsStore();
|
||||||
export const exchangeWireFees = new ExchangeWireFeesStore();
|
export const exchangeWireFees = new ExchangeWireFeesStore();
|
||||||
export const exchanges = new ExchangeStore();
|
export const exchanges = new ExchangeStore();
|
||||||
export const nonces = new NonceStore();
|
|
||||||
export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
|
export const precoins = new Store<PreCoinRecord>("precoins", {keyPath: "coinPub"});
|
||||||
export const proposals = new ProposalsStore();
|
export const proposals = new ProposalsStore();
|
||||||
export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
|
export const refresh = new Store<RefreshSessionRecord>("refresh", {keyPath: "id", autoIncrement: true});
|
||||||
|
@ -42,13 +42,13 @@ msgstr ""
|
|||||||
msgid "Exchanges in the wallet:"
|
msgid "Exchanges in the wallet:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:159
|
#: src/webex/pages/confirm-contract.tsx:175
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "You have insufficient funds of the requested currency in your wallet."
|
msgid "You have insufficient funds of the requested currency in your wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. tslint:disable-next-line:max-line-length
|
#. tslint:disable-next-line:max-line-length
|
||||||
#: src/webex/pages/confirm-contract.tsx:161
|
#: src/webex/pages/confirm-contract.tsx:177
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any funds from an exchange that is accepted by this "
|
"You do not have any funds from an exchange that is accepted by this "
|
||||||
@ -56,12 +56,12 @@ msgid ""
|
|||||||
"wallet."
|
"wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:217
|
#: src/webex/pages/confirm-contract.tsx:236
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "The merchant%1$s offers you to purchase:\n"
|
msgid "The merchant%1$s offers you to purchase:\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:238
|
#: src/webex/pages/confirm-contract.tsx:257
|
||||||
#, fuzzy, c-format
|
#, fuzzy, c-format
|
||||||
msgid "Confirm payment"
|
msgid "Confirm payment"
|
||||||
msgstr "Bezahlung bestätigen"
|
msgstr "Bezahlung bestätigen"
|
||||||
|
@ -42,13 +42,13 @@ msgstr ""
|
|||||||
msgid "Exchanges in the wallet:"
|
msgid "Exchanges in the wallet:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:159
|
#: src/webex/pages/confirm-contract.tsx:175
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "You have insufficient funds of the requested currency in your wallet."
|
msgid "You have insufficient funds of the requested currency in your wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. tslint:disable-next-line:max-line-length
|
#. tslint:disable-next-line:max-line-length
|
||||||
#: src/webex/pages/confirm-contract.tsx:161
|
#: src/webex/pages/confirm-contract.tsx:177
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any funds from an exchange that is accepted by this "
|
"You do not have any funds from an exchange that is accepted by this "
|
||||||
@ -56,12 +56,12 @@ msgid ""
|
|||||||
"wallet."
|
"wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:217
|
#: src/webex/pages/confirm-contract.tsx:236
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "The merchant%1$s offers you to purchase:\n"
|
msgid "The merchant%1$s offers you to purchase:\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:238
|
#: src/webex/pages/confirm-contract.tsx:257
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Confirm payment"
|
msgid "Confirm payment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -42,13 +42,13 @@ msgstr ""
|
|||||||
msgid "Exchanges in the wallet:"
|
msgid "Exchanges in the wallet:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:159
|
#: src/webex/pages/confirm-contract.tsx:175
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "You have insufficient funds of the requested currency in your wallet."
|
msgid "You have insufficient funds of the requested currency in your wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. tslint:disable-next-line:max-line-length
|
#. tslint:disable-next-line:max-line-length
|
||||||
#: src/webex/pages/confirm-contract.tsx:161
|
#: src/webex/pages/confirm-contract.tsx:177
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any funds from an exchange that is accepted by this "
|
"You do not have any funds from an exchange that is accepted by this "
|
||||||
@ -56,12 +56,12 @@ msgid ""
|
|||||||
"wallet."
|
"wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:217
|
#: src/webex/pages/confirm-contract.tsx:236
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "The merchant%1$s offers you to purchase:\n"
|
msgid "The merchant%1$s offers you to purchase:\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:238
|
#: src/webex/pages/confirm-contract.tsx:257
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Confirm payment"
|
msgid "Confirm payment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -42,13 +42,13 @@ msgstr ""
|
|||||||
msgid "Exchanges in the wallet:"
|
msgid "Exchanges in the wallet:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:159
|
#: src/webex/pages/confirm-contract.tsx:175
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "You have insufficient funds of the requested currency in your wallet."
|
msgid "You have insufficient funds of the requested currency in your wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. tslint:disable-next-line:max-line-length
|
#. tslint:disable-next-line:max-line-length
|
||||||
#: src/webex/pages/confirm-contract.tsx:161
|
#: src/webex/pages/confirm-contract.tsx:177
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any funds from an exchange that is accepted by this "
|
"You do not have any funds from an exchange that is accepted by this "
|
||||||
@ -56,12 +56,12 @@ msgid ""
|
|||||||
"wallet."
|
"wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:217
|
#: src/webex/pages/confirm-contract.tsx:236
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "The merchant%1$s offers you to purchase:\n"
|
msgid "The merchant%1$s offers you to purchase:\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:238
|
#: src/webex/pages/confirm-contract.tsx:257
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Confirm payment"
|
msgid "Confirm payment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -42,13 +42,13 @@ msgstr ""
|
|||||||
msgid "Exchanges in the wallet:"
|
msgid "Exchanges in the wallet:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:159
|
#: src/webex/pages/confirm-contract.tsx:175
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "You have insufficient funds of the requested currency in your wallet."
|
msgid "You have insufficient funds of the requested currency in your wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#. tslint:disable-next-line:max-line-length
|
#. tslint:disable-next-line:max-line-length
|
||||||
#: src/webex/pages/confirm-contract.tsx:161
|
#: src/webex/pages/confirm-contract.tsx:177
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You do not have any funds from an exchange that is accepted by this "
|
"You do not have any funds from an exchange that is accepted by this "
|
||||||
@ -56,12 +56,12 @@ msgid ""
|
|||||||
"wallet."
|
"wallet."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:217
|
#: src/webex/pages/confirm-contract.tsx:236
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "The merchant%1$s offers you to purchase:\n"
|
msgid "The merchant%1$s offers you to purchase:\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/webex/pages/confirm-contract.tsx:238
|
#: src/webex/pages/confirm-contract.tsx:257
|
||||||
#, c-format
|
#, c-format
|
||||||
msgid "Confirm payment"
|
msgid "Confirm payment"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -38,7 +38,7 @@ export class Denomination {
|
|||||||
/**
|
/**
|
||||||
* Value of one coin of the denomination.
|
* Value of one coin of the denomination.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
value: AmountJson;
|
value: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -50,25 +50,25 @@ export class Denomination {
|
|||||||
/**
|
/**
|
||||||
* Fee for withdrawing.
|
* Fee for withdrawing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
fee_withdraw: AmountJson;
|
fee_withdraw: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for depositing.
|
* Fee for depositing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
fee_deposit: AmountJson;
|
fee_deposit: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refreshing.
|
* Fee for refreshing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
fee_refresh: AmountJson;
|
fee_refresh: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refunding.
|
* Fee for refunding.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
fee_refund: AmountJson;
|
fee_refund: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,7 +151,7 @@ export class Auditor {
|
|||||||
/**
|
/**
|
||||||
* List of signatures for denominations by the auditor.
|
* List of signatures for denominations by the auditor.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(AuditorDenomSig))
|
@Checkable.List(Checkable.Value(() => AuditorDenomSig))
|
||||||
denomination_keys: AuditorDenomSig[];
|
denomination_keys: AuditorDenomSig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -204,7 +204,7 @@ export class PaybackConfirmation {
|
|||||||
* How much will the exchange pay back (needed by wallet in
|
* How much will the exchange pay back (needed by wallet in
|
||||||
* case coin was partially spent and wallet got restored from backup)
|
* case coin was partially spent and wallet got restored from backup)
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -336,7 +336,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* Total amount payable.
|
* Total amount payable.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -360,7 +360,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* Maximum deposit fee covered by the merchant.
|
* Maximum deposit fee covered by the merchant.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
max_fee: AmountJson;
|
max_fee: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -378,7 +378,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* List of accepted exchanges.
|
* List of accepted exchanges.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(ExchangeHandle))
|
@Checkable.List(Checkable.Value(() => ExchangeHandle))
|
||||||
exchanges: ExchangeHandle[];
|
exchanges: ExchangeHandle[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -428,7 +428,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* Maximum wire fee that the merchant agrees to pay for.
|
* Maximum wire fee that the merchant agrees to pay for.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.Value(AmountJson))
|
@Checkable.Optional(Checkable.Value(() => AmountJson))
|
||||||
max_wire_fee?: AmountJson;
|
max_wire_fee?: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -578,7 +578,7 @@ export class TipResponse {
|
|||||||
/**
|
/**
|
||||||
* The order of the signatures matches the planchets list.
|
* The order of the signatures matches the planchets list.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(ReserveSigSingleton))
|
@Checkable.List(Checkable.Value(() => ReserveSigSingleton))
|
||||||
reserve_sigs: ReserveSigSingleton[];
|
reserve_sigs: ReserveSigSingleton[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -620,7 +620,7 @@ export class TipToken {
|
|||||||
/**
|
/**
|
||||||
* Amount of tip.
|
* Amount of tip.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -659,7 +659,7 @@ export class KeysJson {
|
|||||||
/**
|
/**
|
||||||
* List of offered denominations.
|
* List of offered denominations.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(Denomination))
|
@Checkable.List(Checkable.Value(() => Denomination))
|
||||||
denoms: Denomination[];
|
denoms: Denomination[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -671,7 +671,7 @@ export class KeysJson {
|
|||||||
/**
|
/**
|
||||||
* The list of auditors (partially) auditing the exchange.
|
* The list of auditors (partially) auditing the exchange.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(Auditor))
|
@Checkable.List(Checkable.Value(() => Auditor))
|
||||||
auditors: Auditor[];
|
auditors: Auditor[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -683,7 +683,7 @@ export class KeysJson {
|
|||||||
/**
|
/**
|
||||||
* List of paybacks for compromised denominations.
|
* List of paybacks for compromised denominations.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.List(Checkable.Value(Payback)))
|
@Checkable.Optional(Checkable.List(Checkable.Value(() => Payback)))
|
||||||
payback?: Payback[];
|
payback?: Payback[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -715,13 +715,13 @@ export class WireFeesJson {
|
|||||||
/**
|
/**
|
||||||
* Cost of a wire transfer.
|
* Cost of a wire transfer.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
wire_fee: AmountJson;
|
wire_fee: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cost of clising a reserve.
|
* Cost of clising a reserve.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
closing_fee: AmountJson;
|
closing_fee: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -765,7 +765,7 @@ export class WireDetailJson {
|
|||||||
/**
|
/**
|
||||||
* Fees associated with the wire transfer method.
|
* Fees associated with the wire transfer method.
|
||||||
*/
|
*/
|
||||||
@Checkable.List(Checkable.Value(WireFeesJson))
|
@Checkable.List(Checkable.Value(() => WireFeesJson))
|
||||||
fees: WireFeesJson[];
|
fees: WireFeesJson[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -788,3 +788,21 @@ export type WireDetail = object & { type: string };
|
|||||||
export function isWireDetail(x: any): x is WireDetail {
|
export function isWireDetail(x: any): x is WireDetail {
|
||||||
return x && typeof x === "object" && typeof x.type === "string";
|
return x && typeof x === "object" && typeof x.type === "string";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Proposal returned from the contract URL.
|
||||||
|
*/
|
||||||
|
@Checkable.Class({extra: true})
|
||||||
|
export class Proposal {
|
||||||
|
@Checkable.Value(() => ContractTerms)
|
||||||
|
contract_terms: ContractTerms;
|
||||||
|
|
||||||
|
@Checkable.String
|
||||||
|
sig: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify that a value matches the schema of this class and convert it into a
|
||||||
|
* member.
|
||||||
|
*/
|
||||||
|
static checked: (obj: any) => Proposal;
|
||||||
|
}
|
||||||
|
213
src/wallet.ts
213
src/wallet.ts
@ -49,6 +49,8 @@ import * as Amounts from "./amounts";
|
|||||||
|
|
||||||
import URI = require("urijs");
|
import URI = require("urijs");
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
@ -59,7 +61,7 @@ import {
|
|||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeWireFeesRecord,
|
ExchangeWireFeesRecord,
|
||||||
PreCoinRecord,
|
PreCoinRecord,
|
||||||
ProposalRecord,
|
ProposalDownloadRecord,
|
||||||
PurchaseRecord,
|
PurchaseRecord,
|
||||||
RefreshPreCoinRecord,
|
RefreshPreCoinRecord,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
@ -76,9 +78,11 @@ import {
|
|||||||
KeysJson,
|
KeysJson,
|
||||||
PayReq,
|
PayReq,
|
||||||
PaybackConfirmation,
|
PaybackConfirmation,
|
||||||
|
Proposal,
|
||||||
RefundPermission,
|
RefundPermission,
|
||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
TipResponse,
|
TipResponse,
|
||||||
|
TipToken,
|
||||||
WireDetailJson,
|
WireDetailJson,
|
||||||
isWireDetail,
|
isWireDetail,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
@ -109,7 +113,7 @@ interface SpeculativePayData {
|
|||||||
payCoinInfo: PayCoinInfo;
|
payCoinInfo: PayCoinInfo;
|
||||||
exchangeUrl: string;
|
exchangeUrl: string;
|
||||||
proposalId: number;
|
proposalId: number;
|
||||||
proposal: ProposalRecord;
|
proposal: ProposalDownloadRecord;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -624,9 +628,9 @@ export class Wallet {
|
|||||||
* Record all information that is necessary to
|
* Record all information that is necessary to
|
||||||
* pay for a proposal in the wallet's database.
|
* pay for a proposal in the wallet's database.
|
||||||
*/
|
*/
|
||||||
private async recordConfirmPay(proposal: ProposalRecord,
|
private async recordConfirmPay(proposal: ProposalDownloadRecord,
|
||||||
payCoinInfo: PayCoinInfo,
|
payCoinInfo: PayCoinInfo,
|
||||||
chosenExchange: string): Promise<void> {
|
chosenExchange: string): Promise<PurchaseRecord> {
|
||||||
const payReq: PayReq = {
|
const payReq: PayReq = {
|
||||||
coins: payCoinInfo.sigs,
|
coins: payCoinInfo.sigs,
|
||||||
merchant_pub: proposal.contractTerms.merchant_pub,
|
merchant_pub: proposal.contractTerms.merchant_pub,
|
||||||
@ -651,15 +655,42 @@ export class Wallet {
|
|||||||
.finish();
|
.finish();
|
||||||
this.badge.showNotification();
|
this.badge.showNotification();
|
||||||
this.notifier.notify();
|
this.notifier.notify();
|
||||||
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a proposal in the database and return an id for it to
|
* Download a proposal and store it in the database.
|
||||||
* retrieve it later.
|
* Returns an id for it to retrieve it later.
|
||||||
*/
|
*/
|
||||||
async saveProposal(proposal: ProposalRecord): Promise<number> {
|
async downloadProposal(url: string): Promise<number> {
|
||||||
const id = await this.q().putWithResult(Stores.proposals, proposal);
|
const { priv, pub } = await this.cryptoApi.createEddsaKeypair();
|
||||||
|
const parsed_url = new URI(url);
|
||||||
|
url = parsed_url.setQuery({ nonce: pub }).href();
|
||||||
|
console.log("downloading contract from '" + url + "'");
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
resp = await axios.get(url, { validateStatus: (s) => s === 200 });
|
||||||
|
} catch (e) {
|
||||||
|
console.log("contract download failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
console.log("got response", resp);
|
||||||
|
|
||||||
|
const proposal = Proposal.checked(resp.data);
|
||||||
|
|
||||||
|
const contractTermsHash = await this.hashContract(proposal.contract_terms);
|
||||||
|
|
||||||
|
const proposalRecord: ProposalDownloadRecord = {
|
||||||
|
contractTerms: proposal.contract_terms,
|
||||||
|
contractTermsHash,
|
||||||
|
merchantSig: proposal.sig,
|
||||||
|
noncePriv: priv,
|
||||||
|
timestamp: (new Date()).getTime(),
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
|
||||||
|
const id = await this.q().putWithResult(Stores.proposals, proposalRecord);
|
||||||
this.notifier.notify();
|
this.notifier.notify();
|
||||||
if (typeof id !== "number") {
|
if (typeof id !== "number") {
|
||||||
throw Error("db schema wrong");
|
throw Error("db schema wrong");
|
||||||
@ -667,24 +698,50 @@ export class Wallet {
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async submitPay(purchase: PurchaseRecord, sessionId: string | undefined): Promise<ConfirmPayResult> {
|
||||||
|
let resp;
|
||||||
|
const payReq = { ...purchase.payReq, session_id: sessionId };
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
||||||
|
timeout: 5000, /* 5 seconds */
|
||||||
|
validateStatus: (s: number) => s === 200,
|
||||||
|
};
|
||||||
|
resp = await axios.post(purchase.contractTerms.pay_url, payReq, config);
|
||||||
|
} catch (e) {
|
||||||
|
// Gives the user the option to retry / abort and refresh
|
||||||
|
console.log("payment failed", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const merchantResp = resp.data;
|
||||||
|
console.log("got success from pay_url");
|
||||||
|
await this.paymentSucceeded(purchase.contractTermsHash, merchantResp.sig);
|
||||||
|
const fu = new URI(purchase.contractTerms.fulfillment_url);
|
||||||
|
fu.addSearch("order_id", purchase.contractTerms.order_id);
|
||||||
|
if (merchantResp.session_sig) {
|
||||||
|
fu.addSearch("session_sig", merchantResp.session_sig);
|
||||||
|
}
|
||||||
|
const nextUrl = fu.href();
|
||||||
|
return { nextUrl };
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a contract to the wallet and sign coins,
|
* Add a contract to the wallet and sign coins,
|
||||||
* but do not send them yet.
|
* but do not send them yet.
|
||||||
*/
|
*/
|
||||||
async confirmPay(proposalId: number): Promise<ConfirmPayResult> {
|
async confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> {
|
||||||
console.log("executing confirmPay");
|
console.log(`executing confirmPay with proposalId ${proposalId} and sessionId ${sessionId}`);
|
||||||
const proposal: ProposalRecord|undefined = await this.q().get(Stores.proposals, proposalId);
|
const proposal: ProposalDownloadRecord|undefined = await this.q().get(Stores.proposals, proposalId);
|
||||||
|
|
||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
throw Error(`proposal with id ${proposalId} not found`);
|
throw Error(`proposal with id ${proposalId} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
|
let purchase = await this.q().get(Stores.purchases, proposal.contractTermsHash);
|
||||||
|
|
||||||
if (purchase) {
|
if (purchase) {
|
||||||
// Already payed ...
|
return this.submitPay(purchase, sessionId);
|
||||||
return "paid";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await this.getCoinsForPayment({
|
const res = await this.getCoinsForPayment({
|
||||||
@ -702,22 +759,24 @@ export class Wallet {
|
|||||||
console.log("coin selection result", res);
|
console.log("coin selection result", res);
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
|
// Should not happen, since checkPay should be called first
|
||||||
console.log("not confirming payment, insufficient coins");
|
console.log("not confirming payment, insufficient coins");
|
||||||
return "insufficient-balance";
|
throw Error("insufficient balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sd = await this.getSpeculativePayData(proposalId);
|
const sd = await this.getSpeculativePayData(proposalId);
|
||||||
if (!sd) {
|
if (!sd) {
|
||||||
const { exchangeUrl, cds } = res;
|
const { exchangeUrl, cds } = res;
|
||||||
const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds);
|
const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds);
|
||||||
await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl);
|
purchase = await this.recordConfirmPay(proposal, payCoinInfo, exchangeUrl);
|
||||||
} else {
|
} else {
|
||||||
await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);
|
purchase = await this.recordConfirmPay(sd.proposal, sd.payCoinInfo, sd.exchangeUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return "paid";
|
return this.submitPay(purchase, sessionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the speculative pay data, but only if coins have not changed in between.
|
* Get the speculative pay data, but only if coins have not changed in between.
|
||||||
*/
|
*/
|
||||||
@ -803,7 +862,7 @@ export class Wallet {
|
|||||||
* Retrieve information required to pay for a contract, where the
|
* Retrieve information required to pay for a contract, where the
|
||||||
* contract is identified via the fulfillment url.
|
* contract is identified via the fulfillment url.
|
||||||
*/
|
*/
|
||||||
async queryPayment(url: string): Promise<QueryPaymentResult> {
|
async queryPaymentByFulfillmentUrl(url: string): Promise<QueryPaymentResult> {
|
||||||
console.log("query for payment", url);
|
console.log("query for payment", url);
|
||||||
|
|
||||||
const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url);
|
const t = await this.q().getIndexed(Stores.purchases.fulfillmentUrlIndex, url);
|
||||||
@ -823,6 +882,30 @@ export class Wallet {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
payReq: t.payReq,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First fetch information requred to withdraw from the reserve,
|
* First fetch information requred to withdraw from the reserve,
|
||||||
@ -2020,7 +2103,7 @@ export class Wallet {
|
|||||||
|
|
||||||
// FIXME: do pagination instead of generating the full history
|
// FIXME: do pagination instead of generating the full history
|
||||||
|
|
||||||
const proposals = await this.q().iter<ProposalRecord>(Stores.proposals).toArray();
|
const proposals = await this.q().iter<ProposalDownloadRecord>(Stores.proposals).toArray();
|
||||||
for (const p of proposals) {
|
for (const p of proposals) {
|
||||||
history.push({
|
history.push({
|
||||||
detail: {
|
detail: {
|
||||||
@ -2111,7 +2194,7 @@ export class Wallet {
|
|||||||
return denoms;
|
return denoms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProposal(proposalId: number): Promise<ProposalRecord|undefined> {
|
async getProposal(proposalId: number): Promise<ProposalDownloadRecord|undefined> {
|
||||||
const proposal = await this.q().get(Stores.proposals, proposalId);
|
const proposal = await this.q().get(Stores.proposals, proposalId);
|
||||||
return proposal;
|
return proposal;
|
||||||
}
|
}
|
||||||
@ -2162,18 +2245,6 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a nonce in form of an EdDSA public key.
|
|
||||||
* Store the private key in our DB, so we can prove ownership.
|
|
||||||
*/
|
|
||||||
async generateNonce(): Promise<string> {
|
|
||||||
const {priv, pub} = await this.cryptoApi.createEddsaKeypair();
|
|
||||||
await this.q()
|
|
||||||
.put(Stores.nonces, {priv, pub})
|
|
||||||
.finish();
|
|
||||||
return pub;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> {
|
async getCurrencyRecord(currency: string): Promise<CurrencyRecord|undefined> {
|
||||||
return this.q().get(Stores.currencies, currency);
|
return this.q().get(Stores.currencies, currency);
|
||||||
}
|
}
|
||||||
@ -2466,10 +2537,25 @@ export class Wallet {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async acceptRefund(refundPermissions: RefundPermission[]): Promise<void> {
|
async acceptRefund(refundUrl: string): Promise<string> {
|
||||||
|
console.log("processing refund");
|
||||||
|
let resp;
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
validateStatus: (s: number) => s === 200,
|
||||||
|
};
|
||||||
|
resp = await axios.get(refundUrl, config);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("error downloading refund permission", e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: validate schema
|
||||||
|
const refundPermissions = resp.data;
|
||||||
|
|
||||||
if (!refundPermissions.length) {
|
if (!refundPermissions.length) {
|
||||||
console.warn("got empty refund list");
|
console.warn("got empty refund list");
|
||||||
return;
|
throw Error("empty refund");
|
||||||
}
|
}
|
||||||
const hc = refundPermissions[0].h_contract_terms;
|
const hc = refundPermissions[0].h_contract_terms;
|
||||||
if (!hc) {
|
if (!hc) {
|
||||||
@ -2513,6 +2599,8 @@ export class Wallet {
|
|||||||
|
|
||||||
// Start submitting it but don't wait for it here.
|
// Start submitting it but don't wait for it here.
|
||||||
this.submitRefunds(hc);
|
this.submitRefunds(hc);
|
||||||
|
|
||||||
|
return refundPermissions[0].h_contract_terms;
|
||||||
}
|
}
|
||||||
|
|
||||||
async submitRefunds(contractTermsHash: string): Promise<void> {
|
async submitRefunds(contractTermsHash: string): Promise<void> {
|
||||||
@ -2646,6 +2734,54 @@ export class Wallet {
|
|||||||
return planchetDetail;
|
return planchetDetail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async processTip(tipToken: TipToken): Promise<void> {
|
||||||
|
console.log("got tip token", tipToken);
|
||||||
|
|
||||||
|
const deadlineSec = getTalerStampSec(tipToken.expiration);
|
||||||
|
if (!deadlineSec) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (!planchets) {
|
||||||
|
console.log("failed tip", walletResp);
|
||||||
|
throw Error("processing tip failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let merchantResp;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const config = {
|
||||||
|
validateStatus: (s: number) => s === 200,
|
||||||
|
};
|
||||||
|
const req = { planchets, 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a merchant's response to a tip pickup and start withdrawing the coins.
|
* Accept a merchant's response to a tip pickup and start withdrawing the coins.
|
||||||
* These coins will not appear in the wallet yet.
|
* These coins will not appear in the wallet yet.
|
||||||
@ -2725,6 +2861,11 @@ export class Wallet {
|
|||||||
return tipStatus;
|
return tipStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
getNextUrlFromResourceUrl(resourceUrl: string): string | undefined {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove unreferenced / expired data from the wallet's database
|
* Remove unreferenced / expired data from the wallet's database
|
||||||
* based on the current system time.
|
* based on the current system time.
|
||||||
@ -2745,7 +2886,7 @@ export class Wallet {
|
|||||||
};
|
};
|
||||||
await this.q().deleteIf(Stores.reserves, gcReserve).finish();
|
await this.q().deleteIf(Stores.reserves, gcReserve).finish();
|
||||||
|
|
||||||
const gcProposal = (d: ProposalRecord, n: number) => {
|
const gcProposal = (d: ProposalDownloadRecord, n: number) => {
|
||||||
// Delete proposal after 60 minutes or 5 minutes before pay deadline,
|
// Delete proposal after 60 minutes or 5 minutes before pay deadline,
|
||||||
// whatever comes first.
|
// whatever comes first.
|
||||||
const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
|
const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
|
||||||
|
@ -246,9 +246,11 @@ export interface CheckPayResult {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possible results for confirmPay.
|
* Result for confirmPay
|
||||||
*/
|
*/
|
||||||
export type ConfirmPayResult = "paid" | "insufficient-balance";
|
export interface ConfirmPayResult {
|
||||||
|
nextUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,6 +301,7 @@ export interface QueryPaymentFound {
|
|||||||
found: true;
|
found: true;
|
||||||
contractTermsHash: string;
|
contractTermsHash: string;
|
||||||
contractTerms: ContractTerms;
|
contractTerms: ContractTerms;
|
||||||
|
lastSessionSig?: string;
|
||||||
payReq: PayReq;
|
payReq: PayReq;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,7 +332,7 @@ export class CreateReserveRequest {
|
|||||||
/**
|
/**
|
||||||
* The initial amount for the reserve.
|
* The initial amount for the reserve.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -380,7 +383,7 @@ export class ReturnCoinsRequest {
|
|||||||
/**
|
/**
|
||||||
* The amount to wire.
|
* The amount to wire.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(AmountJson)
|
@Checkable.Value(() => AmountJson)
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -511,7 +514,7 @@ export class ProcessTipResponseRequest {
|
|||||||
/**
|
/**
|
||||||
* Tip response from the merchant.
|
* Tip response from the merchant.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(TipResponse)
|
@Checkable.Value(() => TipResponse)
|
||||||
tipResponse: TipResponse;
|
tipResponse: TipResponse;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -543,7 +546,7 @@ export class GetTipPlanchetsRequest {
|
|||||||
/**
|
/**
|
||||||
* Amount of the tip.
|
* Amount of the tip.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.Value(AmountJson))
|
@Checkable.Optional(Checkable.Value(() => AmountJson))
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -44,10 +44,6 @@ export interface MessageMap {
|
|||||||
};
|
};
|
||||||
response: void;
|
response: void;
|
||||||
};
|
};
|
||||||
"get-tab-cookie": {
|
|
||||||
request: { }
|
|
||||||
response: any;
|
|
||||||
};
|
|
||||||
"ping": {
|
"ping": {
|
||||||
request: { };
|
request: { };
|
||||||
response: void;
|
response: void;
|
||||||
@ -67,12 +63,8 @@ export interface MessageMap {
|
|||||||
request: { reservePub: string };
|
request: { reservePub: string };
|
||||||
response: void;
|
response: void;
|
||||||
};
|
};
|
||||||
"generate-nonce": {
|
|
||||||
request: { }
|
|
||||||
response: string;
|
|
||||||
};
|
|
||||||
"confirm-pay": {
|
"confirm-pay": {
|
||||||
request: { proposalId: number; };
|
request: { proposalId: number; sessionId?: string };
|
||||||
response: walletTypes.ConfirmPayResult;
|
response: walletTypes.ConfirmPayResult;
|
||||||
};
|
};
|
||||||
"check-pay": {
|
"check-pay": {
|
||||||
@ -95,10 +87,6 @@ export interface MessageMap {
|
|||||||
request: { contract: object };
|
request: { contract: object };
|
||||||
response: string;
|
response: string;
|
||||||
};
|
};
|
||||||
"save-proposal": {
|
|
||||||
request: { proposal: dbTypes.ProposalRecord };
|
|
||||||
response: void;
|
|
||||||
};
|
|
||||||
"reserve-creation-info": {
|
"reserve-creation-info": {
|
||||||
request: { baseUrl: string, amount: AmountJson };
|
request: { baseUrl: string, amount: AmountJson };
|
||||||
response: walletTypes.ReserveCreationInfo;
|
response: walletTypes.ReserveCreationInfo;
|
||||||
@ -109,7 +97,7 @@ export interface MessageMap {
|
|||||||
};
|
};
|
||||||
"get-proposal": {
|
"get-proposal": {
|
||||||
request: { proposalId: number };
|
request: { proposalId: number };
|
||||||
response: dbTypes.ProposalRecord | undefined;
|
response: dbTypes.ProposalDownloadRecord | undefined;
|
||||||
};
|
};
|
||||||
"get-coins": {
|
"get-coins": {
|
||||||
request: { exchangeBaseUrl: string };
|
request: { exchangeBaseUrl: string };
|
||||||
@ -155,14 +143,6 @@ export interface MessageMap {
|
|||||||
request: { coinPub: string };
|
request: { coinPub: string };
|
||||||
response: void;
|
response: void;
|
||||||
};
|
};
|
||||||
"payment-failed": {
|
|
||||||
request: { contractTermsHash: string };
|
|
||||||
response: void;
|
|
||||||
};
|
|
||||||
"payment-succeeded": {
|
|
||||||
request: { contractTermsHash: string; merchantSig: string };
|
|
||||||
response: void;
|
|
||||||
};
|
|
||||||
"check-upgrade": {
|
"check-upgrade": {
|
||||||
request: { };
|
request: { };
|
||||||
response: void;
|
response: void;
|
||||||
@ -183,10 +163,6 @@ export interface MessageMap {
|
|||||||
request: { reportUid: string };
|
request: { reportUid: string };
|
||||||
response: void;
|
response: void;
|
||||||
};
|
};
|
||||||
"accept-refund": {
|
|
||||||
request: any;
|
|
||||||
response: void;
|
|
||||||
};
|
|
||||||
"get-purchase": {
|
"get-purchase": {
|
||||||
request: any;
|
request: any;
|
||||||
response: void;
|
response: void;
|
||||||
@ -215,6 +191,14 @@ export interface MessageMap {
|
|||||||
request: { };
|
request: { };
|
||||||
response: void;
|
response: void;
|
||||||
};
|
};
|
||||||
|
"taler-pay": {
|
||||||
|
request: any;
|
||||||
|
response: void;
|
||||||
|
};
|
||||||
|
"download-proposal": {
|
||||||
|
request: any;
|
||||||
|
response: void;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -28,13 +28,6 @@ import URI = require("urijs");
|
|||||||
|
|
||||||
import wxApi = require("./wxApi");
|
import wxApi = require("./wxApi");
|
||||||
|
|
||||||
import { getTalerStampSec } from "../helpers";
|
|
||||||
import { TipToken } from "../talerTypes";
|
|
||||||
import { QueryPaymentResult } from "../walletTypes";
|
|
||||||
|
|
||||||
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
declare var cloneInto: any;
|
declare var cloneInto: any;
|
||||||
|
|
||||||
let logVerbose: boolean = false;
|
let logVerbose: boolean = false;
|
||||||
@ -103,42 +96,6 @@ function setStyles(installed: boolean) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handlePaymentResponse(maybeFoundResponse: QueryPaymentResult) {
|
|
||||||
if (!maybeFoundResponse.found) {
|
|
||||||
console.log("pay-failed", {hint: "payment not found in the wallet"});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const walletResp = maybeFoundResponse;
|
|
||||||
|
|
||||||
logVerbose && console.log("handling taler-notify-payment: ", walletResp);
|
|
||||||
let resp;
|
|
||||||
try {
|
|
||||||
const config = {
|
|
||||||
headers: { "Content-Type": "application/json;charset=UTF-8" },
|
|
||||||
timeout: 5000, /* 5 seconds */
|
|
||||||
validateStatus: (s: number) => s === 200,
|
|
||||||
};
|
|
||||||
resp = await axios.post(walletResp.contractTerms.pay_url, walletResp.payReq, config);
|
|
||||||
} catch (e) {
|
|
||||||
// Gives the user the option to retry / abort and refresh
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
contractTerms: walletResp.contractTerms,
|
|
||||||
message: e.message,
|
|
||||||
name: "pay-post-failed",
|
|
||||||
response: e.response,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
const merchantResp = resp.data;
|
|
||||||
logVerbose && console.log("got success from pay_url");
|
|
||||||
await wxApi.paymentSucceeded(walletResp.contractTermsHash, merchantResp.sig);
|
|
||||||
const nextUrl = walletResp.contractTerms.fulfillment_url;
|
|
||||||
logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl);
|
|
||||||
window.location.href = nextUrl;
|
|
||||||
window.location.reload(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onceOnComplete(cb: () => void) {
|
function onceOnComplete(cb: () => void) {
|
||||||
if (document.readyState === "complete") {
|
if (document.readyState === "complete") {
|
||||||
cb();
|
cb();
|
||||||
@ -153,234 +110,29 @@ function onceOnComplete(cb: () => void) {
|
|||||||
|
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
// Only place where we don't use the nicer RPC wrapper, since the wallet
|
onceOnComplete(() => {
|
||||||
// backend might not be ready (during install, upgrade, etc.)
|
if (document.documentElement.getAttribute("data-taler-nojs")) {
|
||||||
chrome.runtime.sendMessage({type: "get-tab-cookie"}, (resp) => {
|
initStyle();
|
||||||
logVerbose && console.log("got response for get-tab-cookie");
|
setStyles(true);
|
||||||
if (chrome.runtime.lastError) {
|
|
||||||
logVerbose && console.log("extension not yet ready");
|
|
||||||
window.setTimeout(init, 200);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
onceOnComplete(() => {
|
});
|
||||||
if (document.documentElement.getAttribute("data-taler-nojs")) {
|
registerHandlers();
|
||||||
initStyle();
|
// Hack to know when the extension is unloaded
|
||||||
setStyles(true);
|
const port = chrome.runtime.connect();
|
||||||
}
|
|
||||||
});
|
|
||||||
registerHandlers();
|
|
||||||
// Hack to know when the extension is unloaded
|
|
||||||
const port = chrome.runtime.connect();
|
|
||||||
|
|
||||||
port.onDisconnect.addListener(() => {
|
port.onDisconnect.addListener(() => {
|
||||||
logVerbose && console.log("chrome runtime disconnected, removing handlers");
|
logVerbose && console.log("chrome runtime disconnected, removing handlers");
|
||||||
if (document.documentElement.getAttribute("data-taler-nojs")) {
|
if (document.documentElement.getAttribute("data-taler-nojs")) {
|
||||||
setStyles(false);
|
setStyles(false);
|
||||||
}
|
}
|
||||||
for (const handler of handlers) {
|
for (const handler of handlers) {
|
||||||
document.removeEventListener(handler.type, handler.listener);
|
document.removeEventListener(handler.type, handler.listener);
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (resp && resp.type === "pay") {
|
|
||||||
logVerbose && console.log("doing taler.pay with", resp.payDetail);
|
|
||||||
talerPay(resp.payDetail).then(handlePaymentResponse);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
|
type HandlerFn = (detail: any, sendResponse: (msg: any) => void) => void;
|
||||||
|
|
||||||
async function downloadContract(url: string, nonce: string): Promise<any> {
|
|
||||||
const parsed_url = new URI(url);
|
|
||||||
url = parsed_url.setQuery({nonce}).href();
|
|
||||||
console.log("downloading contract from '" + url + "'");
|
|
||||||
let resp;
|
|
||||||
try {
|
|
||||||
resp = await axios.get(url, { validateStatus: (s) => s === 200 });
|
|
||||||
} catch (e) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: e.message,
|
|
||||||
name: "contract-download-failed",
|
|
||||||
response: e.response,
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
console.log("got response", resp);
|
|
||||||
return resp.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processProposal(proposal: any) {
|
|
||||||
|
|
||||||
if (!proposal.contract_terms) {
|
|
||||||
console.error("field proposal.contract_terms field missing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const contractHash = await wxApi.hashContract(proposal.contract_terms);
|
|
||||||
|
|
||||||
const proposalId = await wxApi.saveProposal({
|
|
||||||
contractTerms: proposal.contract_terms,
|
|
||||||
contractTermsHash: contractHash,
|
|
||||||
merchantSig: proposal.sig,
|
|
||||||
timestamp: (new Date()).getTime(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
|
|
||||||
const params = {
|
|
||||||
proposalId: proposalId.toString(),
|
|
||||||
};
|
|
||||||
const target = uri.query(params).href();
|
|
||||||
document.location.replace(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a payment request (coming either from an HTTP 402 or
|
|
||||||
* the JS wallet API).
|
|
||||||
*/
|
|
||||||
function talerPay(msg: any): Promise<any> {
|
|
||||||
// Use a promise directly instead of of an async
|
|
||||||
// function since some paths never resolve the promise.
|
|
||||||
return new Promise(async(resolve, reject) => {
|
|
||||||
if (msg.tip) {
|
|
||||||
const tipToken = TipToken.checked(JSON.parse(msg.tip));
|
|
||||||
|
|
||||||
console.log("got tip token", tipToken);
|
|
||||||
|
|
||||||
const deadlineSec = getTalerStampSec(tipToken.expiration);
|
|
||||||
if (!deadlineSec) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: "invalid expiration",
|
|
||||||
name: "tipping-failed",
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const merchantDomain = new URI(document.location.href).origin();
|
|
||||||
let walletResp;
|
|
||||||
try {
|
|
||||||
walletResp = await wxApi.getTipPlanchets(merchantDomain,
|
|
||||||
tipToken.tip_id,
|
|
||||||
tipToken.amount,
|
|
||||||
deadlineSec,
|
|
||||||
tipToken.exchange_url,
|
|
||||||
tipToken.next_url);
|
|
||||||
} catch (e) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: e.message,
|
|
||||||
name: "tipping-failed",
|
|
||||||
response: e.response,
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
const planchets = walletResp;
|
|
||||||
|
|
||||||
if (!planchets) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
detail: walletResp,
|
|
||||||
message: "processing tip failed",
|
|
||||||
name: "tipping-failed",
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let merchantResp;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = {
|
|
||||||
validateStatus: (s: number) => s === 200,
|
|
||||||
};
|
|
||||||
const req = { planchets, tip_id: tipToken.tip_id };
|
|
||||||
merchantResp = await axios.post(tipToken.pickup_url, req, config);
|
|
||||||
} catch (e) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: e.message,
|
|
||||||
name: "tipping-failed",
|
|
||||||
response: e.response,
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
wxApi.processTipResponse(merchantDomain, tipToken.tip_id, merchantResp.data);
|
|
||||||
} catch (e) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: e.message,
|
|
||||||
name: "tipping-failed",
|
|
||||||
response: e.response,
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Go to tip dialog page, where the user can confirm the tip or
|
|
||||||
// decline if they are not happy with the exchange.
|
|
||||||
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();
|
|
||||||
window.location.href = redirectUrl;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.refund_url) {
|
|
||||||
console.log("processing refund");
|
|
||||||
let resp;
|
|
||||||
try {
|
|
||||||
const config = {
|
|
||||||
validateStatus: (s: number) => s === 200,
|
|
||||||
};
|
|
||||||
resp = await axios.get(msg.refund_url, config);
|
|
||||||
} catch (e) {
|
|
||||||
wxApi.logAndDisplayError({
|
|
||||||
message: e.message,
|
|
||||||
name: "refund-download-failed",
|
|
||||||
response: e.response,
|
|
||||||
sameTab: true,
|
|
||||||
});
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
await wxApi.acceptRefund(resp.data);
|
|
||||||
const hc = resp.data.refund_permissions[0].h_contract_terms;
|
|
||||||
document.location.href = chrome.extension.getURL(`/src/webex/pages/refund.html?contractTermsHash=${hc}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// current URL without fragment
|
|
||||||
const url = new URI(document.location.href).fragment("").href();
|
|
||||||
const res = await wxApi.queryPayment(url);
|
|
||||||
logVerbose && console.log("taler-pay: got response", res);
|
|
||||||
if (res && res.found && res.payReq) {
|
|
||||||
resolve(res);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (msg.contract_url) {
|
|
||||||
const nonce = await wxApi.generateNonce();
|
|
||||||
const proposal = await downloadContract(msg.contract_url, nonce);
|
|
||||||
if (proposal.contract_terms.nonce !== nonce) {
|
|
||||||
console.error("stale contract");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await processProposal(proposal);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.offer_url) {
|
|
||||||
document.location.href = msg.offer_url;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("can't proceed with payment, no way to get contract specified");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function registerHandlers() {
|
function registerHandlers() {
|
||||||
/**
|
/**
|
||||||
@ -457,7 +209,7 @@ function registerHandlers() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
addHandler("taler-pay", async(msg: any, sendResponse: any) => {
|
addHandler("taler-pay", async(msg: any, sendResponse: any) => {
|
||||||
const resp = await talerPay(msg);
|
const resp = await wxApi.talerPay(msg);
|
||||||
sendResponse(resp);
|
sendResponse(resp);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import * as i18n from "../../i18n";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ProposalRecord,
|
ProposalDownloadRecord,
|
||||||
} from "../../dbTypes";
|
} from "../../dbTypes";
|
||||||
import { ContractTerms } from "../../talerTypes";
|
import { ContractTerms } from "../../talerTypes";
|
||||||
import {
|
import {
|
||||||
@ -102,12 +102,15 @@ class Details extends React.Component<DetailProps, DetailState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ContractPromptProps {
|
interface ContractPromptProps {
|
||||||
proposalId: number;
|
proposalId?: number;
|
||||||
|
contractUrl?: string;
|
||||||
|
sessionId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ContractPromptState {
|
interface ContractPromptState {
|
||||||
proposal: ProposalRecord|null;
|
proposalId: number | undefined;
|
||||||
error: string|null;
|
proposal: ProposalDownloadRecord | null;
|
||||||
|
error: string | null;
|
||||||
payDisabled: boolean;
|
payDisabled: boolean;
|
||||||
alreadyPaid: boolean;
|
alreadyPaid: boolean;
|
||||||
exchanges: null|ExchangeRecord[];
|
exchanges: null|ExchangeRecord[];
|
||||||
@ -130,6 +133,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
holdCheck: false,
|
holdCheck: false,
|
||||||
payDisabled: true,
|
payDisabled: true,
|
||||||
proposal: null,
|
proposal: null,
|
||||||
|
proposalId: props.proposalId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,11 +146,19 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
}
|
}
|
||||||
|
|
||||||
async update() {
|
async update() {
|
||||||
const proposal = await wxApi.getProposal(this.props.proposalId);
|
let proposalId = this.props.proposalId;
|
||||||
this.setState({proposal} as any);
|
if (proposalId === undefined) {
|
||||||
|
if (this.props.contractUrl === undefined) {
|
||||||
|
// Nothing we can do ...
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
proposalId = await wxApi.downloadProposal(this.props.contractUrl);
|
||||||
|
}
|
||||||
|
const proposal = await wxApi.getProposal(proposalId);
|
||||||
|
this.setState({ proposal, proposalId });
|
||||||
this.checkPayment();
|
this.checkPayment();
|
||||||
const exchanges = await wxApi.getExchanges();
|
const exchanges = await wxApi.getExchanges();
|
||||||
this.setState({exchanges} as any);
|
this.setState({ exchanges });
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkPayment() {
|
async checkPayment() {
|
||||||
@ -154,7 +166,11 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
if (this.state.holdCheck) {
|
if (this.state.holdCheck) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payStatus = await wxApi.checkPay(this.props.proposalId);
|
const proposalId = this.state.proposalId;
|
||||||
|
if (proposalId === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payStatus = await wxApi.checkPay(proposalId);
|
||||||
if (payStatus.status === "insufficient-balance") {
|
if (payStatus.status === "insufficient-balance") {
|
||||||
const msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`;
|
const msgInsufficient = i18n.str`You have insufficient funds of the requested currency in your wallet.`;
|
||||||
// tslint:disable-next-line:max-line-length
|
// tslint:disable-next-line:max-line-length
|
||||||
@ -163,18 +179,18 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub);
|
const acceptedExchangePubs = this.state.proposal.contractTerms.exchanges.map((e) => e.master_pub);
|
||||||
const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0);
|
const ex = this.state.exchanges.find((e) => acceptedExchangePubs.indexOf(e.masterPublicKey) >= 0);
|
||||||
if (ex) {
|
if (ex) {
|
||||||
this.setState({error: msgInsufficient});
|
this.setState({ error: msgInsufficient });
|
||||||
} else {
|
} else {
|
||||||
this.setState({error: msgNoMatch});
|
this.setState({ error: msgNoMatch });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.setState({error: msgInsufficient});
|
this.setState({ error: msgInsufficient });
|
||||||
}
|
}
|
||||||
this.setState({payDisabled: true});
|
this.setState({ payDisabled: true });
|
||||||
} else if (payStatus.status === "paid") {
|
} else if (payStatus.status === "paid") {
|
||||||
this.setState({alreadyPaid: true, payDisabled: false, error: null, payStatus});
|
this.setState({ alreadyPaid: true, payDisabled: false, error: null, payStatus });
|
||||||
} else {
|
} else {
|
||||||
this.setState({payDisabled: false, error: null, payStatus});
|
this.setState({ payDisabled: false, error: null, payStatus });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,21 +200,24 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
if (!proposal) {
|
if (!proposal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const payStatus = await wxApi.confirmPay(this.props.proposalId);
|
const proposalId = proposal.id;
|
||||||
switch (payStatus) {
|
if (proposalId === undefined) {
|
||||||
case "insufficient-balance":
|
console.error("proposal has no id");
|
||||||
this.checkPayment();
|
return;
|
||||||
return;
|
|
||||||
case "paid":
|
|
||||||
console.log("contract", proposal.contractTerms);
|
|
||||||
document.location.href = proposal.contractTerms.fulfillment_url;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
this.setState({holdCheck: true});
|
const payResult = await wxApi.confirmPay(proposalId, this.props.sessionId);
|
||||||
|
document.location.href = payResult.nextUrl;
|
||||||
|
this.setState({ holdCheck: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (this.props.contractUrl === undefined && this.props.proposalId === undefined) {
|
||||||
|
return <span>Error: either contractUrl or proposalId must be given</span>;
|
||||||
|
}
|
||||||
|
if (this.state.proposalId === undefined) {
|
||||||
|
return <span>Downloading contract terms</span>;
|
||||||
|
}
|
||||||
if (!this.state.proposal) {
|
if (!this.state.proposal) {
|
||||||
return <span>...</span>;
|
return <span>...</span>;
|
||||||
}
|
}
|
||||||
@ -255,8 +274,18 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
const url = new URI(document.location.href);
|
const url = new URI(document.location.href);
|
||||||
const query: any = URI.parseQuery(url.query());
|
const query: any = URI.parseQuery(url.query());
|
||||||
const proposalId = JSON.parse(query.proposalId);
|
|
||||||
|
|
||||||
ReactDOM.render(<ContractPrompt proposalId={proposalId}/>, document.getElementById(
|
let proposalId;
|
||||||
"contract")!);
|
try {
|
||||||
|
proposalId = JSON.parse(query.proposalId);
|
||||||
|
} catch {
|
||||||
|
// ignore error
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionId = query.sessionId;
|
||||||
|
const contractUrl = query.contractUrl;
|
||||||
|
|
||||||
|
ReactDOM.render(
|
||||||
|
<ContractPrompt {...{ proposalId, contractUrl, sessionId }}/>,
|
||||||
|
document.getElementById("contract")!);
|
||||||
});
|
});
|
||||||
|
@ -217,8 +217,8 @@ export function checkPay(proposalId: number): Promise<CheckPayResult> {
|
|||||||
/**
|
/**
|
||||||
* Pay for a proposal.
|
* Pay for a proposal.
|
||||||
*/
|
*/
|
||||||
export function confirmPay(proposalId: number): Promise<ConfirmPayResult> {
|
export function confirmPay(proposalId: number, sessionId: string | undefined): Promise<ConfirmPayResult> {
|
||||||
return callBackend("confirm-pay", { proposalId });
|
return callBackend("confirm-pay", { proposalId, sessionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -228,15 +228,6 @@ export function hashContract(contract: object): Promise<string> {
|
|||||||
return callBackend("hash-contract", { contract });
|
return callBackend("hash-contract", { contract });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a proposal in the wallet. Returns the proposal id that
|
|
||||||
* the proposal is stored under.
|
|
||||||
*/
|
|
||||||
export function saveProposal(proposal: any): Promise<number> {
|
|
||||||
return callBackend("save-proposal", { proposal });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark a reserve as confirmed.
|
* Mark a reserve as confirmed.
|
||||||
*/
|
*/
|
||||||
@ -251,36 +242,6 @@ export function queryPayment(url: string): Promise<QueryPaymentResult> {
|
|||||||
return callBackend("query-payment", { url });
|
return callBackend("query-payment", { url });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a payment as succeeded.
|
|
||||||
*/
|
|
||||||
export function paymentSucceeded(contractTermsHash: string, merchantSig: string): Promise<void> {
|
|
||||||
return callBackend("payment-succeeded", { contractTermsHash, merchantSig });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark a payment as succeeded.
|
|
||||||
*/
|
|
||||||
export function paymentFailed(contractTermsHash: string): Promise<void> {
|
|
||||||
return callBackend("payment-failed", { contractTermsHash });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the payment cookie for the current tab, or undefined if no payment
|
|
||||||
* cookie was set.
|
|
||||||
*/
|
|
||||||
export function getTabCookie(): Promise<any> {
|
|
||||||
return callBackend("get-tab-cookie", { });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a contract nonce (EdDSA key pair), store it in the wallet's
|
|
||||||
* database and return the public key.
|
|
||||||
*/
|
|
||||||
export function generateNonce(): Promise<string> {
|
|
||||||
return callBackend("generate-nonce", { });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check upgrade information
|
* Check upgrade information
|
||||||
*/
|
*/
|
||||||
@ -344,12 +305,6 @@ export function getReport(reportUid: string): Promise<any> {
|
|||||||
return callBackend("get-report", { reportUid });
|
return callBackend("get-report", { reportUid });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply a refund that we got from the merchant.
|
|
||||||
*/
|
|
||||||
export function acceptRefund(refundData: any): Promise<number> {
|
|
||||||
return callBackend("accept-refund", refundData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Look up a purchase in the wallet database from
|
* Look up a purchase in the wallet database from
|
||||||
@ -407,3 +362,17 @@ export function processTipResponse(merchantDomain: string, tipId: string, tipRes
|
|||||||
export function clearNotification(): Promise<void> {
|
export function clearNotification(): Promise<void> {
|
||||||
return callBackend("clear-notification", { });
|
return callBackend("clear-notification", { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger taler payment processing (for payment, tipping and refunds).
|
||||||
|
*/
|
||||||
|
export function talerPay(msg: any): Promise<void> {
|
||||||
|
return callBackend("taler-pay", msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download a contract.
|
||||||
|
*/
|
||||||
|
export function downloadProposal(url: string): Promise<number> {
|
||||||
|
return callBackend("download-proposal", { url });
|
||||||
|
}
|
||||||
|
@ -33,7 +33,6 @@ import {
|
|||||||
|
|
||||||
import { AmountJson } from "../amounts";
|
import { AmountJson } from "../amounts";
|
||||||
|
|
||||||
import { ProposalRecord } from "../dbTypes";
|
|
||||||
import {
|
import {
|
||||||
AcceptTipRequest,
|
AcceptTipRequest,
|
||||||
ConfirmReserveRequest,
|
ConfirmReserveRequest,
|
||||||
@ -41,6 +40,7 @@ import {
|
|||||||
GetTipPlanchetsRequest,
|
GetTipPlanchetsRequest,
|
||||||
Notifier,
|
Notifier,
|
||||||
ProcessTipResponseRequest,
|
ProcessTipResponseRequest,
|
||||||
|
QueryPaymentFound,
|
||||||
ReturnCoinsRequest,
|
ReturnCoinsRequest,
|
||||||
TipStatusRequest,
|
TipStatusRequest,
|
||||||
} from "../walletTypes";
|
} from "../walletTypes";
|
||||||
@ -62,6 +62,7 @@ import * as wxApi from "./wxApi";
|
|||||||
import URI = require("urijs");
|
import URI = require("urijs");
|
||||||
import Port = chrome.runtime.Port;
|
import Port = chrome.runtime.Port;
|
||||||
import MessageSender = chrome.runtime.MessageSender;
|
import MessageSender = chrome.runtime.MessageSender;
|
||||||
|
import { TipToken } from "../talerTypes";
|
||||||
|
|
||||||
|
|
||||||
const DB_NAME = "taler";
|
const DB_NAME = "taler";
|
||||||
@ -93,15 +94,6 @@ function handleMessage(sender: MessageSender,
|
|||||||
const db = needsWallet().db;
|
const db = needsWallet().db;
|
||||||
return importDb(db, detail.dump);
|
return importDb(db, detail.dump);
|
||||||
}
|
}
|
||||||
case "get-tab-cookie": {
|
|
||||||
if (!sender || !sender.tab || !sender.tab.id) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
const id: number = sender.tab.id;
|
|
||||||
const info: any = paymentRequestCookies[id] as any;
|
|
||||||
delete paymentRequestCookies[id];
|
|
||||||
return Promise.resolve(info);
|
|
||||||
}
|
|
||||||
case "ping": {
|
case "ping": {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
@ -138,14 +130,11 @@ function handleMessage(sender: MessageSender,
|
|||||||
const req = ConfirmReserveRequest.checked(d);
|
const req = ConfirmReserveRequest.checked(d);
|
||||||
return needsWallet().confirmReserve(req);
|
return needsWallet().confirmReserve(req);
|
||||||
}
|
}
|
||||||
case "generate-nonce": {
|
|
||||||
return needsWallet().generateNonce();
|
|
||||||
}
|
|
||||||
case "confirm-pay": {
|
case "confirm-pay": {
|
||||||
if (typeof detail.proposalId !== "number") {
|
if (typeof detail.proposalId !== "number") {
|
||||||
throw Error("proposalId must be number");
|
throw Error("proposalId must be number");
|
||||||
}
|
}
|
||||||
return needsWallet().confirmPay(detail.proposalId);
|
return needsWallet().confirmPay(detail.proposalId, detail.sessionId);
|
||||||
}
|
}
|
||||||
case "check-pay": {
|
case "check-pay": {
|
||||||
if (typeof detail.proposalId !== "number") {
|
if (typeof detail.proposalId !== "number") {
|
||||||
@ -166,7 +155,7 @@ function handleMessage(sender: MessageSender,
|
|||||||
return Promise.resolve(msg);
|
return Promise.resolve(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return needsWallet().queryPayment(detail.url);
|
return needsWallet().queryPaymentByFulfillmentUrl(detail.url);
|
||||||
}
|
}
|
||||||
case "exchange-info": {
|
case "exchange-info": {
|
||||||
if (!detail.baseUrl) {
|
if (!detail.baseUrl) {
|
||||||
@ -188,11 +177,6 @@ function handleMessage(sender: MessageSender,
|
|||||||
return hash;
|
return hash;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
case "save-proposal": {
|
|
||||||
console.log("handling save-proposal", detail);
|
|
||||||
const checkedRecord = ProposalRecord.checked(detail.proposal);
|
|
||||||
return needsWallet().saveProposal(checkedRecord);
|
|
||||||
}
|
|
||||||
case "reserve-creation-info": {
|
case "reserve-creation-info": {
|
||||||
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
||||||
return Promise.resolve({ error: "bad url" });
|
return Promise.resolve({ error: "bad url" });
|
||||||
@ -261,25 +245,6 @@ function handleMessage(sender: MessageSender,
|
|||||||
}
|
}
|
||||||
return needsWallet().payback(detail.coinPub);
|
return needsWallet().payback(detail.coinPub);
|
||||||
}
|
}
|
||||||
case "payment-failed": {
|
|
||||||
// For now we just update exchanges (maybe the exchange did something
|
|
||||||
// wrong and the keys were messed up).
|
|
||||||
// FIXME: in the future we should look at what actually went wrong.
|
|
||||||
console.error("payment reported as failed");
|
|
||||||
needsWallet().updateExchanges();
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
case "payment-succeeded": {
|
|
||||||
const contractTermsHash = detail.contractTermsHash;
|
|
||||||
const merchantSig = detail.merchantSig;
|
|
||||||
if (!contractTermsHash) {
|
|
||||||
return Promise.reject(Error("contractHash missing"));
|
|
||||||
}
|
|
||||||
if (!merchantSig) {
|
|
||||||
return Promise.reject(Error("merchantSig missing"));
|
|
||||||
}
|
|
||||||
return needsWallet().paymentSucceeded(contractTermsHash, merchantSig);
|
|
||||||
}
|
|
||||||
case "get-sender-wire-infos": {
|
case "get-sender-wire-infos": {
|
||||||
return needsWallet().getSenderWireInfos();
|
return needsWallet().getSenderWireInfos();
|
||||||
}
|
}
|
||||||
@ -316,8 +281,6 @@ function handleMessage(sender: MessageSender,
|
|||||||
return;
|
return;
|
||||||
case "get-report":
|
case "get-report":
|
||||||
return logging.getReport(detail.reportUid);
|
return logging.getReport(detail.reportUid);
|
||||||
case "accept-refund":
|
|
||||||
return needsWallet().acceptRefund(detail.refund_permissions);
|
|
||||||
case "get-purchase": {
|
case "get-purchase": {
|
||||||
const contractTermsHash = detail.contractTermsHash;
|
const contractTermsHash = detail.contractTermsHash;
|
||||||
if (!contractTermsHash) {
|
if (!contractTermsHash) {
|
||||||
@ -351,6 +314,28 @@ function handleMessage(sender: MessageSender,
|
|||||||
case "clear-notification": {
|
case "clear-notification": {
|
||||||
return needsWallet().clearNotification();
|
return needsWallet().clearNotification();
|
||||||
}
|
}
|
||||||
|
case "download-proposal": {
|
||||||
|
return needsWallet().downloadProposal(detail.url);
|
||||||
|
}
|
||||||
|
case "taler-pay": {
|
||||||
|
const senderUrl = sender.url;
|
||||||
|
if (!senderUrl) {
|
||||||
|
console.log("can't trigger payment, no sender URL");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tab = sender.tab;
|
||||||
|
if (!tab) {
|
||||||
|
console.log("can't trigger payment, no sender tab");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tabId = tab.id;
|
||||||
|
if (typeof tabId !== "string") {
|
||||||
|
console.log("can't trigger payment, no sender tab id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
talerPay(detail, senderUrl, tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
// Exhaustiveness check.
|
// Exhaustiveness check.
|
||||||
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html
|
// See https://www.typescriptlang.org/docs/handbook/advanced-types.html
|
||||||
@ -417,13 +402,67 @@ class ChromeNotifier implements Notifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
async function talerPay(fields: any, url: string, tabId: number): Promise<string | undefined> {
|
||||||
* Mapping from tab ID to payment information (if any).
|
if (!currentWallet) {
|
||||||
*
|
console.log("can't handle payment, no wallet");
|
||||||
* Used to pass information from an intercepted HTTP header to the content
|
return undefined;
|
||||||
* script on the page.
|
}
|
||||||
*/
|
|
||||||
const paymentRequestCookies: { [n: number]: any } = {};
|
const w = currentWallet;
|
||||||
|
|
||||||
|
const goToPayment = (p: QueryPaymentFound): string => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fields.resource_url) {
|
||||||
|
const p = await w.queryPaymentByFulfillmentUrl(fields.resource_url);
|
||||||
|
if (p.found) {
|
||||||
|
return goToPayment(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields.contract_hash) {
|
||||||
|
const p = await w.queryPaymentByContractTermsHash(fields.contract_hash);
|
||||||
|
if (p.found) {
|
||||||
|
goToPayment(p);
|
||||||
|
return goToPayment(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fields.contract_url) {
|
||||||
|
const proposalId = await w.downloadProposal(fields.contract_url);
|
||||||
|
const uri = new URI(chrome.extension.getURL("/src/webex/pages/confirm-contract.html"));
|
||||||
|
if (fields.session_id) {
|
||||||
|
uri.addSearch("sessionId", fields.session_id);
|
||||||
|
}
|
||||||
|
uri.addSearch("proposalId", proposalId);
|
||||||
|
const redirectUrl = uri.href();
|
||||||
|
return redirectUrl;
|
||||||
|
}
|
||||||
|
if (fields.offer_url) {
|
||||||
|
return fields.offer_url;
|
||||||
|
}
|
||||||
|
if (fields.refund_url) {
|
||||||
|
console.log("processing refund");
|
||||||
|
const hc = await w.acceptRefund(fields.refund_url);
|
||||||
|
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 undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -433,6 +472,11 @@ const paymentRequestCookies: { [n: number]: any } = {};
|
|||||||
* in this tab.
|
* in this tab.
|
||||||
*/
|
*/
|
||||||
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any {
|
function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: string, tabId: number): any {
|
||||||
|
if (!currentWallet) {
|
||||||
|
console.log("can't handle payment, no wallet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const headers: { [s: string]: string } = {};
|
const headers: { [s: string]: string } = {};
|
||||||
for (const kv of headerList) {
|
for (const kv of headerList) {
|
||||||
if (kv.value) {
|
if (kv.value) {
|
||||||
@ -441,9 +485,12 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fields = {
|
const fields = {
|
||||||
|
contract_hash: headers["x-taler-contract-hash"],
|
||||||
contract_url: headers["x-taler-contract-url"],
|
contract_url: headers["x-taler-contract-url"],
|
||||||
offer_url: headers["x-taler-offer-url"],
|
offer_url: headers["x-taler-offer-url"],
|
||||||
refund_url: headers["x-taler-refund-url"],
|
refund_url: headers["x-taler-refund-url"],
|
||||||
|
resource_url: headers["x-taler-resource-url"],
|
||||||
|
session_id: headers["x-taler-session-id"],
|
||||||
tip: headers["x-taler-tip"],
|
tip: headers["x-taler-tip"],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -456,21 +503,33 @@ function handleHttpPayment(headerList: chrome.webRequest.HttpHeader[], url: stri
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payDetail = {
|
console.log("got pay detail", fields);
|
||||||
contract_url: fields.contract_url,
|
|
||||||
offer_url: fields.offer_url,
|
|
||||||
refund_url: fields.refund_url,
|
|
||||||
tip: fields.tip,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("got pay detail", payDetail);
|
// Fast path for existing payment
|
||||||
|
if (fields.resource_url) {
|
||||||
|
const nextUrl = currentWallet.getNextUrlFromResourceUrl(fields.resource_url);
|
||||||
|
if (nextUrl) {
|
||||||
|
return { redirectUrl: nextUrl };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fast path for new contract
|
||||||
|
if (!fields.contract_hash && 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) {
|
||||||
|
uri.addSearch("sessionId", fields.session_id);
|
||||||
|
}
|
||||||
|
return { redirectUrl: uri.href() };
|
||||||
|
}
|
||||||
|
|
||||||
// This cookie will be read by the injected content script
|
// We need to do some asynchronous operation, we can't directly redirect
|
||||||
// in the tab that displays the page.
|
talerPay(fields, url, tabId).then((nextUrl) => {
|
||||||
paymentRequestCookies[tabId] = {
|
if (nextUrl) {
|
||||||
payDetail,
|
chrome.tabs.update(tabId, { url: nextUrl });
|
||||||
type: "pay",
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -541,7 +600,7 @@ function handleBankRequest(wallet: Wallet, headerList: chrome.webRequest.HttpHea
|
|||||||
const redirectUrl = uri.query(params).href();
|
const redirectUrl = uri.query(params).href();
|
||||||
console.log("redirecting to", redirectUrl);
|
console.log("redirecting to", redirectUrl);
|
||||||
// FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
|
// FIXME: use direct redirect when https://bugzilla.mozilla.org/show_bug.cgi?id=707624 is fixed
|
||||||
chrome.tabs.update(tabId, {url: redirectUrl});
|
chrome.tabs.update(tabId, { url: redirectUrl });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user