diff options
-rw-r--r-- | src/webex/messages.ts | 6 | ||||
-rw-r--r-- | src/webex/pages/popup.tsx | 3 | ||||
-rw-r--r-- | src/webex/pages/reset-required.html | 30 | ||||
-rw-r--r-- | src/webex/pages/reset-required.tsx | 73 | ||||
-rw-r--r-- | src/webex/style/wallet.css | 4 | ||||
-rw-r--r-- | src/webex/wxApi.ts | 35 | ||||
-rw-r--r-- | src/webex/wxBackend.ts | 208 | ||||
-rw-r--r-- | tsconfig.json | 1 | ||||
-rw-r--r-- | webpack.config.js | 1 |
9 files changed, 280 insertions, 81 deletions
diff --git a/src/webex/messages.ts b/src/webex/messages.ts index 27ff9a5b6..bf9ca00b0 100644 --- a/src/webex/messages.ts +++ b/src/webex/messages.ts @@ -49,7 +49,7 @@ export interface MessageMap { request: { }; response: void; }; - "reset": { + "reset-db": { request: { }; response: void; }; @@ -164,6 +164,10 @@ export interface MessageMap { request: { contractTermsHash: string; merchantSig: string }; response: void; }; + "check-upgrade": { + request: { }; + response: void; + }; } /** diff --git a/src/webex/pages/popup.tsx b/src/webex/pages/popup.tsx index 831147f1e..f1f0353ad 100644 --- a/src/webex/pages/popup.tsx +++ b/src/webex/pages/popup.tsx @@ -36,6 +36,7 @@ import { } from "../../types"; import { abbrev, renderAmount } from "../renderHtml"; +import * as wxApi from "../wxApi"; import * as React from "react"; import * as ReactDOM from "react-dom"; @@ -484,7 +485,7 @@ function reload() { function confirmReset() { if (confirm("Do you want to IRREVOCABLY DESTROY everything inside your" + " wallet and LOSE ALL YOUR COINS?")) { - chrome.runtime.sendMessage({type: "reset"}); + wxApi.resetDb(); window.close(); } } diff --git a/src/webex/pages/reset-required.html b/src/webex/pages/reset-required.html new file mode 100644 index 000000000..72b176b4d --- /dev/null +++ b/src/webex/pages/reset-required.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html> + +<head> + <meta charset="UTF-8"> + <title>Taler Wallet: Select Taler Provider</title> + + <link rel="icon" href="/img/icon.png"> + <link rel="stylesheet" type="text/css" href="../style/wallet.css"> + <link rel="stylesheet" type="text/css" href="../style/pure.css"> + + <script src="/dist/page-common-bundle.js"></script> + <script src="/dist/reset-required-bundle.js"></script> + + <style> + body { + font-size: 100%; + overflow-y: scroll; + } + </style> + +</head> + +<body> + <section id="main"> + <div id="container"></div> + </section> +</body> + +</html> diff --git a/src/webex/pages/reset-required.tsx b/src/webex/pages/reset-required.tsx new file mode 100644 index 000000000..90ea51abe --- /dev/null +++ b/src/webex/pages/reset-required.tsx @@ -0,0 +1,73 @@ +/* + This file is part of TALER + (C) 2017 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, see <http://www.gnu.org/licenses/> + */ + + +/** + * Page to inform the user when a database reset is required. + * + * @author Florian Dold + */ + +import * as React from "react"; +import * as ReactDOM from "react-dom"; + +import * as wxApi from "../wxApi"; + +class State { + checked: boolean; + resetRequired: boolean; +} + + +class ResetNotification extends React.Component<any, State> { + constructor(props: any) { + super(props); + this.state = {checked: false, resetRequired: true}; + setInterval(() => this.update(), 500); + } + async update() { + const res = await wxApi.checkUpgrade(); + this.setState({resetRequired: res.dbResetRequired}); + } + render() { + if (this.state.resetRequired) { + return ( + <div> + <h1>Manual Reset Reqired</h1> + <p>The wallet's database in your browser is incompatible with the currently installed wallet. Please reset manually.</p> + <p>Once the database format has stabilized, we will provide automatic upgrades.</p> + <input id="check" type="checkbox" checked={this.state.checked} onChange={(e) => this.setState({checked: e.target.checked})} />{" "} + <label htmlFor="check"> + I understand that I will lose all my data + </label> + <br /> + <button className="pure-button" disabled={!this.state.checked} onClick={() => wxApi.resetDb()}>Reset</button> + </div> + ); + } + return ( + <div> + <h1>Everything is fine!</h1> + A reset is not required anymore, you can close this page. + </div> + ); + } +} + + +document.addEventListener("DOMContentLoaded", () => { + ReactDOM.render(<ResetNotification />, document.getElementById( "container")!); +}); diff --git a/src/webex/style/wallet.css b/src/webex/style/wallet.css index 7bfb99e6c..5773eb396 100644 --- a/src/webex/style/wallet.css +++ b/src/webex/style/wallet.css @@ -79,10 +79,6 @@ label { padding-right: 1em; } -label::after { - content: ":"; -} - input.url { width: 25em; } diff --git a/src/webex/wxApi.ts b/src/webex/wxApi.ts index 1968b6575..a064b4133 100644 --- a/src/webex/wxApi.ts +++ b/src/webex/wxApi.ts @@ -39,6 +39,27 @@ import { import { MessageType, MessageMap } from "./messages"; +/** + * Response with information about available version upgrades. + */ +export interface UpgradeResponse { + /** + * Is a reset required because of a new DB version + * that can't be atomatically upgraded? + */ + dbResetRequired: boolean; + + /** + * Current database version. + */ + currentDbVersion: string; + + /** + * Old db version (if applicable). + */ + oldDbVersion: string; +} + async function callBackend<T extends MessageType>(type: T, detail: MessageMap[T]["request"]): Promise<any> { return new Promise<any>((resolve, reject) => { @@ -254,3 +275,17 @@ export function getTabCookie(contractTermsHash: string, merchantSig: string): Pr export function generateNonce(): Promise<string> { return callBackend("generate-nonce", { }); } + +/** + * Check upgrade information + */ +export function checkUpgrade(): Promise<UpgradeResponse> { + return callBackend("check-upgrade", { }); +} + +/** + * Reset database + */ +export function resetDb(): Promise<void> { + return callBackend("reset-db", { }); +} diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 35fa0b573..0bd4a211e 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -44,6 +44,7 @@ import { import { ChromeBadge } from "./chromeBadge"; import { MessageType } from "./messages"; +import * as wxApi from "./wxApi"; import URI = require("urijs"); import Port = chrome.runtime.Port; @@ -60,23 +61,34 @@ const DB_NAME = "taler"; */ const DB_VERSION = 18; -function handleMessage(db: IDBDatabase, - wallet: Wallet, - sender: MessageSender, +const NeedsWallet = Symbol("NeedsWallet"); + +function handleMessage(sender: MessageSender, type: MessageType, detail: any): any { function assertNotFound(t: never): never { console.error(`Request type ${t as string} unknown`); console.error(`Request detail was ${detail}`); return { error: "request unknown", requestType: type } as never; } + function needsWallet(): Wallet { + if (!currentWallet) { + throw NeedsWallet; + } + return currentWallet; + } switch (type) { - case "balances": - return wallet.getBalances(); - case "dump-db": + case "balances": { + return needsWallet().getBalances(); + } + case "dump-db": { + const db = needsWallet().db; return exportDb(db); - case "import-db": + } + case "import-db": { + const db = needsWallet().db; return importDb(db, detail.dump); - case "get-tab-cookie": + } + case "get-tab-cookie": { if (!sender || !sender.tab || !sender.tab.id) { return Promise.resolve(); } @@ -84,10 +96,13 @@ function handleMessage(db: IDBDatabase, const info: any = paymentRequestCookies[id] as any; delete paymentRequestCookies[id]; return Promise.resolve(info); - case "ping": + } + case "ping": { return Promise.resolve(); - case "reset": - if (db) { + } + case "reset-db": { + if (currentWallet) { + const db = currentWallet.db; const tx = db.transaction(Array.from(db.objectStoreNames), "readwrite"); // tslint:disable-next-line:prefer-for-of for (let i = 0; i < db.objectStoreNames.length; i++) { @@ -97,34 +112,42 @@ function handleMessage(db: IDBDatabase, deleteDb(); chrome.browserAction.setBadgeText({ text: "" }); console.log("reset done"); + if (!currentWallet) { + reinitWallet(); + } return Promise.resolve({}); + } case "create-reserve": { const d = { amount: detail.amount, exchange: detail.exchange, }; const req = CreateReserveRequest.checked(d); - return wallet.createReserve(req); + return needsWallet().createReserve(req); } - case "confirm-reserve": + case "confirm-reserve": { const d = { reservePub: detail.reservePub, }; const req = ConfirmReserveRequest.checked(d); - return wallet.confirmReserve(req); - case "generate-nonce": - return wallet.generateNonce(); - case "confirm-pay": + return needsWallet().confirmReserve(req); + } + case "generate-nonce": { + return needsWallet().generateNonce(); + } + case "confirm-pay": { if (typeof detail.proposalId !== "number") { throw Error("proposalId must be number"); } - return wallet.confirmPay(detail.proposalId); - case "check-pay": + return needsWallet().confirmPay(detail.proposalId); + } + case "check-pay": { if (typeof detail.proposalId !== "number") { throw Error("proposalId must be number"); } - return wallet.checkPay(detail.proposalId); - case "query-payment": + return needsWallet().checkPay(detail.proposalId); + } + case "query-payment": { if (sender.tab && sender.tab.id) { rateLimitCache[sender.tab.id]++; if (rateLimitCache[sender.tab.id] > 10) { @@ -137,99 +160,120 @@ function handleMessage(db: IDBDatabase, return Promise.resolve(msg); } } - return wallet.queryPayment(detail.url); - case "exchange-info": + return needsWallet().queryPayment(detail.url); + } + case "exchange-info": { if (!detail.baseUrl) { return Promise.resolve({ error: "bad url" }); } - return wallet.updateExchangeFromUrl(detail.baseUrl); - case "currency-info": + return needsWallet().updateExchangeFromUrl(detail.baseUrl); + } + case "currency-info": { if (!detail.name) { return Promise.resolve({ error: "name missing" }); } - return wallet.getCurrencyRecord(detail.name); - case "hash-contract": + return needsWallet().getCurrencyRecord(detail.name); + } + case "hash-contract": { if (!detail.contract) { return Promise.resolve({ error: "contract missing" }); } - return wallet.hashContract(detail.contract).then((hash) => { + return needsWallet().hashContract(detail.contract).then((hash) => { return hash; }); - case "put-history-entry": + } + case "put-history-entry": { if (!detail.historyEntry) { return Promise.resolve({ error: "historyEntry missing" }); } - return wallet.putHistory(detail.historyEntry); - case "save-proposal": + return needsWallet().putHistory(detail.historyEntry); + } + case "save-proposal": { console.log("handling save-proposal", detail); const checkedRecord = ProposalRecord.checked({ contractTerms: detail.data, contractTermsHash: detail.hash, merchantSig: detail.sig, }); - return wallet.saveProposal(checkedRecord); - case "reserve-creation-info": + return needsWallet().saveProposal(checkedRecord); + } + case "reserve-creation-info": { if (!detail.baseUrl || typeof detail.baseUrl !== "string") { return Promise.resolve({ error: "bad url" }); } const amount = AmountJson.checked(detail.amount); - return wallet.getReserveCreationInfo(detail.baseUrl, amount); - case "get-history": + return needsWallet().getReserveCreationInfo(detail.baseUrl, amount); + } + case "get-history": { // TODO: limit history length - return wallet.getHistory(); - case "get-proposal": - return wallet.getProposal(detail.proposalId); - case "get-exchanges": - return wallet.getExchanges(); - case "get-currencies": - return wallet.getCurrencies(); - case "update-currency": - return wallet.updateCurrency(detail.currencyRecord); - case "get-reserves": + return needsWallet().getHistory(); + } + case "get-proposal": { + return needsWallet().getProposal(detail.proposalId); + } + case "get-exchanges": { + return needsWallet().getExchanges(); + } + case "get-currencies": { + return needsWallet().getCurrencies(); + } + case "update-currency": { + return needsWallet().updateCurrency(detail.currencyRecord); + } + case "get-reserves": { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangeBaseUrl missing")); } - return wallet.getReserves(detail.exchangeBaseUrl); - case "get-payback-reserves": - return wallet.getPaybackReserves(); - case "withdraw-payback-reserve": + return needsWallet().getReserves(detail.exchangeBaseUrl); + } + case "get-payback-reserves": { + return needsWallet().getPaybackReserves(); + } + case "withdraw-payback-reserve": { if (typeof detail.reservePub !== "string") { return Promise.reject(Error("reservePub missing")); } - return wallet.withdrawPaybackReserve(detail.reservePub); - case "get-coins": + return needsWallet().withdrawPaybackReserve(detail.reservePub); + } + case "get-coins": { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } - return wallet.getCoins(detail.exchangeBaseUrl); - case "get-precoins": + return needsWallet().getCoins(detail.exchangeBaseUrl); + } + case "get-precoins": { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } - return wallet.getPreCoins(detail.exchangeBaseUrl); - case "get-denoms": + return needsWallet().getPreCoins(detail.exchangeBaseUrl); + } + case "get-denoms": { if (typeof detail.exchangeBaseUrl !== "string") { return Promise.reject(Error("exchangBaseUrl missing")); } - return wallet.getDenoms(detail.exchangeBaseUrl); - case "refresh-coin": + return needsWallet().getDenoms(detail.exchangeBaseUrl); + } + case "refresh-coin": { if (typeof detail.coinPub !== "string") { return Promise.reject(Error("coinPub missing")); } - return wallet.refresh(detail.coinPub); - case "payback-coin": + return needsWallet().refresh(detail.coinPub); + } + case "payback-coin": { if (typeof detail.coinPub !== "string") { return Promise.reject(Error("coinPub missing")); } - return wallet.payback(detail.coinPub); - case "payment-failed": + 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"); - wallet.updateExchanges(); + needsWallet().updateExchanges(); return Promise.resolve(); - case "payment-succeeded": + } + case "payment-succeeded": { const contractTermsHash = detail.contractTermsHash; const merchantSig = detail.merchantSig; if (!contractTermsHash) { @@ -238,7 +282,20 @@ function handleMessage(db: IDBDatabase, if (!merchantSig) { return Promise.reject(Error("merchantSig missing")); } - return wallet.paymentSucceeded(contractTermsHash, merchantSig); + return needsWallet().paymentSucceeded(contractTermsHash, merchantSig); + } + case "check-upgrade": { + let dbResetRequired = false; + if (!currentWallet) { + dbResetRequired = true; + } + const resp: wxApi.UpgradeResponse = { + dbResetRequired, + currentDbVersion: DB_VERSION.toString(), + oldDbVersion: (oldDbVersion || "unknown").toString(), + } + return resp; + } default: // Exhaustiveness check. // See https://www.typescriptlang.org/docs/handbook/advanced-types.html @@ -246,9 +303,9 @@ function handleMessage(db: IDBDatabase, } } -async function dispatch(wallet: Wallet, req: any, sender: any, sendResponse: any): Promise<void> { +async function dispatch(req: any, sender: any, sendResponse: any): Promise<void> { try { - const p = handleMessage(wallet.db, wallet, sender, req.type, req.detail); + const p = handleMessage(sender, req.type, req.detail); const r = await p; try { sendResponse(r); @@ -428,6 +485,11 @@ function clearRateLimitCache() { */ let currentWallet: Wallet|undefined; +/** + * Last version if an outdated DB, if applicable. + */ +let oldDbVersion: number|undefined; + async function reinitWallet() { if (currentWallet) { @@ -548,13 +610,7 @@ export async function wxMain() { // Handlers for messages coming directly from the content // script on the page chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { - const wallet = currentWallet; - if (!wallet) { - console.warn("wallet not available while handling message"); - console.warn("dropped request message was", req); - return; - } - dispatch(wallet, req, sender, sendResponse); + dispatch(req, sender, sendResponse); return true; }); @@ -619,8 +675,10 @@ function openTalerDb(): Promise<IDBDatabase> { break; default: if (e.oldVersion !== DB_VERSION) { - window.alert("Incompatible wallet dababase version, please reset" + - " db."); + oldDbVersion = e.oldVersion; + chrome.tabs.create({ + url: chrome.extension.getURL("/src/webex/pages/reset-required.html"), + }); chrome.browserAction.setBadgeText({text: "err"}); chrome.browserAction.setBadgeBackgroundColor({color: "#F00"}); throw Error("incompatible DB"); diff --git a/tsconfig.json b/tsconfig.json index 7bcf7d495..349b5969a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -63,6 +63,7 @@ "src/webex/pages/logs.tsx", "src/webex/pages/payback.tsx", "src/webex/pages/popup.tsx", + "src/webex/pages/reset-required.tsx", "src/webex/pages/show-db.ts", "src/webex/pages/tree.tsx", "src/webex/renderHtml.tsx", diff --git a/webpack.config.js b/webpack.config.js index 60311aaa2..34342748b 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -73,6 +73,7 @@ module.exports = function (env) { "show-db": "./src/webex/pages/show-db.ts", "tree": "./src/webex/pages/tree.tsx", "payback": "./src/webex/pages/payback.tsx", + "reset-required": "./src/webex/pages/reset-required.tsx", }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ |