new fulfillment protocol

This commit is contained in:
Florian Dold 2016-02-01 15:10:20 +01:00
parent b150470eb6
commit 42a0076f59
19 changed files with 2487 additions and 401 deletions

View File

@ -17,19 +17,22 @@
// query the availability of Taler.
/// <reference path="../lib/decl/chrome/chrome.d.ts" />
"use strict";
console.log("Taler injected");
function subst(url, H_contract) {
// Make sure we don't pollute the namespace too much.
var TalerNotify;
(function (TalerNotify) {
console.log("Taler injected");
function subst(url, H_contract) {
url = url.replace("${H_contract}", H_contract);
url = url.replace("${$}", "$");
return url;
}
document.addEventListener("taler-probe", function (e) {
}
var $ = function (x) { return document.getElementById(x); };
document.addEventListener("taler-probe", function (e) {
var evt = new Event("taler-wallet-present");
document.dispatchEvent(evt);
console.log("handshake done");
});
document.addEventListener("taler-create-reserve", function (e) {
var $ = function (x) { return document.getElementById(x); };
});
document.addEventListener("taler-create-reserve", function (e) {
console.log("taler-create-reserve with " + JSON.stringify(e.detail));
var form_uri = $(e.detail.form_id).action;
// TODO: validate event fields
@ -46,19 +49,18 @@ document.addEventListener("taler-create-reserve", function (e) {
};
var uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html"));
document.location.href = uri.query(params).href();
});
document.addEventListener("taler-contract", function (e) {
});
document.addEventListener("taler-contract", function (e) {
// XXX: the merchant should just give us the parsed data ...
var offer = JSON.parse(e.detail);
var uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
var params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
cookie: document.cookie,
};
document.location.href = uri.query(params).href();
});
document.addEventListener('taler-execute-payment', function (e) {
});
document.addEventListener('taler-execute-payment', function (e) {
console.log("got taler-execute-payment in content page");
if (!e.detail.pay_url) {
console.log("field 'pay_url' missing in taler-execute-payment event");
@ -72,26 +74,34 @@ document.addEventListener('taler-execute-payment', function (e) {
},
};
chrome.runtime.sendMessage(msg, function (resp) {
console.log("got resp");
console.dir(resp);
if (!resp.success) {
console.log("failure!");
console.log("got event detial:");
console.dir(e.detail);
if (e.detail.offering_url) {
console.log("offering url", e.detail.offering_url);
window.location.href = e.detail.offering_url;
}
else {
console.error("execute-payment failed");
}
return;
}
var contract = resp.contract;
if (!contract) {
throw Error("contract missing");
}
var payReq = Object.assign({}, resp.payReq);
if (e.detail.require_contract) {
payReq.contract = contract;
}
console.log("Making request to ", payUrl);
var r = new XMLHttpRequest();
r.open('post', payUrl);
r.send(JSON.stringify(payReq));
r.send(JSON.stringify(resp.payReq));
r.onload = function () {
switch (r.status) {
case 200:
console.log("going to", contract.fulfillment_url);
// TODO: Is this the right thing? Does the reload
// TODO: override setting location.href?
window.location.href = subst(contract.fulfillment_url, e.detail.H_contract);
window.location.reload(true);
break;
@ -101,5 +111,6 @@ document.addEventListener('taler-execute-payment', function (e) {
}
};
});
});
});
})(TalerNotify || (TalerNotify = {}));
//# sourceMappingURL=notify.js.map

View File

@ -21,23 +21,26 @@
"use strict";
console.log("Taler injected");
function subst(url: string, H_contract) {
// Make sure we don't pollute the namespace too much.
namespace TalerNotify {
console.log("Taler injected");
function subst(url: string, H_contract) {
url = url.replace("${H_contract}", H_contract);
url = url.replace("${$}", "$");
return url;
}
}
let $ = (x) => document.getElementById(x);
document.addEventListener("taler-probe", function(e) {
document.addEventListener("taler-probe", function(e) {
let evt = new Event("taler-wallet-present");
document.dispatchEvent(evt);
console.log("handshake done");
});
});
document.addEventListener("taler-create-reserve", function(e: CustomEvent) {
let $ = (x) => document.getElementById(x);
document.addEventListener("taler-create-reserve", function(e: CustomEvent) {
console.log("taler-create-reserve with " + JSON.stringify(e.detail));
let form_uri = (<HTMLFormElement>$(e.detail.form_id)).action;
// TODO: validate event fields
@ -54,23 +57,22 @@ document.addEventListener("taler-create-reserve", function(e: CustomEvent) {
};
let uri = URI(chrome.extension.getURL("pages/confirm-create-reserve.html"));
document.location.href = uri.query(params).href();
});
});
document.addEventListener("taler-contract", function(e: CustomEvent) {
document.addEventListener("taler-contract", function(e: CustomEvent) {
// XXX: the merchant should just give us the parsed data ...
let offer = JSON.parse(e.detail);
let uri = URI(chrome.extension.getURL("pages/confirm-contract.html"));
let params = {
offer: JSON.stringify(offer),
merchantPageUrl: document.location.href,
cookie: document.cookie,
};
document.location.href = uri.query(params).href();
});
});
document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
console.log("got taler-execute-payment in content page");
if (!e.detail.pay_url) {
console.log("field 'pay_url' missing in taler-execute-payment event");
@ -84,8 +86,17 @@ document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
},
};
chrome.runtime.sendMessage(msg, (resp) => {
console.log("got resp");
console.dir(resp);
if (!resp.success) {
console.log("failure!");
console.log("got event detial:");
console.dir(e.detail);
if (e.detail.offering_url) {
console.log("offering url", e.detail.offering_url);
window.location.href = e.detail.offering_url;
} else {
console.error("execute-payment failed");
}
return;
}
let contract = resp.contract;
@ -93,19 +104,16 @@ document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
throw Error("contract missing");
}
let payReq = Object.assign({}, resp.payReq);
if (e.detail.require_contract) {
payReq.contract = contract;
}
console.log("Making request to ", payUrl);
let r = new XMLHttpRequest();
r.open('post', payUrl);
r.send(JSON.stringify(payReq));
r.send(JSON.stringify(resp.payReq));
r.onload = () => {
switch (r.status) {
case 200:
console.log("going to", contract.fulfillment_url);
// TODO: Is this the right thing? Does the reload
// TODO: override setting location.href?
window.location.href = subst(contract.fulfillment_url,
e.detail.H_contract);
window.location.reload(true);
@ -116,4 +124,5 @@ document.addEventListener('taler-execute-payment', function(e: CustomEvent) {
}
};
});
});
});
}

2178
extension/lib/decl/node.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,136 +0,0 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
"use strict";
/**
* Decorators for type-checking JSON into
* an object.
* @module Checkable
* @author Florian Dold
*/
export namespace Checkable {
let chkSym = Symbol("checkable");
function checkNumber(target, prop): any {
if ((typeof target) !== "number") {
throw Error("number expected for " + prop.propertyKey);
}
return target;
}
function checkString(target, prop): any {
if (typeof target !== "string") {
throw Error("string expected for " + prop.propertyKey);
}
return target;
}
function checkAnyObject(target, prop): any {
if (typeof target !== "object") {
throw Error("object expected for " + prop.propertyKey);
}
return target;
}
function checkValue(target, prop): any {
let type = prop.type;
if (!type) {
throw Error("assertion failed");
}
let v = target;
if (!v || typeof v !== "object") {
throw Error("expected object for " + prop.propertyKey);
}
let props = type.prototype[chkSym].props;
let remainingPropNames = new Set(Object.getOwnPropertyNames(v));
let obj = new type();
for (let prop of props) {
if (!remainingPropNames.has(prop.propertyKey)) {
throw Error("Property missing: " + prop.propertyKey);
}
if (!remainingPropNames.delete(prop.propertyKey)) {
throw Error("assertion failed");
}
let propVal = v[prop.propertyKey];
obj[prop.propertyKey] = prop.checker(propVal, prop);
}
if (remainingPropNames.size != 0) {
throw Error("superfluous properties " + JSON.stringify(Array.from(
remainingPropNames.values())));
}
return obj;
}
export function Class(target) {
target.checked = (v) => {
return checkValue(v, {
propertyKey: "(root)",
type: target,
checker: checkValue
});
};
return target;
}
export function Value(type) {
function deco(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
chk.props.push({
propertyKey: propertyKey,
checker: checkValue,
type: type
});
}
return deco;
}
export function List(type) {
function deco(target: Object, propertyKey: string | symbol): void {
throw Error("not implemented");
}
return deco;
}
export function Number(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
chk.props.push({propertyKey: propertyKey, checker: checkNumber});
}
export function AnyObject(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
chk.props.push({propertyKey: propertyKey, checker: checkAnyObject});
}
export function String(target: Object, propertyKey: string | symbol): void {
let chk = mkChk(target);
chk.props.push({propertyKey: propertyKey, checker: checkString});
}
function mkChk(target) {
let chk = target[chkSym];
if (!chk) {
chk = {props: []};
target[chkSym] = chk;
}
return chk;
}
}

View File

@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
import {AmountJson_interface} from "./types";
import {AmountJson} from "./types";
import * as EmscWrapper from "../emscripten/emsc";
/**
@ -288,7 +288,7 @@ arenaStack.push(new SyncArena());
export class Amount extends ArenaObject {
constructor(args?: AmountJson_interface, arena?: Arena) {
constructor(args?: AmountJson, arena?: Arena) {
super(arena);
if (args) {
this.nativePtr = emscAlloc.get_amount(args.value,

View File

@ -14,8 +14,6 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
/// <reference path="../decl/urijs/URIjs.d.ts" />
/**
* Database query abstractions.

View File

@ -33,10 +33,10 @@ export interface Keys {
}
export interface Denomination {
value: AmountJson_interface;
value: AmountJson;
denom_pub: string;
fee_withdraw: AmountJson_interface;
fee_deposit: AmountJson_interface;
fee_withdraw: AmountJson;
fee_deposit: AmountJson;
}
export interface PreCoin {
@ -48,7 +48,7 @@ export interface PreCoin {
withdrawSig: string;
coinEv: string;
mintBaseUrl: string;
coinValue: AmountJson_interface;
coinValue: AmountJson;
}
export interface Coin {
@ -56,17 +56,18 @@ export interface Coin {
coinPriv: string;
denomPub: string;
denomSig: string;
currentAmount: AmountJson_interface;
currentAmount: AmountJson;
mintBaseUrl: string;
}
export interface AmountJson_interface {
export interface AmountJson {
value: number;
fraction: number
currency: string;
}
export interface ConfirmReserveRequest {
/**
* Name of the form field for the amount.

View File

@ -22,7 +22,6 @@
*/
import {Amount} from "./emscriptif"
import {AmountJson_interface} from "./types";
import {CoinWithDenom} from "./types";
import {DepositRequestPS_Args} from "./emscriptif";
import {HashCode} from "./emscriptif";
@ -45,46 +44,25 @@ import {PreCoin} from "./types";
import {rsaUnblind} from "./emscriptif";
import {RsaSignature} from "./emscriptif";
import {Mint} from "./types";
import {Checkable} from "./checkable";
import {HttpResponse} from "./http";
import {RequestException} from "./http";
import {Query} from "./query";
import {AmountJson} from "./types";
"use strict";
@Checkable.Class
class AmountJson {
@Checkable.Number
value: number;
@Checkable.Number
fraction: number;
@Checkable.String
currency: string;
static check: (v: any) => AmountJson;
}
@Checkable.Class
class CoinPaySig {
@Checkable.String
coin_sig: string;
@Checkable.String
coin_pub: string;
@Checkable.String
ub_sig: string;
@Checkable.String
denom_pub: string;
@Checkable.Value(AmountJson)
f: AmountJson;
static check: (v: any) => CoinPaySig;
}
@ -104,17 +82,17 @@ interface MintInfo {
interface Offer {
contract: Contract;
sig: string;
merchant_sig: string;
H_contract: string;
}
interface Contract {
H_wire: string;
amount: AmountJson_interface;
amount: AmountJson;
auditors: string[];
expiry: string,
locations: string[];
max_fee: AmountJson_interface;
max_fee: AmountJson;
merchant: any;
merchant_pub: string;
mints: MintInfo[];
@ -122,6 +100,7 @@ interface Contract {
refund_deadline: string;
timestamp: string;
transaction_id: number;
fulfillment_url: string;
}
@ -130,7 +109,7 @@ interface CoinPaySig_interface {
coin_pub: string;
ub_sig: string;
denom_pub: string;
f: AmountJson_interface;
f: AmountJson;
}
@ -177,7 +156,7 @@ function canonicalizeBaseUrl(url) {
return x.href()
}
function parsePrettyAmount(pretty: string): AmountJson_interface {
function parsePrettyAmount(pretty: string): AmountJson {
const res = /([0-9]+)(.[0-9]+)?\s*(\w+)/.exec(pretty);
if (!res) {
return null;
@ -291,8 +270,8 @@ export class Wallet {
* @param depositFeeLimit
* @param allowedMints
*/
getPossibleMintCoins(paymentAmount: AmountJson_interface,
depositFeeLimit: AmountJson_interface,
getPossibleMintCoins(paymentAmount: AmountJson,
depositFeeLimit: AmountJson,
allowedMints: MintInfo[]): Promise<MintCoins> {
@ -366,15 +345,17 @@ export class Wallet {
executePay(offer: Offer,
payCoinInfo: PayCoinInfo,
chosenMint: string): Promise<void> {
chosenMint: string): Promise<any> {
let payReq = {};
payReq["H_wire"] = offer.contract.H_wire;
payReq["H_contract"] = offer.H_contract;
payReq["transaction_id"] = offer.contract.transaction_id;
payReq["refund_deadline"] = offer.contract.refund_deadline;
payReq["mint"] = URI(chosenMint).href();
payReq["amount"] = offer.contract.amount;
payReq["coins"] = payCoinInfo.map((x) => x.sig);
payReq["H_contract"] = offer.H_contract;
payReq["max_fee"] = offer.contract.max_fee;
payReq["merchant_sig"] = offer.merchant_sig;
payReq["mint"] = URI(chosenMint).href();
payReq["refund_deadline"] = offer.contract.refund_deadline;
payReq["timestamp"] = offer.contract.timestamp;
payReq["transaction_id"] = offer.contract.transaction_id;
let t: Transaction = {
contractHash: offer.H_contract,
contract: offer.contract,
@ -387,7 +368,8 @@ export class Wallet {
detail: {
merchantName: offer.contract.merchant.name,
amount: offer.contract.amount,
contractHash: offer.H_contract
contractHash: offer.H_contract,
fulfillmentUrl: offer.contract.fulfillment_url
}
};
@ -395,7 +377,12 @@ export class Wallet {
.put("transactions", t)
.put("history", historyEntry)
.putAll("coins", payCoinInfo.map((pci) => pci.updatedCoin))
.finish();
.finish()
.then(() => {
return {
success: true
};
});
}
confirmPay(offer: Offer): Promise<any> {
@ -405,23 +392,31 @@ export class Wallet {
offer.contract.mints)
}).then((mcs) => {
if (Object.keys(mcs).length == 0) {
throw Error("Not enough coins.");
return {
success: false,
message: "Not enough coins",
};
}
let mintUrl = Object.keys(mcs)[0];
let ds = Wallet.signDeposit(offer, mcs[mintUrl]);
return this.executePay(offer, ds, mintUrl);
return this
.executePay(offer, ds, mintUrl);
});
}
doPayment(H_contract): Promise<PaymentResponse> {
doPayment(H_contract): Promise<any> {
return Promise.resolve().then(() => {
return Query(this.db)
.get("transactions", H_contract)
.then((t) => {
if (!t) {
throw Error("contract not found");
return {
success: false,
contractFound: false,
}
let resp: PaymentResponse = {
}
let resp = {
success: true,
payReq: t.payReq,
contract: t.contract,
};
@ -682,8 +677,10 @@ export class Wallet {
let next = () => {
if (workList.length == 0) {
resolve();
return;
}
let d = workList.pop();
console.log("withdrawing", JSON.stringify(d));
this.withdraw(d, reserve)
.then(() => next())
.catch((e) => {
@ -760,7 +757,7 @@ export class Wallet {
getBalances(): Promise<any> {
function collectBalances(c: Coin, byCurrency) {
let acc: AmountJson_interface = byCurrency[c.currentAmount.currency];
let acc: AmountJson = byCurrency[c.currentAmount.currency];
if (!acc) {
acc = Amount.getZero(c.currentAmount.currency).toJson();
}

View File

@ -66,8 +66,8 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
},
_a["confirm-pay"] = function (db, detail, sendResponse) {
wallet.confirmPay(detail.offer, detail.merchantPageUrl)
.then(function () {
sendResponse({ success: true });
.then(function (r) {
sendResponse(r);
})
.catch(function (e) {
console.error("exception during 'confirm-pay'");
@ -79,11 +79,7 @@ System.register(["./wallet", "./db", "./http"], function(exports_1) {
_a["execute-payment"] = function (db, detail, sendResponse) {
wallet.doPayment(detail.H_contract)
.then(function (r) {
sendResponse({
success: true,
payReq: r.payReq,
contract: r.contract,
});
sendResponse(r);
})
.catch(function (e) {
console.error("exception during 'execute-payment'");

View File

@ -83,8 +83,8 @@ function makeHandlers(wallet) {
},
["confirm-pay"]: function(db, detail, sendResponse) {
wallet.confirmPay(detail.offer, detail.merchantPageUrl)
.then(() => {
sendResponse({success: true})
.then((r) => {
sendResponse(r)
})
.catch((e) => {
console.error("exception during 'confirm-pay'");
@ -96,11 +96,7 @@ function makeHandlers(wallet) {
["execute-payment"]: function(db, detail, sendResponse) {
wallet.doPayment(detail.H_contract)
.then((r) => {
sendResponse({
success: true,
payReq: r.payReq,
contract: r.contract,
});
sendResponse(r);
})
.catch((e) => {
console.error("exception during 'execute-payment'");

View File

@ -14,8 +14,8 @@
TALER; see the file COPYING. If not, If not, see <http://www.gnu.org/licenses/>
*/
export function substituteFulfillmentUrl(url: string, offer) {
url = url.replace("${H_contract}", offer.H_contract);
export function substituteFulfillmentUrl(url: string, vars) {
url = url.replace("${H_contract}", vars.H_contract);
url = url.replace("${$}", "$");
return url;
}

View File

@ -42,7 +42,6 @@
"background": {
"scripts": [
"lib/vendor/URI.js",
"lib/vendor/handlebars-v4.0.5.js",
"lib/emscripten/libwrapper.js",
"lib/vendor/system-csp-production.src.js",
"background/main.js"

View File

@ -11,23 +11,6 @@
<script src="../lib/module-trampoline.js"></script>
<link rel="stylesheet" type="text/css" href="../style/wallet.css">
<script id="contract-template" type="text/x-handlebars-template">
Your contract includes these products:
<ul>
{{#each products}}
<li>{{description}}: {{prettyAmount price}}</li>
{{/each}}
</ul>
<p />
</script>
<script id="error-template" type="text/x-handlebars-template">
Payment was not successful: {{error}}
</script>
</head>
<body>
<header>
<div id="logo"></div>

View File

@ -27,13 +27,15 @@ System.register(["../lib/web-common"], function(exports_1) {
var offer = JSON.parse(query.offer);
console.dir(offer);
var contract = offer.contract;
var error = null;
var Contract = {
view: function (ctrl) {
return [
m("p", (_a = ["Hello, this is the wallet. The merchant \"", "\"\n wants to enter a contract over ", "\n with you."], _a.raw = ["Hello, this is the wallet. The merchant \"", "\"\n wants to enter a contract over ", "\n with you."], i18n(_a, contract.merchant.name, prettyAmount(contract.amount)))),
m("p", (_b = ["The contract contains the following products:"], _b.raw = ["The contract contains the following products:"], i18n(_b))),
m('ul', _.map(contract.products, function (p) { return m("li", p.description + ": " + prettyAmount(p.price)); })),
m("button", { onclick: doPayment }, (_c = ["Confirm Payment"], _c.raw = ["Confirm Payment"], i18n(_c)))
m("button", { onclick: doPayment }, (_c = ["Confirm Payment"], _c.raw = ["Confirm Payment"], i18n(_c))),
m("p", error ? error : []),
];
var _a, _b, _c;
}
@ -46,6 +48,8 @@ System.register(["../lib/web-common"], function(exports_1) {
chrome.runtime.sendMessage({ type: 'confirm-pay', detail: d }, function (resp) {
if (!resp.success) {
console.log("confirm-pay error", JSON.stringify(resp));
error = resp.message;
m.redraw();
return;
}
var c = d.offer.contract;

View File

@ -33,6 +33,7 @@ export function main() {
let offer = JSON.parse(query.offer);
console.dir(offer);
let contract = offer.contract;
let error = null;
var Contract = {
view(ctrl) {
@ -47,7 +48,8 @@ export function main() {
_.map(contract.products,
(p: any) => m("li",
`${p.description}: ${prettyAmount(p.price)}`))),
m("button", {onclick: doPayment}, i18n`Confirm Payment`)
m("button", {onclick: doPayment}, i18n`Confirm Payment`),
m("p", error ? error : []),
];
}
};
@ -62,6 +64,8 @@ export function main() {
chrome.runtime.sendMessage({type: 'confirm-pay', detail: d}, (resp) => {
if (!resp.success) {
console.log("confirm-pay error", JSON.stringify(resp));
error = resp.message;
m.redraw();
return;
}
let c = d.offer.contract;

View File

@ -8,13 +8,13 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-01-27 01:05+0100\n"
"POT-Creation-Date: 2016-01-27 01:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: example/test.ts:3
@ -95,3 +95,13 @@ msgid ""
"asdf"
msgstr ""
#: example/test.ts:42
#, csharp-format
msgid "This message appears twice"
msgstr ""
#: example/test.ts:45
#, csharp-format
msgid "This message appears twice"
msgstr ""

View File

@ -18,7 +18,11 @@ It has multiple lines, and a trailing empty line.
*/
console.log(/*lol*/i18n.foo`Hello7,${123} World${42}`);
console.log(i18n.foo`${"foo"}Hello8,${123} World${42}`);
i18n.plural()
console.log(i18n`${"foo"}Hello8,${123} World${42}`);
/*
@ -40,3 +44,10 @@ it should be wrapped and stuff`);
// This is a single line comment
console.log(i18n`Hello12 this is a long long string it will go over multiple lines and in the pofile it should be wrapped and stuff. asdf asdf asdf asdf asdf asdf asdf asdf adsf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf`);
// First occurence
console.log(i18n`This message appears twice`);
// Second occurence
console.log(i18n`This message appears twice`);

View File

@ -20,6 +20,8 @@
"use strict";
import {substituteFulfillmentUrl} from "../lib/web-common";
declare var m: any;
declare var i18n: any;
@ -106,11 +108,22 @@ function formatAmount(amount) {
return `${v.toFixed(2)} ${amount.currency}`;
}
function abbrevKey(s: string) {
return m("span.abbrev", {title: s}, (s.slice(0, 5) + ".."))
}
function retryPayment(url, contractHash) {
return function() {
chrome.tabs.create({
"url": substituteFulfillmentUrl(url,
{H_contract: contractHash})
});
}
}
function formatHistoryItem(historyItem) {
const d = historyItem.detail;
const t = historyItem.timestamp;
@ -124,10 +137,14 @@ function formatHistoryItem(historyItem) {
return m("p",
i18n`Withdraw at ${formatTimestamp(t)}`);
case "pay":
let url = substituteFulfillmentUrl(d.fulfillmentUrl,
{H_contract: d.contractHash});
return m("p",
[
i18n`Payment for ${formatAmount(d.amount)} to merchant ${d.merchantName}. `,
m("a[href=javascript:;]", "Retry")
m(`a`,
{href: url, onclick: openTab(url)},
"Retry")
]);
default:
return m("p", i18n`Unknown event (${historyItem.type})`);
@ -204,3 +221,12 @@ function openExtensionPage(page) {
}
}
function openTab(page) {
return function() {
chrome.tabs.create({
"url": page
});
}
}

View File

@ -11,7 +11,6 @@
"lib/i18n.ts",
"lib/refs.ts",
"lib/web-common.ts",
"lib/wallet/checkable.ts",
"lib/wallet/db.ts",
"lib/wallet/emscriptif.ts",
"lib/wallet/http.ts",