/* @source https://www.git.taler.net/?p=web-common.git;a=blob_plain;f=taler-wallet-lib.ts;hb=HEAD @license magnet:?xt=urn:btih:5de60da917303dbfad4f93fb1b985ced5a89eac2&dn=lgpl-2.1.txt LGPL v21 @licstart The following is the entire license notice for the JavaScript code in this page. Copyright (C) 2015, 2016 INRIA The JavaScript code in this page is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License (GNU LGPL) as published by the Free Software Foundation, either version 2.1 of the License, or (at your option) any later version. The code is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU LGPL for more details. As additional permission under GNU LGPL version 2.1 section 7, you may distribute non-source (e.g., minimized or compacted) forms of that code without the copy of the GNU LGPL normally required by section 4, provided you include this license notice and a URL through which recipients can access the Corresponding Source. @licend The above is the entire license notice for the JavaScript code in this page. @author Marcello Stanisci @author Florian Dold */ export namespace taler { "use strict"; let logVerbose: boolean = false; try { logVerbose = !!localStorage.getItem("taler-log-verbose"); } catch (e) { // can't read from local storage } const presentHandlers: any[] = []; const absentHandlers: any[] = []; // Are we running as the content script of an // extension (and not just from a normal page)? let runningInExtension = false; let callSeqId = 1; let installed = false; let probeExecuted = false; let pageLoaded = false; let errorHandler: any = undefined; let sheet: CSSStyleSheet; export function onError(handler: any) { if (errorHandler) { console.warn("Overriding error handler"); } errorHandler = handler; } /** * Error handler for things that go wrong in the merchant * frontend browser code. */ function raise_error(reason: string, detail: any) { if (errorHandler) { errorHandler(reason, detail); return; } alert(`Failure: ${reason}. No error handler installed. Open the developer console for more information.`); console.error(reason, detail); console.warn("No custom error handler set."); } function callWallet(funcName: string, args: any, onResult?: any): void { const detail = JSON.parse(JSON.stringify(args || {})); const callId = callSeqId++; detail.callId = callId; let onTimeout = () => { console.warn("timeout for invocation of " + funcName); } const timeoutHandle: number = setTimeout(onTimeout, 1000); let handler = (evt: CustomEvent) => { if (evt.detail.callId !== callId) { return; } if (onResult) { onResult(evt.detail); } clearTimeout(timeoutHandle); document.removeEventListener(funcName + "-result", handler); }; document.addEventListener(funcName + "-result", handler); const evt = new CustomEvent(funcName, {detail}); document.dispatchEvent(evt) } /** * Confirm that a reserve was created. * * Used by tightly integrated bank portals. */ export function confirmReserve(reservePub: string) { if (!installed) { logVerbose && console.log("delaying confirmReserve"); taler.onPresent(() => { confirmReserve(reservePub); }); return; } callWallet("taler-confirm-reserve", {reserve_pub: reservePub}); } export function createReserve(callbackUrl: string, amount: any, wtTypes: string[], suggestedExchangeUrl?: string) { if (!installed) { logVerbose && console.log("delaying createReserve"); taler.onPresent(() => { createReserve(callbackUrl, amount, wtTypes, suggestedExchangeUrl); }); return; } let args = { callback_url: callbackUrl, amount: amount, wt_types: wtTypes, suggested_exchange_url: suggestedExchangeUrl }; callWallet("taler-create-reserve", args); } function handlePaymentResponse(walletResp: any) { /** * Handle a failed payment. * * Try to notify the wallet first, before we show a potentially * synchronous error message (such as an alert) or leave the page. */ function handleFailedPayment(r: XMLHttpRequest) { let timeoutHandle: number|null = null; function err() { raise_error("pay-failed", {status: r.status, response: r.responseText}); } function onResp() { if (timeoutHandle != null) { clearTimeout(timeoutHandle); timeoutHandle = null; } err(); } function onTimeout() { timeoutHandle = null err(); } callWallet("taler-payment-failed", {H_contract: walletResp.H_contract}, onResp); timeoutHandle = setTimeout(onTimeout, 200); } logVerbose && console.log("handling taler-notify-payment: ", walletResp); // Payment timeout in ms. let timeout_ms = 1000; // Current request. let r: XMLHttpRequest|null; let timeoutHandle: number|null = null; function sendPay() { r = new XMLHttpRequest(); r.open("post", walletResp.contract.pay_url); r.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); r.send(JSON.stringify(walletResp.payReq)); r.onload = function() { if (!r) { return; } switch (r.status) { case 200: const merchantResp = JSON.parse(r.responseText); logVerbose && console.log("got success from pay_url"); callWallet("taler-payment-succeeded", {H_contract: walletResp.H_contract, merchantSig: merchantResp.sig}, () => { let nextUrl = walletResp.contract.fulfillment_url; logVerbose && console.log("taler-payment-succeeded done, going to", nextUrl); window.location.href = nextUrl; window.location.reload(true); }); break; default: handleFailedPayment(r); break; } r = null; if (timeoutHandle != null) { clearTimeout(timeoutHandle!); timeoutHandle = null; } }; function retry() { if (r) { r.abort(); r = null; } timeout_ms = Math.min(timeout_ms * 2, 10 * 1000); logVerbose && console.log("sendPay timed out, retrying in ", timeout_ms, "ms"); sendPay(); } timeoutHandle = setTimeout(retry, timeout_ms); } sendPay(); } export function onPresent(f: any) { presentHandlers.push(f); } export function onAbsent(f: any) { absentHandlers.push(f); } interface FulfillmentQuery { type: "fulfillment_url"; } interface OrderIdQuery { type: "order_id"; order_id: string; } interface PayDetail { contract_url?: string; offer_url?: string; } export function internalPay(p: PayDetail) { // either the callback gets called, // or the wallet will redirect the browser callWallet("taler-pay", p, handlePaymentResponse); } export function pay(p: PayDetail) { if (!installed) { logVerbose && console.log("delaying call to 'pay' until GNU Taler wallet is present"); taler.onPresent(() => { pay(p); }); return; } internalPay(p); } export interface AuditorDetail { currency: string; url: string; auditorPub: string; expirationStamp: number; } export function internalAddAuditor(d: AuditorDetail) { // either the callback gets called, // or the wallet will redirect the browser callWallet("taler-add-auditor", d); } export function addAuditor(d: AuditorDetail) { if (!installed) { logVerbose && console.log("delaying call to 'addAuditor' until GNU Taler wallet is present"); taler.onPresent(() => { addAuditor(d); }); return; } internalAddAuditor(d); } export function internalCheckAuditor(url: string): Promise { return new Promise((resolve, reject) => { callWallet("taler-check-auditor", url, (x: any) => resolve(x as AuditorDetail)); }); } /** * Check if an auditor is already added to the wallet. * * Same-origin restrictions apply. */ export function checkAuditor(url: string): Promise { if (!installed) { logVerbose && console.log("delaying call to 'checkAuditor' until GNU Taler wallet is present"); return new Promise((resolve, reject) => { taler.onPresent(() => { resolve(checkAuditor(url)); }); }); } return internalCheckAuditor(url); } function initTaler() { function handleUninstall() { installed = false; // not really true, but we want "uninstalled" to be shown firstTimeoutCalled = true; announce(); } function handleProbe() { probeExecuted = true; if (!installed) { logVerbose && console.log("taler install detected"); installed = true; announce(); } } function probeTaler() { probeExecuted = false; var eve = new Event("taler-probe"); document.dispatchEvent(eve); } let firstTimeoutCalled = false; function onProbeTimeout() { if (!probeExecuted) { if (installed || !firstTimeoutCalled) { installed = false; firstTimeoutCalled = true; logVerbose && console.log("taler uninstall detected"); announce(); } } // try again, maybe it'll be installed ... probeTaler(); } /** * Announce presence/absence and update stylesheets. * * Only called after document.readyState is at least "interactive". */ function announce() { if (!pageLoaded) { logVerbose && console.log("page not loaded yet, announcing later"); return; } setStyles(); if (installed) { logVerbose && console.log("announcing installed"); for (var i = 0; i < presentHandlers.length; i++) { presentHandlers[i](); } } else { if (firstTimeoutCalled) { logVerbose && console.log("announcing uninstalled"); for (var i = 0; i < absentHandlers.length; i++) { absentHandlers[i](); } } else { logVerbose && console.log("announcing nothing"); } } } function setStyles() { if (!sheet || !sheet.cssRules) { return; } while (sheet.cssRules.length > 0) { sheet.deleteRule(0); } if (installed) { sheet.insertRule(".taler-installed-hide { display: none; }", 0); sheet.insertRule(".taler-probed-hide { display: none; }", 0); } else { sheet.insertRule(".taler-installed-show { display: none; }", 0); if (firstTimeoutCalled) { sheet.insertRule(".taler-probed-hide { display: none; }", 0); } else { // We're still doing the detection sheet.insertRule(".taler-installed-hide { display: none; }", 0); } } } function initStyle() { logVerbose && console.log("taking over styles"); const name = "taler-presence-stylesheet"; const content = "/* Taler stylesheet controlled by JS */"; let style = document.getElementById(name) as HTMLStyleElement|null; if (!style) { style = document.createElement("style"); // Needed by WebKit style.appendChild(document.createTextNode(content)); style.id = name; document.head.appendChild(style); sheet = style.sheet as CSSStyleSheet; } else { // We've taken over the stylesheet now, // make it clear by clearing all the rules in it // and making it obvious in the DOM. if (style.tagName.toLowerCase() === "style") { style.innerText = content; } if (!style.sheet) { throw Error("taler-presence-stylesheet should be a style sheet ( or