From 7079a5b2f2309c43e29696444088145321348d6c Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Tue, 19 Sep 2017 22:21:57 +0200 Subject: [PATCH 1/6] Moves eventStore into appStore Resolves #11009 --- app/browser/reducers/ledgerReducer.js | 221 ++ app/browser/reducers/pageDataReducer.js | 90 + app/browser/reducers/siteSettingsReducer.js | 86 + app/browser/tabs.js | 2 +- app/common/lib/ledgerUtil.js | 1853 +++++++++++++++- app/common/lib/pageDataUtil.js | 17 + app/common/state/ledgerState.js | 143 ++ app/common/state/pageDataState.js | 98 + app/common/state/siteSettingsState.js | 18 +- app/ledger.js | 1946 +---------------- app/sessionStore.js | 13 +- docs/state.md | 230 +- js/actions/appActions.js | 35 - js/constants/appConstants.js | 4 - js/stores/appStore.js | 106 +- js/stores/eventStore.js | 156 -- .../browser/reducers/pageDataReducerTest.js | 436 ++++ test/unit/app/common/lib/ledgerUtilTest.js | 35 +- test/unit/app/common/lib/pageDataUtilTest.js | 23 + .../app/common/state/pageDataStateTest.js | 304 +++ 20 files changed, 3551 insertions(+), 2265 deletions(-) create mode 100644 app/browser/reducers/ledgerReducer.js create mode 100644 app/browser/reducers/pageDataReducer.js create mode 100644 app/browser/reducers/siteSettingsReducer.js create mode 100644 app/common/lib/pageDataUtil.js create mode 100644 app/common/state/ledgerState.js create mode 100644 app/common/state/pageDataState.js delete mode 100644 js/stores/eventStore.js create mode 100644 test/unit/app/browser/reducers/pageDataReducerTest.js create mode 100644 test/unit/app/common/lib/pageDataUtilTest.js create mode 100644 test/unit/app/common/state/pageDataStateTest.js diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js new file mode 100644 index 00000000000..adfa8830a00 --- /dev/null +++ b/app/browser/reducers/ledgerReducer.js @@ -0,0 +1,221 @@ +/* This SourceCode Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Immutable = require('immutable') +const underscore = require('underscore') + +// Constants +const appConstants = require('../../../js/constants/appConstants') +const windowConstants = require('../../../js/constants/windowConstants') +const settings = require('../../../js/constants/settings') + +// State +const ledgerState = require('../../common/state/ledgerState') + +// Utils +const ledgerUtil = require('../../common/lib/ledgerUtil') +const {makeImmutable} = require('../../common/state/immutableUtil') +const getSetting = require('../../../js/settings').getSetting + +const ledgerReducer = (state, action, immutableAction) => { + action = immutableAction || makeImmutable(action) + switch (action.get('actionType')) { + case appConstants.APP_UPDATE_LEDGER_INFO: + { + state = state.setIn(['ledger', 'info'], action.get('ledgerInfo')) + break + } + // TODO refactor + case appConstants.APP_UPDATE_LOCATION_INFO: + { + state = state.setIn(['ledger', 'locations'], action.get('locationInfo')) + break + } + case appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED: + { + state = ledgerState.setRecoveryStatus(state, action.get('recoverySucceeded')) + break + } + case appConstants.APP_SET_STATE: + { + state = ledgerUtil.init(state) + break + } + case appConstants.APP_BACKUP_KEYS: + { + ledgerUtil.backupKeys(state, action.get('backupAction')) + break + } + case appConstants.APP_RECOVER_WALLET: + { + state = ledgerUtil.recoverKeys( + state, + action.get('useRecoveryKeyFile'), + action.get('firstRecoveryKey'), + action.get('secondRecoveryKey') + ) + break + } + case appConstants.APP_SHUTTING_DOWN: + { + state = ledgerUtil.quit(state) + break + } + case appConstants.APP_ON_CLEAR_BROWSING_DATA: + { + const defaults = state.get('clearBrowsingDataDefaults') + const temp = state.get('tempClearBrowsingData', Immutable.Map()) + const clearData = defaults ? defaults.merge(temp) : temp + if (clearData.get('browserHistory') && !getSetting(settings.PAYMENTS_ENABLED)) { + state = ledgerState.resetSynopsis(state) + } + break + } + // TODO not sure that we use APP_IDLE_STATE_CHANGED anymore + case appConstants.APP_IDLE_STATE_CHANGED: + { + state = ledgerUtil.pageDataChanged(state) + ledgerUtil.addVisit('NOOP', underscore.now(), null) + break + } + case appConstants.APP_CHANGE_SETTING: + { + switch (action.get('key')) { + case settings.PAYMENTS_ENABLED: + { + state = ledgerUtil.initialize(state, action.get('value')) + break + } + case settings.PAYMENTS_CONTRIBUTION_AMOUNT: + { + ledgerUtil.setPaymentInfo(action.get('value')) + break + } + case settings.PAYMENTS_MINIMUM_VISIT_TIME: + { + const value = action.get('value') + if (value <= 0) break + ledgerUtil.synopsis.options.minPublisherDuration = action.value + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + break + } + case settings.PAYMENTS_MINIMUM_VISITS: + { + const value = action.get('value') + if (value <= 0) break + + ledgerUtil.synopsis.options.minPublisherVisits = value + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + break + } + + case settings.PAYMENTS_ALLOW_NON_VERIFIED: + { + const value = action.get('value') + ledgerUtil.synopsis.options.showOnlyVerified = value + state = ledgerState.setSynopsisOption(state, 'showOnlyVerified', value) + break + } + } + break + } + case appConstants.APP_CHANGE_SITE_SETTING: + { + const pattern = action.get('hostPattern') + if (!pattern) { + console.warn('Changing site settings should always have a hostPattern') + break + } + const i = pattern.indexOf('://') + if (i === -1) break + + const publisherKey = pattern.substr(i + 3) + switch (action.get('key')) { + case 'ledgerPaymentsShown': + { + if (action.get('value') === false) { + delete ledgerUtil.synopsis.publishers[publisherKey] + state = ledgerState.deletePublishers(state, publisherKey) + state = ledgerUtil.updatePublisherInfo(state) + } + break + } + case 'ledgerPayments': + { + const publisher = ledgerState.getPublisher(state, publisherKey) + if (publisher.isEmpty()) { + break + } + state = ledgerUtil.updatePublisherInfo(state) + state = ledgerUtil.verifiedP(state, publisherKey) + break + } + case 'ledgerPinPercentage': + { + const publisher = ledgerState.getPublisher(state, publisherKey) + if (publisher.isEmpty()) { + break + } + + ledgerUtil.synopsis.publishers[publisherKey].pinPercentage = action.get('value') + state = ledgerUtil.updatePublisherInfo(state, publisherKey) + break + } + } + break + } + case appConstants.APP_REMOVE_SITE_SETTING: + { + const pattern = action.get('hostPattern') + if (!pattern) { + console.warn('Changing site settings should always have a hostPattern') + break + } + + const i = pattern.indexOf('://') + if (i === -1) break + + const publisherKey = pattern.substr(i + 3) + if (action.get('key') === 'ledgerPayments') { + const publisher = ledgerState.getPublisher(state, publisherKey) + if (publisher.isEmpty()) { + break + } + state = ledgerUtil.updatePublisherInfo(state) + } + break + } + case appConstants.APP_NETWORK_CONNECTED: + { + setTimeout((state) => { + ledgerUtil.networkConnected(state) + }, 1000, state) + break + } + case appConstants.APP_NAVIGATOR_HANDLER_REGISTERED: + { + const hasBitcoinHandler = (action.get('protocol') === 'bitcoin') + state = ledgerState.setInfoProp(state, 'hasBitcoinHandler', hasBitcoinHandler) + break + } + case appConstants.APP_NAVIGATOR_HANDLER_UNREGISTERED: + { + const hasBitcoinHandler = false + state = ledgerState.setInfoProp(state, 'hasBitcoinHandler', hasBitcoinHandler) + break + } + case 'event-set-page-info': + case appConstants.APP_WINDOW_BLURRED: + case appConstants.APP_CLOSE_WINDOW: + case windowConstants.WINDOW_SET_FOCUSED_FRAME: + case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: + { + state = ledgerUtil.pageDataChanged(state) + break + } + } + return state +} + +module.exports = ledgerReducer diff --git a/app/browser/reducers/pageDataReducer.js b/app/browser/reducers/pageDataReducer.js new file mode 100644 index 00000000000..74eaf9df48e --- /dev/null +++ b/app/browser/reducers/pageDataReducer.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const electron = require('electron') +const BrowserWindow = electron.BrowserWindow + +// Constants +const appConstants = require('../../../js/constants/appConstants') +const windowConstants = require('../../../js/constants/windowConstants') + +// State +const pageDataState = require('../../common/state/pageDataState') + +// Utils +const {makeImmutable} = require('../../common/state/immutableUtil') +const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {responseHasContent} = require('../../common/lib/httpUtil') + +const pageDataReducer = (state, action, immutableAction) => { + action = immutableAction || makeImmutable(action) + switch (action.get('actionType')) { + case windowConstants.WINDOW_SET_FOCUSED_FRAME: + { + if (action.get('location')) { + state = pageDataState.addView(state, action.get('location'), action.get('tabId')) + } + break + } + case appConstants.APP_WINDOW_BLURRED: + { + let windowCount = BrowserWindow.getAllWindows().filter((win) => win.isFocused()).length + if (windowCount === 0) { + state = pageDataState.addView(state) + } + break + } + // TODO check if this is used anymore + case appConstants.APP_IDLE_STATE_CHANGED: + { + if (action.has('idleState') && action.get('idleState') !== 'active') { + state = pageDataState.addView(state) + } + break + } + case appConstants.APP_WINDOW_CLOSED: + { + state = pageDataState.addView(state) + break + } + case 'event-set-page-info': + { + // retains all past pages, not really sure that's needed... [MTR] + state = pageDataState.addInfo(state, action.get('pageInfo')) + break + } + case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: + { + // Only capture response for the page (not subresources, like images, JavaScript, etc) + if (action.getIn(['details', 'resourceType']) === 'mainFrame') { + const pageUrl = action.getIn(['details', 'newURL']) + + // create a page view event if this is a page load on the active tabId + const lastActiveTabId = pageDataState.getLastActiveTabId(state) + const tabId = action.get('tabId') + if (!lastActiveTabId || tabId === lastActiveTabId) { + state = pageDataState.addView(state, pageUrl, tabId) + } + + const responseCode = action.getIn(['details', 'httpResponseCode']) + if (isSourceAboutUrl(pageUrl) || !responseHasContent(responseCode)) { + break + } + + const pageLoadEvent = makeImmutable({ + timestamp: new Date().getTime(), + url: pageUrl, + tabId: tabId, + details: action.get('details') + }) + state = pageDataState.addLoad(state, pageLoadEvent) + } + break + } + } + + return state +} + +module.exports = pageDataReducer diff --git a/app/browser/reducers/siteSettingsReducer.js b/app/browser/reducers/siteSettingsReducer.js new file mode 100644 index 00000000000..e0a7be29800 --- /dev/null +++ b/app/browser/reducers/siteSettingsReducer.js @@ -0,0 +1,86 @@ +/* This SourceCode Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' +const Immutable = require('immutable') +const appConstants = require('../../../js/constants/appConstants') +const siteSettings = require('../../../js/state/siteSettings') +const urlUtil = require('../../../js/lib/urlutil') +const {makeImmutable} = require('../../common/state/immutableUtil') + +const siteSettingsReducer = (state, action, immutableAction) => { + action = immutableAction || makeImmutable(action) + switch (action.get('actionType')) { + case appConstants.APP_ALLOW_FLASH_ONCE: + { + const propertyName = action.get('isPrivate') ? 'temporarySiteSettings' : 'siteSettings' + state = state.set(propertyName, + siteSettings.mergeSiteSetting(state.get(propertyName), urlUtil.getOrigin(action.get('url')), 'flash', 1)) + break + } + case appConstants.APP_ALLOW_FLASH_ALWAYS: + { + const propertyName = action.get('isPrivate') ? 'temporarySiteSettings' : 'siteSettings' + const expirationTime = Date.now() + (7 * 24 * 3600 * 1000) + state = state.set(propertyName, + siteSettings.mergeSiteSetting(state.get(propertyName), urlUtil.getOrigin(action.get('url')), 'flash', expirationTime)) + break + } + case appConstants.APP_CHANGE_SITE_SETTING: + { + let propertyName = action.get('temporary') ? 'temporarySiteSettings' : 'siteSettings' + let newSiteSettings = siteSettings.mergeSiteSetting(state.get(propertyName), action.get('hostPattern'), action.get('key'), action.get('value')) + if (action.get('skipSync')) { + newSiteSettings = newSiteSettings.setIn([action.get('hostPattern'), 'skipSync'], true) + } + state = state.set(propertyName, newSiteSettings) + break + } + case appConstants.APP_REMOVE_SITE_SETTING: + { + let propertyName = action.get('temporary') ? 'temporarySiteSettings' : 'siteSettings' + let newSiteSettings = siteSettings.removeSiteSetting(state.get(propertyName), + action.get('hostPattern'), action.get('key')) + if (action.get('skipSync')) { + newSiteSettings = newSiteSettings.setIn([action.get('hostPattern'), 'skipSync'], true) + } + state = state.set(propertyName, newSiteSettings) + break + } + case appConstants.APP_CLEAR_SITE_SETTINGS: + { + let propertyName = action.get('temporary') ? 'temporarySiteSettings' : 'siteSettings' + let newSiteSettings = new Immutable.Map() + state.get(propertyName).map((entry, hostPattern) => { + let newEntry = entry.delete(action.get('key')) + if (action.get('skipSync')) { + newEntry = newEntry.set('skipSync', true) + } + newSiteSettings = newSiteSettings.set(hostPattern, newEntry) + }) + state = state.set(propertyName, newSiteSettings) + break + } + case appConstants.APP_ADD_NOSCRIPT_EXCEPTIONS: + { + const origin = action.get('origins') + const hostPattern = action.get('hostPattern') + const propertyName = action.get('temporary') ? 'temporarySiteSettings' : 'siteSettings' + // Note that this is always cleared on restart or reload, so should not + // be synced or persisted. + const key = 'noScriptExceptions' + if (!origin || !origin.size) { + // Clear the exceptions + state = state.setIn([propertyName, hostPattern, key], new Immutable.Map()) + } else { + const currentExceptions = state.getIn([propertyName, hostPattern, key]) || new Immutable.Map() + state = state.setIn([propertyName, hostPattern, key], currentExceptions.merge(origin)) + } + break + } + } + return state +} + +module.exports = siteSettingsReducer diff --git a/app/browser/tabs.js b/app/browser/tabs.js index 5affb1fd8ea..8e6101337cb 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -538,7 +538,7 @@ const api = { tab.on('did-get-response-details', (evt, status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers, resourceType) => { if (resourceType === 'mainFrame') { - windowActions.gotResponseDetails(tabId, {status, newURL, originalURL, httpResponseCode, requestMethod, referrer, headers, resourceType}) + windowActions.gotResponseDetails(tabId, {status, newURL, originalURL, httpResponseCode, requestMethod, referrer, resourceType}) } }) }) diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index e788259bef2..89625ae8b92 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -4,44 +4,143 @@ 'use strict' -const {responseHasContent} = require('./httpUtil') +const acorn = require('acorn') const moment = require('moment') +const Immutable = require('immutable') +const electron = require('electron') +const path = require('path') +const os = require('os') +const qr = require('qr-image') +const underscore = require('underscore') +const tldjs = require('tldjs') +const urlFormat = require('url').format +const queryString = require('queryString') +const levelUp = require('level') +const random = require('random-lib') + +// Actions +const appActions = require('../../../js/actions/appActions') + +// State +const ledgerState = require('../state/ledgerState') +const pageDataState = require('../state/pageDataState') + +// Constants +const settings = require('../../../js/constants/settings') + +// Utils +const {responseHasContent} = require('./httpUtil') +const {makeImmutable} = require('../../common/state/immutableUtil') +const tabs = require('../../browser/tabs') +const locale = require('../../locale') +const siteSettingsState = require('../state/siteSettingsState') +const appConfig = require('../../../js/constants/appConfig') +const getSetting = require('../../../js/settings').getSetting +const {fileUrl} = require('../../../js/lib/appUrlUtil') +const urlParse = require('../urlParse') +const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation') +const request = require('../../../js/lib/request') + +let ledgerPublisher +let ledgerClient +let ledgerBalance +let client +let locationDefault = 'NOOP' +let currentUrl = locationDefault +let currentTimestamp = new Date().getTime() +let visitsByPublisher = {} +let synopsis +let notificationTimeout +let runTimeoutId + +// Database +let v2RulesetDB +const v2RulesetPath = 'ledger-rulesV2.leveldb' +let v2PublishersDB +const v2PublishersPath = 'ledger-publishersV2.leveldb' +const statePath = 'ledger-state.json' + +const miliseconds = { + year: 365 * 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000 +} + +const clientOptions = { + debugP: process.env.LEDGER_DEBUG, + loggingP: process.env.LEDGER_LOGGING, + rulesTestP: process.env.LEDGER_RULES_TESTING, + verboseP: process.env.LEDGER_VERBOSE, + server: process.env.LEDGER_SERVER_URL, + createWorker: electron.app.createWorker +} + +const ledgerInfo = { + _internal: { + paymentInfo: {} + } +} + +// TODO only temporally so that standard is happy +const publisherInfo = { + _internal: { + verboseP: true, + debugP: true, + enabled: false, + ruleset: { + raw: [], + cooked: [] + } + } +} /** * Is page an actual page being viewed by the user? (not an error page, etc) * If the page is invalid, we don't want to collect usage info. - * @param {Object} view - an entry from page_view (from EventStore) - * @param {Object} responseList - full page_response array (from EventStore) + * @param {Map} view - an entry from ['pageData', 'view'] + * @param {List} responseList - full ['pageData', 'load'] List * @return {boolean} true if page should have usage collected, false if not */ -module.exports.shouldTrackView = (view, responseList) => { - if (!view || !view.url || !view.tabId) { +const shouldTrackView = (view, responseList) => { + view = makeImmutable(view) + + if (view == null) { return false } - if (!responseList || !Array.isArray(responseList) || !responseList.length) { + + const tabId = view.get('tabId') + const url = view.get('url') + + if (!url || !tabId) { return false } - const tabId = view.tabId - const url = view.url + responseList = makeImmutable(responseList) + if (!responseList || responseList.size === 0) { + return false + } - for (let i = responseList.length; i > -1; i--) { - const response = responseList[i] + for (let i = (responseList.size - 1); i > -1; i--) { + const response = responseList.get(i) - if (!response) continue + if (!response) { + continue + } - const responseUrl = response && response.details - ? response.details.newURL - : null + const responseUrl = response.getIn(['details', 'newURL'], null) - if (url === responseUrl && response.tabId === tabId) { - return responseHasContent(response.details.httpResponseCode) + if (url === responseUrl && response.get('tabId') === tabId) { + return responseHasContent(response.getIn(['details', 'httpResponseCode'])) } } + return false } -module.exports.btcToCurrencyString = (btc, ledgerData) => { +const btcToCurrencyString = (btc, ledgerData) => { const balance = Number(btc || 0) const currency = ledgerData.get('currency') || 'USD' @@ -69,17 +168,17 @@ module.exports.btcToCurrencyString = (btc, ledgerData) => { return `${balance} BTC` } -module.exports.formattedTimeFromNow = (timestamp) => { +const formattedTimeFromNow = (timestamp) => { moment.locale(navigator.language) return moment(new Date(timestamp)).fromNow() } -module.exports.formattedDateFromTimestamp = (timestamp, format) => { +const formattedDateFromTimestamp = (timestamp, format) => { moment.locale(navigator.language) return moment(new Date(timestamp)).format(format) } -module.exports.walletStatus = (ledgerData) => { +const walletStatus = (ledgerData) => { let status = {} if (ledgerData.get('error')) { @@ -93,7 +192,7 @@ module.exports.walletStatus = (ledgerData) => { status.id = 'insufficientFundsStatus' } else if (pendingFunds > 0) { status.id = 'pendingFundsStatus' - status.args = {funds: module.exports.btcToCurrencyString(pendingFunds, ledgerData)} + status.args = {funds: btcToCurrencyString(pendingFunds, ledgerData)} } else if (transactions && transactions.size > 0) { status.id = 'defaultWalletStatus' } else { @@ -106,3 +205,1715 @@ module.exports.walletStatus = (ledgerData) => { } return status } + +const promptForRecoveryKeyFile = () => { + const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + let files + if (process.env.SPECTRON) { + // skip the dialog for tests + console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) + files = [defaultRecoveryKeyFilePath] + } else { + const dialog = electron.dialog + files = dialog.showOpenDialog({ + properties: ['openFile'], + defaultPath: defaultRecoveryKeyFilePath, + filters: [{ + name: 'TXT files', + extensions: ['txt'] + }] + }) + } + + return (files && files.length ? files[0] : null) +} + +const logError = (state, err, caller) => { + if (err) { + console.error('Error in %j: %j', caller, err) + state = ledgerState.setLedgerError(state, err, caller) + } else { + state = ledgerState.setLedgerError(state) + } + + return state +} + +const loadKeysFromBackupFile = (state, filePath) => { + let keys = null + const fs = require('fs') + let data = fs.readFileSync(filePath) + + if (!data || !data.length || !(data.toString())) { + state = logError(state, 'No data in backup file', 'recoveryWallet') + } else { + try { + const recoveryFileContents = data.toString() + + let messageLines = recoveryFileContents.split(os.EOL) + + let paymentIdLine = '' || messageLines[3] + let passphraseLine = '' || messageLines[4] + + const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' ')) + const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] + + const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' ')) + const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] + + keys = { + paymentId, + passphrase + } + } catch (exc) { + state = logError(state, exc, 'recoveryWallet') + } + } + + return { + state, + keys + } +} + +const getPublisherData = (result, scorekeeper) => { + let duration = result.duration + + let data = { + verified: result.options.verified || false, + site: result.publisher, + views: result.visits, + duration: duration, + daysSpent: 0, + hoursSpent: 0, + minutesSpent: 0, + secondsSpent: 0, + faviconURL: result.faviconURL, + score: result.scores[scorekeeper], + pinPercentage: result.pinPercentage, + weight: result.pinPercentage + } + // HACK: Protocol is sometimes blank here, so default to http:// so we can + // still generate publisherURL. + data.publisherURL = (result.protocol || 'http:') + '//' + result.publisher + + if (duration >= miliseconds.day) { + data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1) + } else if (duration >= miliseconds.hour) { + data.hoursSpent = Math.max(Math.floor(duration / miliseconds.hour), 1) + data.minutesSpent = Math.round((duration % miliseconds.hour) / miliseconds.minute) + } else if (duration >= miliseconds.minute) { + data.minutesSpent = Math.max(Math.round(duration / miliseconds.minute), 1) + data.secondsSpent = Math.round((duration % miliseconds.minute) / miliseconds.second) + } else { + data.secondsSpent = Math.max(Math.round(duration / miliseconds.second), 1) + } + + return data +} + +const normalizePinned = (dataPinned, total, target, setOne) => { + return dataPinned.map((publisher) => { + let newPer + let floatNumber + + if (setOne) { + newPer = 1 + floatNumber = 1 + } else { + floatNumber = (publisher.pinPercentage / total) * target + newPer = Math.floor(floatNumber) + if (newPer < 1) { + newPer = 1 + } + } + + publisher.weight = floatNumber + publisher.pinPercentage = newPer + return publisher + }) +} + +// courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 +const roundToTarget = (l, target, property) => { + let off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x[property]) }, 0) + + return underscore.sortBy(l, (x) => Math.round(x[property]) - x[property]) + .map((x, i) => { + x[property] = Math.round(x[property]) + (off > i) - (i >= (l.length + off)) + return x + }) +} + +// TODO rename function +const blockedP = (state, publisherKey) => { + const pattern = `https?://${publisherKey}` + const ledgerPaymentsShown = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPaymentsShown') + + return ledgerPaymentsShown === false +} + +// TODO rename function +const stickyP = (state, publisherKey) => { + const pattern = `https?://${publisherKey}` + let result = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPayments') + + // NB: legacy clean-up + if ((typeof result === 'undefined') && (typeof synopsis.publishers[publisherKey].options.stickyP !== 'undefined')) { + result = synopsis.publishers[publisherKey].options.stickyP + appActions.changeSiteSetting(pattern, 'ledgerPayments', result) + } + if (synopsis.publishers[publisherKey] && + synopsis.publishers[publisherKey].options && + synopsis.publishers[publisherKey].options.stickyP) { + delete synopsis.publishers[publisherKey].options.stickyP + } + + return (result === undefined || result) +} + +// TODO rename function +const eligibleP = (state, publisherKey) => { + if (!synopsis.options.minPublisherDuration && process.env.NODE_ENV !== 'test') { + // TODO make sure that appState has correct data in + synopsis.options.minPublisherDuration = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) + } + + const scorekeeper = ledgerState.getSynopsisOption(state, 'scorekeeper') + const minPublisherDuration = ledgerState.getSynopsisOption(state, 'minPublisherDuration') + const minPublisherVisits = ledgerState.getSynopsisOption(state, 'minPublisherVisits') + const publisher = ledgerState.getPublisher(state, publisherKey) + + return ( + publisher.getIn(['scores', scorekeeper]) > 0 && + publisher.get('duration') >= minPublisherDuration && + publisher.get('visits') >= minPublisherVisits + ) +} + +// TODO rename function +const visibleP = (state, publisherKey) => { + const publisher = ledgerState.getPublisher(state, publisherKey) + // TODO you stopped here + let showOnlyVerified = ledgerState.getSynopsisOption(state, 'showOnlyVerified') + if (showOnlyVerified == null) { + showOnlyVerified = getSetting(settings.PAYMENTS_ALLOW_NON_VERIFIED) + state = ledgerState.setSynopsisOption(state, 'showOnlyVerified', showOnlyVerified) + synopsis.options.showOnlyVerified = showOnlyVerified + } + + const publisherOptions = publisher.get('options', Immutable.Map()) + const onlyVerified = !showOnlyVerified + + // Publisher Options + const excludedByUser = blockedP(state, publisherKey) + const eligibleByPublisherToggle = stickyP(state, publisherKey) != null + const eligibleByNumberOfVisits = eligibleP(state, publisherKey) + const isInExclusionList = publisherOptions.get('exclude') + const verifiedPublisher = publisherOptions.get('verified') + + // websites not included in exclusion list are eligible by number of visits + // but can be enabled by user action in the publisher toggle + const isEligible = (eligibleByNumberOfVisits && !isInExclusionList) || eligibleByPublisherToggle + + // If user decide to remove the website, don't show it. + if (excludedByUser) { + return false + } + + // Unless user decided to enable publisher with publisherToggle, + // do not show exclusion list. + if (!eligibleByPublisherToggle && isInExclusionList) { + return false + } + + // If verified option is set, only show verified publishers + if (isEligible && onlyVerified) { + return verifiedPublisher + } + + return isEligible +} + +const synopsisNormalizer = (state, publishers, options, changedPublisher) => { + let results + let dataPinned = [] + let dataUnPinned = [] + let dataExcluded = [] + let pinnedTotal = 0 + let unPinnedTotal = 0 + const scorekeeper = options.scorekeeper + + results = [] // TODO convert to Immutable.List + publishers.forEach((publisher, index) => { + if (!visibleP(state, index)) { + return + } + + publisher.publisher = index + results.push(publisher) + }) + results = underscore.sortBy(results, (entry) => { return -entry.scores[scorekeeper] }) + + // move publisher to the correct array and get totals + results.forEach((result) => { + if (result.pinPercentage && result.pinPercentage > 0) { + // pinned + pinnedTotal += result.pinPercentage + dataPinned.push(getPublisherData(result, scorekeeper)) + } else if (stickyP(result.publisher)) { + // unpinned + unPinnedTotal += result.scores[scorekeeper] + dataUnPinned.push(result) + } else { + // excluded + let publisher = getPublisherData(result, scorekeeper) + publisher.percentage = 0 + publisher.weight = 0 + dataExcluded.push(publisher) + } + }) + + // round if over 100% of pinned publishers + if (pinnedTotal > 100) { + if (changedPublisher) { + const changedObject = dataPinned.filter(publisher => publisher.site === changedPublisher)[0] + const setOne = changedObject.pinPercentage > (100 - dataPinned.length - 1) + + if (setOne) { + changedObject.pinPercentage = 100 - dataPinned.length + 1 + changedObject.weight = changedObject.pinPercentage + } + + const pinnedRestTotal = pinnedTotal - changedObject.pinPercentage + dataPinned = dataPinned.filter(publisher => publisher.site !== changedPublisher) + dataPinned = normalizePinned(dataPinned, pinnedRestTotal, (100 - changedObject.pinPercentage), setOne) + dataPinned = roundToTarget(dataPinned, (100 - changedObject.pinPercentage), 'pinPercentage') + + dataPinned.push(changedObject) + } else { + dataPinned = normalizePinned(dataPinned, pinnedTotal, 100) + dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') + } + + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result, scorekeeper) + publisher.percentage = 0 + publisher.weight = 0 + return publisher + }) + + // sync app store + state = ledgerState.changePinnedValues(dataPinned) + } else if (dataUnPinned.length === 0 && pinnedTotal < 100) { + // when you don't have any unpinned sites and pinned total is less then 100 % + dataPinned = normalizePinned(dataPinned, pinnedTotal, 100, false) + dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') + + // sync app store + state = ledgerState.changePinnedValues(dataPinned) + } else { + // unpinned publishers + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result, scorekeeper) + const floatNumber = (publisher.score / unPinnedTotal) * (100 - pinnedTotal) + publisher.percentage = Math.round(floatNumber) + publisher.weight = floatNumber + return publisher + }) + + // normalize unpinned values + dataUnPinned = roundToTarget(dataUnPinned, (100 - pinnedTotal), 'percentage') + } + + const newData = dataPinned.concat(dataUnPinned, dataExcluded) + + // sync synopsis + newData.forEach((item) => { + synopsis.publishers[item.site].weight = item.weight + synopsis.publishers[item.site].pinPercentage = item.pinPercentage + }) + + return ledgerState.saveSynopsis(state, newData, options) +} + +// TODO make sure that every call assign state +const updatePublisherInfo = (state, changedPublisher) => { + if (!getSetting(settings.PAYMENTS_ENABLED)) { + return + } + + const options = synopsis.options + state = synopsisNormalizer(state, synopsis.publishers, options, changedPublisher) + + if (publisherInfo._internal.debugP) { + const data = [] + synopsis.publishers.forEach((entry) => { + data.push(underscore.extend(underscore.omit(entry, [ 'faviconURL' ]), { faviconURL: entry.faviconURL && '...' })) + }) + + console.log('\nupdatePublisherInfo: ' + JSON.stringify({ options: options, synopsis: data }, null, 2)) + } + + return state +} + +// TODO rename function name +// TODO make sure that every call assign state +const verifiedP = (state, publisherKey, callback) => { + inspectP(v2PublishersDB, v2PublishersPath, publisherKey, 'verified', null, callback) + + if (process.env.NODE_ENV === 'test') { + ['brianbondy.com', 'clifton.io'].forEach((key) => { + const publisher = ledgerState.getPublisher(state, key) + if (!publisher.isEmpty()) { + state = ledgerState.setSynopsisOption(state, 'verified', true) + } + }) + state = updatePublisherInfo(state) + } + + return state +} + +// TODO refactor +const inspectP = (db, path, publisher, property, key, callback) => { + var done = (err, result) => { + if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && + (synopsis.publishers[publisher].options[property] !== result[property])) { + synopsis.publishers[publisher].options[property] = result[property] + updatePublisherInfo() + } + + if (callback) callback(err, result) + } + + if (!key) key = publisher + db.get(key, (err, value) => { + var result + + if (err) { + if (!err.notFound) console.error(path + ' get ' + key + ' error: ' + JSON.stringify(err, null, 2)) + return done(err) + } + + try { + result = JSON.parse(value) + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid JSON ' + key + ': ' + value) + result = {} + } + + done(null, result) + }) +} + +// TODO refactor +const excludeP = (publisher, callback) => { + var doneP + + var done = (err, result) => { + doneP = true + if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && + (synopsis.publishers[publisher].options.exclude !== result)) { + synopsis.publishers[publisher].options.exclude = result + updatePublisherInfo() + } + + if (callback) callback(err, result) + } + + if (!v2RulesetDB) return setTimeout(() => { excludeP(publisher, callback) }, 5 * miliseconds.second) + + inspectP(v2RulesetDB, v2RulesetPath, publisher, 'exclude', 'domain:' + publisher, (err, result) => { + var props + + if (!err) return done(err, result.exclude) + + props = ledgerPublisher.getPublisherProps('https://' + publisher) + if (!props) return done() + + v2RulesetDB.createReadStream({ lt: 'domain:' }).on('data', (data) => { + var regexp, result, sldP, tldP + + if (doneP) return + + sldP = data.key.indexOf('SLD:') === 0 + tldP = data.key.indexOf('TLD:') === 0 + if ((!tldP) && (!sldP)) return + + if (underscore.intersection(data.key.split(''), + [ '^', '$', '*', '+', '?', '[', '(', '{', '|' ]).length === 0) { + if ((data.key !== ('TLD:' + props.TLD)) && (props.SLD && data.key !== ('SLD:' + props.SLD.split('.')[0]))) return + } else { + try { + regexp = new RegExp(data.key.substr(4)) + if (!regexp.test(props[tldP ? 'TLD' : 'SLD'])) return + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid regexp ' + data.key + ': ' + ex.toString()) + } + } + + try { + result = JSON.parse(data.value) + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid JSON ' + data.entry + ': ' + data.value) + } + + done(null, result.exclude) + }).on('error', (err) => { + console.error(v2RulesetPath + ' stream error: ' + JSON.stringify(err, null, 2)) + }).on('close', () => { + }).on('end', () => { + if (!doneP) done(null, false) + }) + }) +} + +const setLocation = (state, timestamp, tabId) => { + if (!synopsis) { + return + } + + const locationData = ledgerState.getLocation(currentUrl) + if (publisherInfo._internal.verboseP) { + console.log( + `locations[${currentUrl}]=${JSON.stringify(locationData, null, 2)} ` + + `duration=${(timestamp - currentTimestamp)} msec tabId= ${tabId}` + ) + } + if (!locationData || !tabId) { + return state + } + + let publisherKey = locationData.get('publisher') + if (!publisherKey) { + return state + } + + if (!visitsByPublisher[publisherKey]) { + visitsByPublisher[publisherKey] = {} + } + + if (!visitsByPublisher[publisherKey][currentUrl]) { + visitsByPublisher[publisherKey][currentUrl] = { + tabIds: [] + } + } + + const revisitP = visitsByPublisher[publisherKey][currentUrl].tabIds.indexOf(tabId) !== -1 + if (!revisitP) { + visitsByPublisher[publisherKey][currentUrl].tabIds.push(tabId) + } + + let duration = timestamp - currentTimestamp + if (publisherInfo._internal.verboseP) { + console.log('\nadd publisher ' + publisherKey + ': ' + duration + ' msec' + ' revisitP=' + revisitP + ' state=' + + JSON.stringify(underscore.extend({ location: currentUrl }, visitsByPublisher[publisherKey][currentUrl]), + null, 2)) + } + + synopsis.addPublisher(publisherKey, { duration: duration, revisitP: revisitP }) + state = updatePublisherInfo(state) + state = verifiedP(state, publisherKey) + + return state +} + +const addVisit = (state, location, timestamp, tabId) => { + if (location === currentUrl) { + return state + } + + state = setLocation(state, timestamp, tabId) + + currentUrl = location.match(/^about/) ? locationDefault : location + currentTimestamp = timestamp + return state +} + +// TODO refactor +const pageDataChanged = (state) => { + // NB: in theory we have already seen every element in info except for (perhaps) the last one... + const info = pageDataState.getLastInfo(state) + + if (!synopsis || info.isEmpty()) { + return + } + + if (info.get('url', '').match(/^about/)) { + return + } + + let publisher = info.get('publisher') + const location = info.get('key') + if (publisher) { + // TODO refactor + if (synopsis.publishers[publisher] && + (typeof synopsis.publishers[publisher].faviconURL === 'undefined' || synopsis.publishers[publisher].faviconURL === null)) { + getFavIcon(synopsis.publishers[publisher], info, location) + } + + // TODO refactor + return updateLocation(location, publisher) + } else { + try { + publisher = ledgerPublisher.getPublisher(location, publisherInfo._internal.ruleset.raw) + // TODO refactor + if (publisher && !blockedP(state, publisher)) { + state = pageDataState.setPublisher(state, location, publisher) + } else { + publisher = null + } + } catch (ex) { + console.error('getPublisher error for ' + location + ': ' + ex.toString()) + } + } + + if (!publisher) { + return + } + + const pattern = `https?://${publisher}` + const initP = !synopsis.publishers[publisher] + // TODO refactor + synopsis.initPublisher(publisher) + + if (initP) { + // TODO refactor + state = excludeP(state, publisher, (unused, exclude) => { + if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { + exclude = false + } else { + exclude = !exclude + } + appActions.changeSiteSetting(pattern, 'ledgerPayments', exclude) + updatePublisherInfo() + }) + } + // TODO refactor + updateLocation(location, publisher) + // TODO refactor + getFavIcon(synopsis.publishers[publisher], info, location) + + const pageLoad = pageDataState.getLoad(state) + const view = pageDataState.getView(state) + + if (shouldTrackView(view, pageLoad)) { + // TODO refactor + addVisit(view.get('url', 'NOOP'), view.get('timestamp', underscore.now()), view.get('tabId')) + } + + return state +} + +const backupKeys = (state, backupAction) => { + const date = moment().format('L') + const paymentId = state.getIn(['ledgerInfo', 'paymentId']) + const passphrase = state.getIn(['ledgerInfo', 'passphrase']) + + const messageLines = [ + locale.translation('ledgerBackupText1'), + [locale.translation('ledgerBackupText2'), date].join(' '), + '', + [locale.translation('ledgerBackupText3'), paymentId].join(' '), + [locale.translation('ledgerBackupText4'), passphrase].join(' '), + '', + locale.translation('ledgerBackupText5') + ] + + const message = messageLines.join(os.EOL) + const filePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + + const fs = require('fs') + fs.writeFile(filePath, message, (err) => { + if (err) { + console.error(err) + } else { + tabs.create({url: fileUrl(filePath)}, (webContents) => { + if (backupAction === 'print') { + webContents.print({silent: false, printBackground: false}) + } else { + webContents.downloadURL(fileUrl(filePath), true) + } + }) + } + }) +} + +const recoverKeys = (state, useRecoveryKeyFile, firstKey, secondKey) => { + let firstRecoveryKey, secondRecoveryKey + + if (useRecoveryKeyFile) { + let recoveryKeyFile = promptForRecoveryKeyFile() + if (!recoveryKeyFile) { + // user canceled from dialog, we abort without error + return + } + + if (recoveryKeyFile) { + const result = loadKeysFromBackupFile(state, recoveryKeyFile) + const keys = result.keys || {} + state = result.state + + if (keys) { + firstRecoveryKey = keys.paymentId + secondRecoveryKey = keys.passphrase + } + } + } + + if (!firstRecoveryKey || !secondRecoveryKey) { + firstRecoveryKey = firstKey + secondRecoveryKey = secondKey + } + + const UUID_REGEX = /^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/ + if ( + typeof firstRecoveryKey !== 'string' || + !firstRecoveryKey.match(UUID_REGEX) || + typeof secondRecoveryKey !== 'string' || + !secondRecoveryKey.match(UUID_REGEX) + ) { + // calling logError sets the error object + state = logError(state, true, 'recoverKeys') + state = ledgerState.setRecoveryStatus(state, false) + return state + } + + // TODO should we change this to async await? + // TODO enable when ledger will work again + /* + client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, result) => { + let existingLedgerError = ledgerInfo.error + + if (err) { + // we reset ledgerInfo.error to what it was before (likely null) + // if ledgerInfo.error is not null, the wallet info will not display in UI + // logError sets ledgerInfo.error, so we must we clear it or UI will show an error + state = logError(err, 'recoveryWallet') + appActions.updateLedgerInfoProp('error', existingLedgerError) + // appActions.ledgerRecoveryFailed() TODO update based on top comment (async) + } else { + callback(err, result) + + if (balanceTimeoutId) { + clearTimeout(balanceTimeoutId) + } + getBalance() + // appActions.ledgerRecoverySucceeded() TODO update based on top comment (async) + } + }) + */ + + return state +} + +const quit = (state) => { + // quitP = true TODO remove if not needed + state = addVisit(state, locationDefault, new Date().getTime(), null) + + if ((!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { + state = ledgerState.resetSynopsis(state) + } + + return state +} + +const initSynopsis = (state) => { + // cf., the `Synopsis` constructor, https://github.com/brave/ledger-publisher/blob/master/index.js#L167 + let value = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) + if (!value) { + value = 8 * 1000 + appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME, value) + } + + // for earlier versions of the code... + if ((value > 0) && (value < 1000)) { + value = value * 1000 + synopsis.options.minPublisherDuration = value + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + } + + value = getSetting(settings.PAYMENTS_MINIMUM_VISITS) + if (!value) { + value = 1 + appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISITS, value) + } + + if (value > 0) { + synopsis.options.minPublisherVisits = value + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + } + + if (process.env.NODE_ENV === 'test') { + synopsis.options.minPublisherDuration = 0 + synopsis.options.minPublisherVisits = 0 + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', 0) + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', 0) + } else { + if (process.env.LEDGER_PUBLISHER_MIN_DURATION) { + value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_DURATION) + synopsis.options.minPublisherDuration = value + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + } + if (process.env.LEDGER_PUBLISHER_MIN_VISITS) { + value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_VISITS) + synopsis.options.minPublisherVisits = value + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + } + } + + underscore.keys(synopsis.publishers).forEach((publisher) => { + excludeP(publisher) + state = verifiedP(state, publisher) + }) + + state = updatePublisherInfo(state) + + return state +} + +const enable = (state, paymentsEnabled) => { + if (paymentsEnabled && !getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) + } + + publisherInfo._internal.enabled = paymentsEnabled + if (synopsis) { + return updatePublisherInfo(state) + } + + if (!ledgerPublisher) { + ledgerPublisher = require('ledger-publisher') + } + synopsis = new (ledgerPublisher.Synopsis)() + const stateSynopsis = ledgerState.getSynopsis(state) + + if (publisherInfo._internal.verboseP) { + console.log('\nstarting up ledger publisher integration') + } + + if (stateSynopsis.isEmpty()) { + return initSynopsis(state) + } + + try { + synopsis = new (ledgerPublisher.Synopsis)(stateSynopsis) + } catch (ex) { + console.error('synopsisPath parse error: ' + ex.toString()) + } + + state = initSynopsis(state) + + // synopsis cleanup + underscore.keys(synopsis.publishers).forEach((publisher) => { + if (synopsis.publishers[publisher].faviconURL === null) { + delete synopsis.publishers[publisher].faviconURL + } + }) + + // change undefined include publishers to include publishers + state = ledgerState.enableUndefinedPublishers(state, stateSynopsis.get('publishers')) + + return state +} + +const pathName = (name) => { + const parts = path.parse(name) + return path.join(electron.app.getPath('userData'), parts.name + parts.ext) +} + +const sufficientBalanceToReconcile = (state) => { + const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) + const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0) + const btc = ledgerState.getInfoProp(state, 'btc') + return btc && (balance + unconfirmed > 0.9 * Number(btc)) +} + +const shouldShowNotificationReviewPublishers = () => { + const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP) + return !nextTime || (underscore.now() > nextTime) +} + +const shouldShowNotificationAddFunds = () => { + const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP) + return !nextTime || (underscore.now() > nextTime) +} + +const showNotificationReviewPublishers = (nextTime) => { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('reconciliationNotification'), + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('dismiss')}, + {text: locale.translation('reviewSites'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +const showNotificationAddFunds = () => { + const nextTime = underscore.now() + (3 * miliseconds.day) + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('addFundsNotification'), + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('updateLater')}, + {text: locale.translation('addFunds'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +/** + * Show message that it's time to add funds if reconciliation is less than + * a day in the future and balance is too low. + * 24 hours prior to reconciliation, show message asking user to review + * their votes. + */ +const showEnabledNotifications = (state) => { + const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp') + + if (!reconcileStamp) { + return + } + + if (reconcileStamp - new Date().getTime() < miliseconds.day) { + if (sufficientBalanceToReconcile(state)) { + if (shouldShowNotificationReviewPublishers()) { + const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency') + showNotificationReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day)) + } + } else if (shouldShowNotificationAddFunds()) { + showNotificationAddFunds() + } + } else if (reconcileStamp - underscore.now() < 2 * miliseconds.day) { + if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { + showNotificationReviewPublishers(underscore.now() + miliseconds.day) + } + } +} + +const showDisabledNotifications = (state) => { + if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { + const firstRunTimestamp = state.get('firstRunTimestamp') + if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) { + return + } + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('notificationTryPayments'), + buttons: [ + {text: locale.translation('noThanks')}, + {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) + } +} + +const showNotifications = (state) => { + if (getSetting(settings.PAYMENTS_ENABLED)) { + if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + showEnabledNotifications(state) + } + } else { + showDisabledNotifications(state) + } +} + +const cacheRuleSet = (state, ruleset) => { + if ((!ruleset) || (underscore.isEqual(publisherInfo._internal.ruleset.raw, ruleset))) return + + try { + let stewed = [] + ruleset.forEach((rule) => { + let entry = { condition: acorn.parse(rule.condition) } + + if (rule.dom) { + if (rule.dom.publisher) { + entry.publisher = { selector: rule.dom.publisher.nodeSelector, + consequent: acorn.parse(rule.dom.publisher.consequent) + } + } + if (rule.dom.faviconURL) { + entry.faviconURL = { selector: rule.dom.faviconURL.nodeSelector, + consequent: acorn.parse(rule.dom.faviconURL.consequent) + } + } + } + if (!entry.publisher) entry.consequent = rule.consequent ? acorn.parse(rule.consequent) : rule.consequent + + stewed.push(entry) + }) + + publisherInfo._internal.ruleset.raw = ruleset + publisherInfo._internal.ruleset.cooked = stewed + if (!synopsis) { + return + } + + let syncP = false + ledgerState.getPublishers(state).forEach((publisher, index) => { + const location = (publisher.get('protocol') || 'http:') + '//' + index + let ctx = urlParse(location, true) + + ctx.TLD = tldjs.getPublicSuffix(ctx.host) + if (!ctx.TLD) return + + ctx = underscore.mapObject(ctx, function (value, key) { if (!underscore.isFunction(value)) return value }) + ctx.URL = location + ctx.SLD = tldjs.getDomain(ctx.host) + ctx.RLD = tldjs.getSubdomain(ctx.host) + ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' + + stewed.forEach((rule) => { + if ((rule.consequent !== null) || (rule.dom)) return + if (!ruleSolver.resolve(rule.condition, ctx)) return + + if (publisherInfo._internal.verboseP) console.log('\npurging ' + index) + delete synopsis.publishers[publisher] + state = ledgerState.deletePublishers(state, index) + syncP = true + }) + }) + + if (!syncP) { + return + } + + return updatePublisherInfo(state) + } catch (ex) { + console.error('ruleset error: ', ex) + return state + } +} + +const clientprep = () => { + if (!ledgerClient) ledgerClient = require('ledger-client') + ledgerInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_CLIENT_DEBUG) + publisherInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_DEBUG) + publisherInfo._internal.verboseP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_VERBOSE) +} + +const roundtrip = (params, options, callback) => { + let parts = typeof params.server === 'string' ? urlParse(params.server) + : typeof params.server !== 'undefined' ? params.server + : typeof options.server === 'string' ? urlParse(options.server) : options.server + const rawP = options.rawP + + if (!params.method) params.method = 'GET' + parts = underscore.extend(underscore.pick(parts, [ 'protocol', 'hostname', 'port' ]), + underscore.omit(params, [ 'headers', 'payload', 'timeout' ])) + +// TBD: let the user configure this via preferences [MTR] + if ((parts.hostname === 'ledger.brave.com') && (params.useProxy)) parts.hostname = 'ledger-proxy.privateinternetaccess.com' + + const i = parts.path.indexOf('?') + if (i !== -1) { + parts.pathname = parts.path.substring(0, i) + parts.search = parts.path.substring(i) + } else { + parts.pathname = parts.path + } + + options = { + url: urlFormat(parts), + method: params.method, + payload: params.payload, + responseType: 'text', + headers: underscore.defaults(params.headers || {}, { 'content-type': 'application/json; charset=utf-8' }), + verboseP: options.verboseP + } + request.request(options, (err, response, body) => { + let payload + + if ((response) && (options.verboseP)) { + console.log('[ response for ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path + ' ]') + console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + + ' ' + (response.statusMessage || '')) + underscore.keys(response.headers).forEach((header) => { console.log('>>> ' + header + ': ' + response.headers[header]) }) + console.log('>>>') + console.log('>>> ' + (body || '').split('\n').join('\n>>> ')) + } + + if (err) return callback(err) + + if (Math.floor(response.statusCode / 100) !== 2) { + return callback(new Error('HTTP response ' + response.statusCode) + ' for ' + params.method + ' ' + params.path) + } + + try { + payload = rawP ? body : (response.statusCode !== 204) ? JSON.parse(body) : null + } catch (err) { + return callback(err) + } + + try { + callback(null, response, payload) + } catch (err0) { + if (options.verboseP) console.log('\ncallback: ' + err0.toString() + '\n' + err0.stack) + } + }) + + if (!options.verboseP) return + + console.log('<<< ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path) + underscore.keys(options.headers).forEach((header) => { console.log('<<< ' + header + ': ' + options.headers[header]) }) + console.log('<<<') + if (options.payload) console.log('<<< ' + JSON.stringify(params.payload, null, 2).split('\n').join('\n<<< ')) +} + +const updateLedgerInfo = (state) => { + const info = ledgerInfo._internal.paymentInfo + const now = underscore.now() + + // TODO check if we can have internal info in the state already + if (info) { + underscore.extend(ledgerInfo, + underscore.pick(info, [ 'address', 'passphrase', 'balance', 'unconfirmed', 'satoshis', 'btc', 'amount', + 'currency' ])) + if ((!info.buyURLExpires) || (info.buyURLExpires > now)) { + ledgerInfo.buyURL = info.buyURL + ledgerInfo.buyMaximumUSD = 6 + } + if (typeof process.env.ADDFUNDS_URL !== 'undefined') { + ledgerInfo.buyURLFrame = true + ledgerInfo.buyURL = process.env.ADDFUNDS_URL + '?' + + queryString.stringify({ currency: ledgerInfo.currency, + amount: getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT), + address: ledgerInfo.address }) + ledgerInfo.buyMaximumUSD = false + } + + underscore.extend(ledgerInfo, ledgerInfo._internal.cache || {}) + } + + // TODO we don't need this for BAT + /* + if ((client) && (now > ledgerInfo._internal.geoipExpiry)) { + ledgerInfo._internal.geoipExpiry = now + (5 * miliseconds.minute) + + if (!ledgerGeoIP) ledgerGeoIP = require('ledger-geoip') + return ledgerGeoIP.getGeoIP(client.options, (err, provider, result) => { + if (err) console.warn('ledger geoip warning: ' + JSON.stringify(err, null, 2)) + if (result) ledgerInfo.countryCode = result + + ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] + + if (now <= ledgerInfo._internal.exchangeExpiry) return updateLedgerInfo() + + ledgerInfo._internal.exchangeExpiry = now + miliseconds.day + roundtrip({ path: '/v1/exchange/providers' }, client.options, (err, response, body) => { + if (err) console.error('ledger exchange error: ' + JSON.stringify(err, null, 2)) + + ledgerInfo._internal.exchanges = body || {} + ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] + updateLedgerInfo() + }) + }) + } + */ + + if (ledgerInfo._internal.debugP) { + console.log('\nupdateLedgerInfo: ' + JSON.stringify(underscore.omit(ledgerInfo, [ '_internal' ]), null, 2)) + } + + return ledgerState.mergeInfoProp(state, underscore.omit(ledgerInfo, [ '_internal' ])) +} + +// Called from observeTransactions() when we see a new payment (transaction). +const showNotificationPaymentDone = (transactionContributionFiat) => { + const notificationPaymentDoneMessage = locale.translation('notificationPaymentDone') + .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount) + .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency) + // Hide the 'waiting for deposit' message box if it exists + appActions.hideNotification(locale.translation('addFundsNotification')) + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: notificationPaymentDoneMessage, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('Ok'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +const observeTransactions = (state, transactions) => { + const current = ledgerState.getInfoProp(state, 'transactions') + if (underscore.isEqual(current, transactions)) { + return + } + // Notify the user of new transactions. + if (getSetting(settings.PAYMENTS_NOTIFICATIONS) && current !== null) { + const newTransactions = underscore.difference(transactions, current) + if (newTransactions.length > 0) { + const newestTransaction = newTransactions[newTransactions.length - 1] + showNotificationPaymentDone(newestTransaction.contribution.fiat) + } + } +} + +const getStateInfo = (state, parsedData) => { + const info = parsedData.paymentInfo + const then = underscore.now() - miliseconds.year + + if (!parsedData.properties.wallet) { + return + } + + const newInfo = { + paymentId: parsedData.properties.wallet.paymentId, + passphrase: parsedData.properties.wallet.keychains.passphrase, + created: !!parsedData.properties.wallet, + creating: !parsedData.properties.wallet, + reconcileFrequency: parsedData.properties.days, + reconcileStamp: parsedData.reconcileStamp + } + + state = ledgerState.mergeInfoProp(state, newInfo) + + if (info) { + ledgerInfo._internal.paymentInfo = info // TODO check if we can just save this into the state + const paymentURL = 'bitcoin:' + info.address + '?amount=' + info.btc + '&label=' + encodeURI('Brave Software') + const oldUrl = ledgerState.getInfoProp(state, 'paymentURL') + if (oldUrl !== paymentURL) { + state = ledgerState.setInfoProp(state, 'paymentURL', paymentURL) + try { + let chunks = [] + qr.image(paymentURL, { type: 'png' }) + .on('data', (chunk) => { chunks.push(chunk) }) + .on('end', () => { + const paymentIMG = 'data:image/png;base64,' + Buffer.concat(chunks).toString('base64') + state = ledgerState.setInfoProp(state, 'paymentIMG', paymentIMG) + }) + } catch (ex) { + console.error('qr.imageSync error: ' + ex.toString()) + } + } + } + + let transactions = [] + if (!parsedData.transactions) { + return updateLedgerInfo(state) + } + + for (let i = parsedData.transactions.length - 1; i >= 0; i--) { + let transaction = parsedData.transactions[i] + if (transaction.stamp < then) break + + if ((!transaction.ballots) || (transaction.ballots.length < transaction.count)) continue + + let ballots = underscore.clone(transaction.ballots || {}) + parsedData.ballots.forEach((ballot) => { + if (ballot.viewingId !== transaction.viewingId) return + + if (!ballots[ballot.publisher]) ballots[ballot.publisher] = 0 + ballots[ballot.publisher]++ + }) + + transactions.push(underscore.extend(underscore.pick(transaction, + [ 'viewingId', 'contribution', 'submissionStamp', 'count' ]), + { ballots: ballots })) + } + + observeTransactions(state, transactions) + state = ledgerState.setInfoProp(state, 'transactions', transactions) + return updateLedgerInfo(state) +} + +// TODO refactor when action is added +/* +var getPaymentInfo = () => { + var amount, currency + + if (!client) return + + try { + ledgerInfo.bravery = client.getBraveryProperties() + if (ledgerInfo.bravery.fee) { + amount = ledgerInfo.bravery.fee.amount + currency = ledgerInfo.bravery.fee.currency + } + + client.getWalletProperties(amount, currency, function (err, body) { + var info = ledgerInfo._internal.paymentInfo || {} + + if (logError(err, 'getWalletProperties')) { + return + } + + info = underscore.extend(info, underscore.pick(body, [ 'buyURL', 'buyURLExpires', 'balance', 'unconfirmed', 'satoshis' ])) + info.address = client.getWalletAddress() + if ((amount) && (currency)) { + info = underscore.extend(info, { amount: amount, currency: currency }) + if ((body.rates) && (body.rates[currency])) { + info.btc = (amount / body.rates[currency]).toFixed(8) + } + } + ledgerInfo._internal.paymentInfo = info + updateLedgerInfo() + cacheReturnValue() + }) + } catch (ex) { + console.error('properties error: ' + ex.toString()) + } +} +*/ + +const setPaymentInfo = (amount) => { + var bravery + + if (!client) return + + try { + bravery = client.getBraveryProperties() + } catch (ex) { + // wallet being created... + return setTimeout(function () { setPaymentInfo(amount) }, 2 * miliseconds.second) + } + + amount = parseInt(amount, 10) + if (isNaN(amount) || (amount <= 0)) return + + underscore.extend(bravery.fee, { amount: amount }) + client.setBraveryProperties(bravery, (err, result) => { + if (ledgerInfo.created) { + // getPaymentInfo() TODO create action for this + } + + if (err) return console.error('ledger setBraveryProperties: ' + err.toString()) + + if (result) { + muonWriter(pathName(statePath), result) + // TODO save this new data to appState + } + }) +} + +let balanceTimeoutId = false +const getBalance = (state) => { + if (!client) return + + balanceTimeoutId = setTimeout(getBalance, 1 * miliseconds.minute) + if (!ledgerState.getInfoProp(state, 'address')) { + return + } + + if (!ledgerBalance) ledgerBalance = require('ledger-balance') + ledgerBalance.getBalance(ledgerInfo.address, underscore.extend({ balancesP: true }, client.options), + (err, provider, result) => { + // TODO create action to handle callback + if (err) { + return console.warn('ledger balance warning: ' + JSON.stringify(err, null, 2)) + } + /* + var unconfirmed + var info = ledgerInfo._internal.paymentInfo + + if (typeof result.unconfirmed === 'undefined') return + + if (result.unconfirmed > 0) { + unconfirmed = (result.unconfirmed / 1e8).toFixed(4) + if ((info || ledgerInfo).unconfirmed === unconfirmed) return + + ledgerInfo.unconfirmed = unconfirmed + if (info) info.unconfirmed = ledgerInfo.unconfirmed + if (clientOptions.verboseP) console.log('\ngetBalance refreshes ledger info: ' + ledgerInfo.unconfirmed) + return updateLedgerInfo() + } + + if (ledgerInfo.unconfirmed === '0.0000') return + + if (clientOptions.verboseP) console.log('\ngetBalance refreshes payment info') + getPaymentInfo() + */ + }) +} + +// TODO +const callback = (err, result, delayTime) => { + /* + var results + var entries = client && client.report() + + if (clientOptions.verboseP) { + console.log('\nledger client callback: clientP=' + (!!client) + ' errP=' + (!!err) + ' resultP=' + (!!result) + + ' delayTime=' + delayTime) + } + + if (err) { + console.log('ledger client error(1): ' + JSON.stringify(err, null, 2) + (err.stack ? ('\n' + err.stack) : '')) + if (!client) return + + if (typeof delayTime === 'undefined') delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) + } + + if (!result) return run(delayTime) + + if ((client) && (result.properties.wallet)) { + if (!ledgerInfo.created) setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) + + getStateInfo(result) + getPaymentInfo() + } + cacheRuleSet(result.ruleset) + if (result.rulesetV2) { + results = result.rulesetV2 + delete result.rulesetV2 + + entries = [] + results.forEach((entry) => { + var key = entry.facet + ':' + entry.publisher + + if (entry.exclude !== false) { + entries.push({ type: 'put', key: key, value: JSON.stringify(underscore.omit(entry, [ 'facet', 'publisher' ])) }) + } else { + entries.push({ type: 'del', key: key }) + } + }) + + v2RulesetDB.batch(entries, (err) => { + if (err) return console.error(v2RulesetPath + ' error: ' + JSON.stringify(err, null, 2)) + + if (entries.length === 0) return + + underscore.keys(synopsis.publishers).forEach((publisher) => { +// be safe... + if (synopsis.publishers[publisher]) delete synopsis.publishers[publisher].options.exclude + + excludeP(publisher) + }) + }) + } + if (result.publishersV2) { + results = result.publishersV2 + delete result.publishersV2 + + entries = [] + results.forEach((entry) => { + entries.push({ type: 'put', + key: entry.publisher, + value: JSON.stringify(underscore.omit(entry, [ 'publisher' ])) + }) + if ((synopsis.publishers[entry.publisher]) && + (synopsis.publishers[entry.publisher].options.verified !== entry.verified)) { + synopsis.publishers[entry.publisher].options.verified = entry.verified + updatePublisherInfo() + } + }) + v2PublishersDB.batch(entries, (err) => { + if (err) return console.error(v2PublishersPath + ' error: ' + JSON.stringify(err, null, 2)) + }) + } + + muonWriter(pathName(statePath), result) + run(delayTime) + */ +} + +const initialize = (state, paymentsEnabled) => { + if (!v2RulesetDB) v2RulesetDB = levelUp(pathName(v2RulesetPath)) + if (!v2PublishersDB) v2PublishersDB = levelUp(pathName(v2PublishersPath)) + state = enable(state, paymentsEnabled) + + // Check if relevant browser notifications should be shown every 15 minutes + if (notificationTimeout) { + clearInterval(notificationTimeout) + } + notificationTimeout = setInterval((state) => { + showNotifications(state) + }, 15 * miliseconds.minute, state) + + if (!paymentsEnabled) { + client = null + return ledgerState.resetInfo(state) + } + + if (client) { + return + } + + if (!ledgerPublisher) ledgerPublisher = require('ledger-publisher') + let ruleset = [] + ledgerPublisher.ruleset.forEach(rule => { if (rule.consequent) ruleset.push(rule) }) + state = cacheRuleSet(state, ruleset) + + try { + const fs = require('fs') + fs.accessSync(pathName(statePath), fs.FF_OK) + const data = fs.readFileSync(pathName(statePath)) + let parsedData + + try { + parsedData = JSON.parse(data) + if (clientOptions.verboseP) { + console.log('\nstarting up ledger client integration') + } + } catch (ex) { + console.error('statePath parse error: ' + ex.toString()) + return state + } + + state = getStateInfo(state, parsedData) + + try { + let timeUntilReconcile + clientprep() + client = ledgerClient(parsedData.personaId, + underscore.extend(parsedData.options, { roundtrip: roundtrip }, clientOptions), + parsedData) + + // Scenario: User enables Payments, disables it, waits 30+ days, then + // enables it again -> reconcileStamp is in the past. + // In this case reset reconcileStamp to the future. + try { timeUntilReconcile = client.timeUntilReconcile() } catch (ex) {} + let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') + if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { + client.setTimeUntilReconcile(null, (err, stateResult) => { + if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) + + if (!stateResult) { + return + } + state = getStateInfo(stateResult) + + muonWriter(pathName(statePath), stateResult) + }) + } + } catch (ex) { + return console.error('ledger client creation error: ', ex) + } + + // speed-up browser start-up by delaying the first synchronization action + // TODO create new action that is triggered after 3s + /* + setTimeout(() => { + if (!client) return + + if (client.sync(callback) === true) run(random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute })) + state = cacheRuleSet(state, parsedData.ruleset) + }, 3 * miliseconds.second) + */ + + // Make sure bravery props are up-to-date with user settings + const address = ledgerState.getInfoProp(state, 'address') + if (!address) { + state = ledgerState.setInfoProp(state, 'address', client.getWalletAddress()) + } + + setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) + getBalance(state) + + return state + } catch (err) { + if (err.code !== 'ENOENT') { + console.error('statePath read error: ' + err.toString()) + } + state = ledgerState.resetInfo(state) + return state + } +} + +const init = (state) => { + try { + state = initialize(state, getSetting(settings.PAYMENTS_ENABLED)) + } catch (ex) { + console.error('ledger.js initialization failed: ', ex) + } + + return state +} + +// TODO rename +const contributeP = (state, publisherKey) => { + const publisher = ledgerState.getPublisher(state, publisherKey) + return ( + (stickyP(state, publisherKey) || publisher.getIn(['options', 'exclude']) !== true) && + eligibleP(state, publisherKey) && + !blockedP(state, publisherKey) + ) +} + +const run = (delayTime) => { + // TODO implement + /* + if (clientOptions.verboseP) { + var entries + + console.log('\nledger client run: clientP=' + (!!client) + ' delayTime=' + delayTime) + + var line = (fields) => { + var result = '' + + fields.forEach((field) => { + var spaces + var max = (result.length > 0) ? 9 : 19 + + if (typeof field !== 'string') field = field.toString() + if (field.length < max) { + spaces = ' '.repeat(max - field.length) + field = spaces + field + } else { + field = field.substr(0, max) + } + result += ' ' + field + }) + + console.log(result.substr(1)) + } + + line([ 'publisher', + 'blockedP', 'stickyP', 'verified', + 'excluded', 'eligibleP', 'visibleP', + 'contribP', + 'duration', 'visits' + ]) + entries = synopsis.topN() || [] + entries.forEach((entry) => { + var publisher = entry.publisher + + line([ publisher, + blockedP(publisher), stickyP(publisher), synopsis.publishers[publisher].options.verified === true, + synopsis.publishers[publisher].options.exclude === true, eligibleP(publisher), visibleP(publisher), + contributeP(publisher), + Math.round(synopsis.publishers[publisher].duration / 1000), synopsis.publishers[publisher].visits ]) + }) + } + + if ((typeof delayTime === 'undefined') || (!client)) return + + var active, state, weights, winners + var ballots = client.ballots() + var data = (synopsis) && (ballots > 0) && synopsisNormalizer() + + if (data) { + weights = [] + data.forEach((datum) => { weights.push({ publisher: datum.site, weight: datum.weight / 100.0 }) }) + winners = synopsis.winners(ballots, weights) + } + if (!winners) winners = [] + + try { + winners.forEach((winner) => { + var result + + if (!contributeP(winner)) return + + result = client.vote(winner) + if (result) state = result + }) + if (state) muonWriter(pathName(statePath), state) + } catch (ex) { + console.log('ledger client error(2): ' + ex.toString() + (ex.stack ? ('\n' + ex.stack) : '')) + } + + if (delayTime === 0) { + try { + delayTime = client.timeUntilReconcile() + } catch (ex) { + delayTime = false + } + if (delayTime === false) delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) + } + if (delayTime > 0) { + if (runTimeoutId) return + + active = client + if (delayTime > (1 * miliseconds.hour)) delayTime = random.randomInt({ min: 3 * miliseconds.minute, max: miliseconds.hour }) + + runTimeoutId = setTimeout(() => { + runTimeoutId = false + if (active !== client) return + + if (!client) return console.log('\n\n*** MTR says this can\'t happen(1)... please tell him that he\'s wrong!\n\n') + + if (client.sync(callback) === true) return run(0) + }, delayTime) + return + } + + if (client.isReadyToReconcile()) return client.reconcile(uuid.v4().toLowerCase(), callback) + + console.log('what? wait, how can this happen?') + */ +} + +const networkConnected = (state) => { + // TODO pass state into debounced function + underscore.debounce((state) => { + if (!client) return + + if (runTimeoutId) { + clearTimeout(runTimeoutId) + runTimeoutId = false + } + if (client.sync(callback) === true) { + // TODO refactor + const delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) + run(state, delayTime) + } + + if (balanceTimeoutId) clearTimeout(balanceTimeoutId) + balanceTimeoutId = setTimeout(getBalance, 5 * miliseconds.second) + }, 1 * miliseconds.minute, true) +} + +// TODO check if quitP is needed, now is defined in ledgerUtil.quit +const muonWriter = (path, payload) => { + muon.file.writeImportant(path, JSON.stringify(payload, null, 2), (success) => { + if (!success) return console.error('write error: ' + path) + + if ((quitP) && (!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { + if (ledgerInfo._internal.debugP) { + console.log('\ndeleting ' + path) + } + + const fs = require('fs') + return fs.unlink(path, (err) => { if (err) console.error('unlink error: ' + err.toString()) }) + } + + if (ledgerInfo._internal.debugP) console.log('\nwrote ' + path) + }) +} + +module.exports = { + synopsis, + shouldTrackView, + btcToCurrencyString, + formattedTimeFromNow, + formattedDateFromTimestamp, + walletStatus, + backupKeys, + recoverKeys, + quit, + addVisit, + pageDataChanged, + init, + initialize, + setPaymentInfo, + updatePublisherInfo, + networkConnected, + verifiedP +} diff --git a/app/common/lib/pageDataUtil.js b/app/common/lib/pageDataUtil.js new file mode 100644 index 00000000000..c9ed1131916 --- /dev/null +++ b/app/common/lib/pageDataUtil.js @@ -0,0 +1,17 @@ + +const urlFormat = require('url').format +const _ = require('underscore') + +const urlParse = require('../../common/urlParse') + +const getInfoKey = (url) => { + if (typeof url !== 'string') { + return null + } + + return urlFormat(_.pick(urlParse(url), [ 'protocol', 'host', 'hostname', 'port', 'pathname' ])) +} + +module.exports = { + getInfoKey +} diff --git a/app/common/state/ledgerState.js b/app/common/state/ledgerState.js new file mode 100644 index 00000000000..2b51a532c27 --- /dev/null +++ b/app/common/state/ledgerState.js @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Immutable = require('immutable') + +// Utils +const siteSettings = require('../../../js/state/siteSettings') +const {makeImmutable} = require('../../common/state/immutableUtil') + +const ledgerState = { + setRecoveryStatus: (state, status) => { + const date = new Date().getTime() + state = state.setIn(['about', 'preferences', 'recoverySucceeded'], status) + return state.setIn(['about', 'preferences', 'updatedStamp'], date) + }, + + setLedgerError: (state, error, caller) => { + if (error == null && caller == null) { + return state.setIn(['ledger', 'info', 'error'], null) + } + + return state + .setIn(['ledger', 'info', 'error', 'caller'], caller) + .setIn(['ledger', 'info', 'error', 'error'], error) + }, + + getLocation: (state, url) => { + if (url == null) { + return null + } + + return state.getIn(['ledger', 'locations', url]) + }, + + changePinnedValues: (state, publishers) => { + if (publishers == null) { + return state + } + + publishers = makeImmutable(publishers) + publishers.forEach((item, index) => { + const pattern = `https?://${index}` + const percentage = item.get('pinPercentage') + let newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPinPercentage', percentage) + state = state.set('siteSettings', newSiteSettings) + }) + + return state + }, + + getSynopsis: (state) => { + return state.getIn(['ledger', 'synopsis']) || Immutable.Map() + }, + + saveSynopsis: (state, publishers, options) => { + return state + .setIn(['ledger', 'synopsis', 'publishers'], publishers) + .setIn(['ledger', 'synopsis', 'options'], options) + }, + + getPublisher: (state, key) => { + if (key == null) { + return Immutable.Map() + } + + return state.getIn(['ledger', 'synopsis', 'publishers', key]) || Immutable.Map() + }, + + getPublishers: (state) => { + return state.getIn(['ledger', 'synopsis', 'publishers']) || Immutable.Map() + }, + + deletePublishers: (state, key) => { + return state.deleteIn(['ledger', 'synopsis', 'publishers', key]) + }, + + getSynopsisOption: (state, prop) => { + if (prop == null) { + return state.getIn(['ledger', 'synopsis', 'options']) + } + + return state.getIn(['ledger', 'synopsis', 'options', prop]) + }, + + setSynopsisOption: (state, prop, value) => { + if (prop == null) { + return state + } + + return state.setIn(['ledger', 'synopsis', 'options', prop], value) + }, + + enableUndefinedPublishers: (state, publishers) => { + const sitesObject = state.get('siteSettings') + Object.keys(publishers).map((item) => { + const pattern = `https?://${item}` + const result = sitesObject.getIn([pattern, 'ledgerPayments']) + + if (result === undefined) { + const newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPayments', true) + state = state.set('siteSettings', newSiteSettings) + } + }) + + return state + }, + + getInfoProp: (state, prop) => { + if (prop == null) { + return state.getIn(['ledger', 'info']) + } + + return state.getIn(['ledger', 'info', prop]) + }, + + setInfoProp: (state, prop, value) => { + if (prop == null) { + return state + } + + return state.setIn(['ledger', 'info', prop], value) + }, + + mergeInfoProp: (state, data) => { + if (data == null) { + return state + } + + const oldData = ledgerState.getInfoProp() + return state.setIn(['ledger', 'info'], oldData.merge(data)) + }, + + resetInfo: (state) => { + return state.setIn(['ledger', 'info'], {}) + }, + + resetSynopsis: (state) => { + return state.deleteIn(['ledger', 'synopsis']) + } +} + +module.exports = ledgerState diff --git a/app/common/state/pageDataState.js b/app/common/state/pageDataState.js new file mode 100644 index 00000000000..2b399e7c4d3 --- /dev/null +++ b/app/common/state/pageDataState.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const Immutable = require('immutable') + +// Utils +const pageDataUtil = require('../lib/pageDataUtil') +const {getWebContents} = require('../../browser/webContentsCache') +const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {makeImmutable} = require('./immutableUtil') + +const pageDataState = { + addView: (state, url = null, tabId = null) => { + const tab = getWebContents(tabId) + const isPrivate = !tab || + tab.isDestroyed() || + !tab.session.partition.startsWith('persist:') + + state = pageDataState.setLastActiveTabId(state, tabId) + + if ((url && isSourceAboutUrl(url)) || isPrivate) { + url = null + } + + const lastView = pageDataState.getView(state) + if (lastView.get('url') === url) { + return state + } + + let pageViewEvent = makeImmutable({ + timestamp: new Date().getTime(), + url, + tabId + }) + return state.setIn(['pageData', 'view'], pageViewEvent) + }, + + addInfo: (state, data) => { + data = makeImmutable(data) + + if (data == null) { + return state + } + + const key = pageDataUtil.getInfoKey(data.get('url')) + + data = data.set('key', key) + state = state.setIn(['pageData', 'last', 'info'], key) + return state.setIn(['pageData', 'info', key], data) + }, + + addLoad: (state, data) => { + if (data == null) { + return state + } + + // select only last 100 loads + const newLoad = state.getIn(['pageData', 'load'], Immutable.List()).slice(-100).push(data) + return state.setIn(['pageData', 'load'], newLoad) + }, + + getView: (state) => { + return state.getIn(['pageData', 'view']) || Immutable.Map() + }, + + getLastInfo: (state) => { + const key = state.getIn(['pageData', 'last', 'info']) + + if (key == null) { + Immutable.Map() + } + + return state.getIn(['pageData', 'info', key], Immutable.Map()) + }, + + getLoad: (state) => { + return state.getIn(['pageData', 'load'], Immutable.List()) + }, + + getLastActiveTabId: (state) => { + return state.getIn(['pageData', 'last', 'tabId']) + }, + + setLastActiveTabId: (state, tabId) => { + return state.setIn(['pageData', 'last', 'tabId'], tabId) + }, + + setPublisher: (state, key, publisher) => { + if (key == null) { + return state + } + + return state.setIn(['pageData', 'info', key, 'publisher'], publisher) + } +} + +module.exports = pageDataState diff --git a/app/common/state/siteSettingsState.js b/app/common/state/siteSettingsState.js index ba00633663e..5fd8041487b 100644 --- a/app/common/state/siteSettingsState.js +++ b/app/common/state/siteSettingsState.js @@ -28,8 +28,24 @@ const api = { return siteSettings ? siteSettings.get(hostPattern) : Immutable.Map() }, - isNoScriptEnabled (state, settings) { + isNoScriptEnabled: (state, settings) => { return siteSettings.activeSettings(settings, state, appConfig).noScript === true + }, + + getSettingsProp: (state, pattern, prop) => { + if (prop == null) { + return null + } + + return state.getIn(['siteSettings', pattern, prop]) + }, + + setSettingsProp: (state, pattern, prop, value) => { + if (prop == null) { + return null + } + + return state.setIn(['siteSettings', pattern, prop], value) } } diff --git a/app/ledger.js b/app/ledger.js index a38856f46f6..d8e3a03882e 100644 --- a/app/ledger.js +++ b/app/ledger.js @@ -36,7 +36,6 @@ const os = require('os') const path = require('path') const urlParse = require('./common/urlParse') const urlFormat = require('url').format -const util = require('util') const Immutable = require('immutable') const electron = require('electron') @@ -56,18 +55,16 @@ const uuid = require('uuid') const appActions = require('../js/actions/appActions') const appConfig = require('../js/constants/appConfig') -const appConstants = require('../js/constants/appConstants') const messages = require('../js/constants/messages') const settings = require('../js/constants/settings') const request = require('../js/lib/request') const getSetting = require('../js/settings').getSetting const locale = require('./locale') const appStore = require('../js/stores/appStore') -const eventStore = require('../js/stores/eventStore') const rulesolver = require('./extensions/brave/content/scripts/pageInformation') const ledgerUtil = require('./common/lib/ledgerUtil') const tabs = require('./browser/tabs') -const {fileUrl} = require('../js/lib/appUrlUtil') +const pageDataState = require('./common/state/pageDataState') // "only-when-needed" loading... let ledgerBalance = null @@ -76,18 +73,15 @@ let ledgerGeoIP = null let ledgerPublisher = null // testing data -const testVerifiedPublishers = [ - 'brianbondy.com', - 'clifton.io' -] + // TBD: remove these post beta [MTR] +// TODO remove, it's not used anymore const logPath = 'ledger-log.json' const publisherPath = 'ledger-publisher.json' const scoresPath = 'ledger-scores.json' // TBD: move these to secureState post beta [MTR] -const statePath = 'ledger-state.json' const synopsisPath = 'ledger-synopsis.json' /* @@ -104,16 +98,8 @@ const clientOptions = { server: process.env.LEDGER_SERVER_URL, createWorker: app.createWorker } - -var doneTimer var quitP -var v2RulesetDB -const v2RulesetPath = 'ledger-rulesV2.leveldb' - -var v2PublishersDB -const v2PublishersPath = 'ledger-publishersV2.leveldb' - /* * publisher globals */ @@ -126,14 +112,6 @@ var publishers = {} * utility globals */ -const msecs = { year: 365 * 24 * 60 * 60 * 1000, - week: 7 * 24 * 60 * 60 * 1000, - day: 24 * 60 * 60 * 1000, - hour: 60 * 60 * 1000, - minute: 60 * 1000, - second: 1000 -} - /* * notification state globals */ @@ -144,170 +122,11 @@ let notificationPaymentDoneMessage let notificationTryPaymentsMessage let notificationTimeout = null -// TODO(bridiver) - create a better way to get setting changes -const doAction = (state, action) => { - var i, publisher - -/* TBD: handle - - { actionType: "window-set-blocked-by" - , frameProps: - { audioPlaybackActive: true - ... - } - , ... - } - */ - if (publisherInfo._internal.debugP) { - console.log('\napplication event: ' + JSON.stringify(underscore.pick(action, [ 'actionType', 'key' ]), null, 2)) - } - - switch (action.actionType) { - case appConstants.APP_SET_STATE: - init() - break - - case appConstants.APP_BACKUP_KEYS: - state = backupKeys(state, action) - break - - case appConstants.APP_RECOVER_WALLET: - state = recoverKeys(state, action) - break - - case appConstants.APP_SHUTTING_DOWN: - quit() - break - - case appConstants.APP_ON_CLEAR_BROWSING_DATA: - { - const defaults = state.get('clearBrowsingDataDefaults') - const temp = state.get('tempClearBrowsingData', Immutable.Map()) - const clearData = defaults ? defaults.merge(temp) : temp - if (clearData.get('browserHistory') && !getSetting(settings.PAYMENTS_ENABLED)) { - reset(true) - } - break - } - - case appConstants.APP_IDLE_STATE_CHANGED: - visit('NOOP', underscore.now(), null) - break - - case appConstants.APP_CHANGE_SETTING: - switch (action.key) { - case settings.PAYMENTS_ENABLED: - initialize(action.value) - break - - case settings.PAYMENTS_CONTRIBUTION_AMOUNT: - setPaymentInfo(action.value) - break - - case settings.PAYMENTS_MINIMUM_VISIT_TIME: - if (action.value <= 0) break - - synopsis.options.minPublisherDuration = action.value - updatePublisherInfo() - break - - case settings.PAYMENTS_MINIMUM_VISITS: - if (action.value <= 0) break - - synopsis.options.minPublisherVisits = action.value - updatePublisherInfo() - break - - case settings.PAYMENTS_ALLOW_NON_VERIFIED: - synopsis.options.showOnlyVerified = action.value - updatePublisherInfo() - break - - default: - break - } - break - - case appConstants.APP_CHANGE_SITE_SETTING: - if (!action.hostPattern) { - console.warn('Changing site settings should always have a hostPattern') - break - } - i = action.hostPattern.indexOf('://') - if (i === -1) break - - publisher = action.hostPattern.substr(i + 3) - if (action.key === 'ledgerPaymentsShown') { - if (action.value === false) { - if (publisherInfo._internal.verboseP) console.log('\npurging ' + publisher) - delete synopsis.publishers[publisher] - delete publishers[publisher] - updatePublisherInfo() - } - } else if (action.key === 'ledgerPayments') { - if (!synopsis.publishers[publisher]) break - - if (publisherInfo._internal.verboseP) console.log('\nupdating ' + publisher + ' stickyP=' + action.value) - updatePublisherInfo() - verifiedP(publisher) - } else if (action.key === 'ledgerPinPercentage') { - if (!synopsis.publishers[publisher]) break - synopsis.publishers[publisher].pinPercentage = action.value - updatePublisherInfo(publisher) - } - break - - case appConstants.APP_REMOVE_SITE_SETTING: - i = action.hostPattern.indexOf('://') - if (i === -1) break - - publisher = action.hostPattern.substr(i + 3) - if (action.key === 'ledgerPayments') { - if (!synopsis.publishers[publisher]) break - - if (publisherInfo._internal.verboseP) console.log('\nupdating ' + publisher + ' stickyP=' + true) - updatePublisherInfo() - } - break - - case appConstants.APP_NETWORK_CONNECTED: - setTimeout(networkConnected, 1 * msecs.second) - break - - case appConstants.APP_NAVIGATOR_HANDLER_REGISTERED: - ledgerInfo.hasBitcoinHandler = (action.protocol === 'bitcoin') - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - break - - case appConstants.APP_NAVIGATOR_HANDLER_UNREGISTERED: - ledgerInfo.hasBitcoinHandler = false - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - break - - default: - break - } - - return state -} /* * module entry points */ -var init = () => { - try { - initialize(getSetting(settings.PAYMENTS_ENABLED)) - } catch (ex) { console.error('ledger.js initialization failed: ', ex) } -} - -var quit = () => { - quitP = true - visit('NOOP', underscore.now(), null) - clearInterval(doneTimer) - - if ((!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) reset(true) -} var boot = () => { if ((bootP) || (client)) return @@ -329,184 +148,22 @@ var boot = () => { bootP = false return console.error('ledger client boot error: ', ex) } - if (client.sync(callback) === true) run(random.randomInt({ min: msecs.minute, max: 10 * msecs.minute })) + if (client.sync(callback) === true) run(random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute })) getBalance() bootP = false }) } -var reset = (doneP) => { - var files = [ logPath, publisherPath, scoresPath, synopsisPath ] - - if (!doneP) files.push(statePath) - files.forEach((file) => { - fs.unlink(pathName(file), (err) => { - if ((err) && (err.code !== 'ENOENT')) { - console.error('error removing file ' + file + ': ', err) - } - }) - }) -} - /* * Print or Save Recovery Keys */ -var backupKeys = (appState, action) => { - const date = moment().format('L') - const paymentId = appState.getIn(['ledgerInfo', 'paymentId']) - const passphrase = appState.getIn(['ledgerInfo', 'passphrase']) - - const messageLines = [ - locale.translation('ledgerBackupText1'), - [locale.translation('ledgerBackupText2'), date].join(' '), - '', - [locale.translation('ledgerBackupText3'), paymentId].join(' '), - [locale.translation('ledgerBackupText4'), passphrase].join(' '), - '', - locale.translation('ledgerBackupText5') - ] - - const message = messageLines.join(os.EOL) - const filePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt') - - fs.writeFile(filePath, message, (err) => { - if (err) { - console.error(err) - } else { - tabs.create({url: fileUrl(filePath)}, (webContents) => { - if (action.backupAction === 'print') { - webContents.print({silent: false, printBackground: false}) - } else { - webContents.downloadURL(fileUrl(filePath), true) - } - }) - } - }) - - return appState -} - -var loadKeysFromBackupFile = (filePath) => { - let keys = null - let data = fs.readFileSync(filePath) - - if (!data || !data.length || !(data.toString())) { - logError('No data in backup file', 'recoveryWallet') - } else { - try { - const recoveryFileContents = data.toString() - - let messageLines = recoveryFileContents.split(os.EOL) - - let paymentIdLine = '' || messageLines[3] - let passphraseLine = '' || messageLines[4] - - const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' ')) - const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] - - const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' ')) - const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] - - keys = { - paymentId, - passphrase - } - } catch (exc) { - logError(exc, 'recoveryWallet') - } - } - - return keys -} /* * Recover Ledger Keys */ -var recoverKeys = (appState, action) => { - let firstRecoveryKey, secondRecoveryKey - - if (action.useRecoveryKeyFile) { - let recoveryKeyFile = promptForRecoveryKeyFile() - if (!recoveryKeyFile) { - // user canceled from dialog, we abort without error - return appState - } - - if (recoveryKeyFile) { - let keys = loadKeysFromBackupFile(recoveryKeyFile) || {} - - if (keys) { - firstRecoveryKey = keys.paymentId - secondRecoveryKey = keys.passphrase - } - } - } - - if (!firstRecoveryKey || !secondRecoveryKey) { - firstRecoveryKey = action.firstRecoveryKey - secondRecoveryKey = action.secondRecoveryKey - } - - const UUID_REGEX = /^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/ - if (typeof firstRecoveryKey !== 'string' || !firstRecoveryKey.match(UUID_REGEX) || typeof secondRecoveryKey !== 'string' || !secondRecoveryKey.match(UUID_REGEX)) { - // calling logError sets the error object - logError(true, 'recoverKeys') - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - appActions.ledgerRecoveryFailed() - return appState - } - - client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, result) => { - let existingLedgerError = ledgerInfo.error - - if (logError(err, 'recoveryWallet')) { - // we reset ledgerInfo.error to what it was before (likely null) - // if ledgerInfo.error is not null, the wallet info will not display in UI - // logError sets ledgerInfo.error, so we must we clear it or UI will show an error - ledgerInfo.error = existingLedgerError - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - appActions.ledgerRecoveryFailed() - } else { - callback(err, result) - - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) - if (balanceTimeoutId) clearTimeout(balanceTimeoutId) - getBalance() - appActions.ledgerRecoverySucceeded() - } - }) - - return appState -} - -const dialog = electron.dialog - -var promptForRecoveryKeyFile = () => { - const defaultRecoveryKeyFilePath = path.join(app.getPath('userData'), '/brave_wallet_recovery.txt') - - let files - - if (process.env.SPECTRON) { - // skip the dialog for tests - console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) - files = [defaultRecoveryKeyFilePath] - } else { - files = dialog.showOpenDialog({ - properties: ['openFile'], - defaultPath: defaultRecoveryKeyFilePath, - filters: [{ - name: 'TXT files', - extensions: ['txt'] - }] - }) - } - - return (files && files.length ? files[0] : null) -} - /* * IPC entry point */ @@ -562,7 +219,7 @@ if (ipc) { ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => { const win = electron.BrowserWindow.getActiveWindow() - if (message === addFundsMessage) { + if (message === locale.translation('addFundsNotification')) { appActions.hideNotification(message) // See showNotificationAddFunds() for buttons. // buttonIndex === 1 is "Later"; the timestamp until which to delay is set @@ -576,7 +233,7 @@ if (ipc) { windowId: win.id }) } - } else if (message === reconciliationMessage) { + } else if (message === locale.translation('reconciliationNotification')) { appActions.hideNotification(message) // buttonIndex === 1 is Dismiss if (buttonIndex === 0) { @@ -592,7 +249,7 @@ if (ipc) { if (buttonIndex === 0) { appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) } - } else if (message === notificationTryPaymentsMessage) { + } else if (message === locale.translation('notificationTryPayments')) { appActions.hideNotification(message) if (buttonIndex === 1 && win) { appActions.createTabRequested({ @@ -606,7 +263,7 @@ if (ipc) { ipc.on(messages.ADD_FUNDS_CLOSED, () => { if (balanceTimeoutId) clearTimeout(balanceTimeoutId) - balanceTimeoutId = setTimeout(getBalance, 5 * msecs.second) + balanceTimeoutId = setTimeout(getBalance, 5 * milisecons.second) }) } @@ -628,256 +285,10 @@ underscore.keys(fileTypes).forEach((fileType) => { }) signatureMax = Math.ceil(signatureMax * 1.5) -eventStore.addChangeListener(() => { - var initP - const eventState = eventStore.getState().toJS() - var view = eventState.page_view - var info = eventState.page_info - var pageLoad = eventState.page_load - - if ((!synopsis) || (!util.isArray(info))) return - -// NB: in theory we have already seen every element in info except for (perhaps) the last one... - underscore.rest(info, info.length - 1).forEach((page) => { - let pattern, publisher - let location = page.url - - if (location.match(/^about/)) return - - location = urlFormat(underscore.pick(urlParse(location), [ 'protocol', 'host', 'hostname', 'port', 'pathname' ])) - publisher = locations[location] && locations[location].publisher - if (publisher) { - if (synopsis.publishers[publisher] && - (typeof synopsis.publishers[publisher].faviconURL === 'undefined' || synopsis.publishers[publisher].faviconURL === null)) { - getFavIcon(synopsis.publishers[publisher], page, location) - } - return updateLocation(location, publisher) - } - - if (!page.publisher) { - try { - publisher = ledgerPublisher.getPublisher(location, publisherInfo._internal.ruleset.raw) - if ((publisher) && (blockedP(publisher))) publisher = null - if (publisher) page.publisher = publisher - } catch (ex) { - console.error('getPublisher error for ' + location + ': ' + ex.toString()) - } - } - locations[location] = underscore.omit(page, [ 'url', 'protocol', 'faviconURL' ]) - if (!page.publisher) return - - publisher = page.publisher - pattern = `https?://${publisher}` - initP = !synopsis.publishers[publisher] - synopsis.initPublisher(publisher) - if (initP) { - excludeP(publisher, (unused, exclude) => { - if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { - exclude = false - } else { - exclude = !exclude - } - appActions.changeSiteSetting(pattern, 'ledgerPayments', exclude) - updatePublisherInfo() - }) - } - updateLocation(location, publisher) - getFavIcon(synopsis.publishers[publisher], page, location) - }) - - view = underscore.last(view) || {} - if (ledgerUtil.shouldTrackView(view, pageLoad)) { - visit(view.url || 'NOOP', view.timestamp || underscore.now(), view.tabId) - } -}) - /* * module initialization */ -var initialize = (paymentsEnabled) => { - var ruleset - - if (!v2RulesetDB) v2RulesetDB = levelup(pathName(v2RulesetPath)) - if (!v2PublishersDB) v2PublishersDB = levelup(pathName(v2PublishersPath)) - enable(paymentsEnabled) - - // Check if relevant browser notifications should be shown every 15 minutes - if (notificationTimeout) clearInterval(notificationTimeout) - notificationTimeout = setInterval(showNotifications, 15 * msecs.minute) - - if (!paymentsEnabled) { - client = null - return appActions.updateLedgerInfo({}) - } - if (client) return - - if (!ledgerPublisher) ledgerPublisher = require('ledger-publisher') - ruleset = [] - ledgerPublisher.ruleset.forEach(rule => { if (rule.consequent) ruleset.push(rule) }) - cacheRuleSet(ruleset) - - fs.access(pathName(statePath), fs.FF_OK, (err) => { - if (!err) { - if (clientOptions.verboseP) console.log('\nfound ' + pathName(statePath)) - - fs.readFile(pathName(statePath), (err, data) => { - var state - - if (err) return console.error('read error: ' + err.toString()) - - try { - state = JSON.parse(data) - if (clientOptions.verboseP) console.log('\nstarting up ledger client integration') - } catch (ex) { - return console.error('statePath parse error: ' + ex.toString()) - } - - getStateInfo(state) - - try { - var timeUntilReconcile - clientprep() - client = ledgerClient(state.personaId, - underscore.extend(state.options, { roundtrip: roundtrip }, clientOptions), - state) - - // Scenario: User enables Payments, disables it, waits 30+ days, then - // enables it again -> reconcileStamp is in the past. - // In this case reset reconcileStamp to the future. - try { timeUntilReconcile = client.timeUntilReconcile() } catch (ex) {} - let ledgerWindow = (synopsis.options.numFrames - 1) * synopsis.options.frameSize - if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { - client.setTimeUntilReconcile(null, (err, stateResult) => { - if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) - - if (!stateResult) { - return - } - getStateInfo(stateResult) - - muonWriter(pathName(statePath), stateResult) - }) - } - } catch (ex) { - return console.error('ledger client creation error: ', ex) - } - - // speed-up browser start-up by delaying the first synchronization action - setTimeout(() => { - if (!client) return - - if (client.sync(callback) === true) run(random.randomInt({ min: msecs.minute, max: 10 * msecs.minute })) - cacheRuleSet(state.ruleset) - }, 3 * msecs.second) - - // Make sure bravery props are up-to-date with user settings - if (!ledgerInfo.address) ledgerInfo.address = client.getWalletAddress() - setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) - getBalance() - }) - return - } - - if (err.code !== 'ENOENT') console.error('statePath read error: ' + err.toString()) - appActions.updateLedgerInfo({}) - }) -} - -var clientprep = () => { - if (!ledgerClient) ledgerClient = require('ledger-client') - ledgerInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_CLIENT_DEBUG) - publisherInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_DEBUG) - publisherInfo._internal.verboseP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_VERBOSE) -} - -var enable = (paymentsEnabled) => { - if (paymentsEnabled && !getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) - } - - publisherInfo._internal.enabled = paymentsEnabled - if (synopsis) return updatePublisherInfo() - - if (!ledgerPublisher) ledgerPublisher = require('ledger-publisher') - synopsis = new (ledgerPublisher.Synopsis)() - fs.readFile(pathName(synopsisPath), (err, data) => { - var initSynopsis = () => { - var value - - // cf., the `Synopsis` constructor, https://github.com/brave/ledger-publisher/blob/master/index.js#L167 - value = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) - if (!value) { - value = 8 * 1000 - appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME, value) - } - - // for earlier versions of the code... - if ((value > 0) && (value < 1000)) synopsis.options.minPublisherDuration = value * 1000 - - value = getSetting(settings.PAYMENTS_MINIMUM_VISITS) - if (!value) { - value = 1 - appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISITS, value) - } - if (value > 0) synopsis.options.minPublisherVisits = value - - if (process.env.NODE_ENV === 'test') { - synopsis.options.minPublisherDuration = 0 - synopsis.options.minPublisherVisits = 0 - } else { - if (process.env.LEDGER_PUBLISHER_MIN_DURATION) { - synopsis.options.minPublisherDuration = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_DURATION) - } - if (process.env.LEDGER_PUBLISHER_MIN_VISITS) { - synopsis.options.minPublisherVisits = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_VISITS) - } - } - - underscore.keys(synopsis.publishers).forEach((publisher) => { - excludeP(publisher) - verifiedP(publisher) - }) - - updatePublisherInfo() - } - - if (publisherInfo._internal.verboseP) console.log('\nstarting up ledger publisher integration') - - if (err) { - if (err.code !== 'ENOENT') console.error('synopsisPath read error: ' + err.toString()) - initSynopsis() - return updatePublisherInfo() - } - - if (publisherInfo._internal.verboseP) console.log('\nfound ' + pathName(synopsisPath)) - try { - synopsis = new (ledgerPublisher.Synopsis)(data) - } catch (ex) { - console.error('synopsisPath parse error: ' + ex.toString()) - } - initSynopsis() - underscore.keys(synopsis.publishers).forEach((publisher) => { - if (synopsis.publishers[publisher].faviconURL === null) delete synopsis.publishers[publisher].faviconURL - }) - updatePublisherInfo() - - // change undefined include publishers to include publishers - appActions.enableUndefinedPublishers(synopsis.publishers) - - fs.unlink(pathName(publisherPath), (err) => { - if ((err) && (err.code !== 'ENOENT')) { - console.error('error removing file ' + pathName(publisherPath) + ': ', err) - } - }) - fs.unlink(pathName(scoresPath), (err) => { - if ((err) && (err.code !== 'ENOENT')) { - console.error('error removing file ' + pathName(scoresPath) + ': ', err) - } - }) - }) -} - /* * update location information */ @@ -919,39 +330,23 @@ var updateLocation = (location, publisher) => { if (updateP) updateLocationInfo(location) } -/* - * update publisher information - */ - -var publisherInfo = { - options: undefined, - - synopsis: undefined, - - _internal: { - enabled: false, - - ruleset: { raw: [], cooked: [] } - } -} - -const getFavIcon = (entry, page, location) => { - if ((page.protocol) && (!entry.protocol)) { - entry.protocol = page.protocol +const getFavIcon = (publisher, page, location) => { + if ((page.protocol) && (!publisher.protocol)) { + publisher.protocol = page.protocol } - if ((typeof entry.faviconURL === 'undefined') && ((page.faviconURL) || (entry.protocol))) { - let faviconURL = page.faviconURL || entry.protocol + '//' + urlParse(location).host + '/favicon.ico' + if ((typeof publisher.faviconURL === 'undefined') && ((page.faviconURL) || (publisher.protocol))) { + let faviconURL = page.faviconURL || publisher.protocol + '//' + urlParse(location).host + '/favicon.ico' if (publisherInfo._internal.debugP) { console.log('\nrequest: ' + faviconURL) } - entry.faviconURL = null - fetchFavIcon(entry, faviconURL) + publisher.faviconURL = null + fetchFavIcon(publisher, faviconURL) } } -const fetchFavIcon = (entry, url, redirects) => { +const fetchFavIcon = (publisher, url, redirects) => { if (typeof redirects === 'undefined') redirects = 0 request.request({ url: url, responseType: 'blob' }, (err, response, blob) => { @@ -978,7 +373,7 @@ const fetchFavIcon = (entry, url, redirects) => { } if ((response.statusCode === 301) && (response.headers.location)) { - if (redirects < 3) fetchFavIcon(entry, response.headers.location, redirects++) + if (redirects < 3) fetchFavIcon(publisher, response.headers.location, redirects++) return null } @@ -1008,1262 +403,111 @@ const fetchFavIcon = (entry, url, redirects) => { } else if ((tail > 0) && (tail + 8 >= blob.length)) return if (publisherInfo._internal.debugP) { - console.log('\n' + entry.site + ' synopsis=' + - JSON.stringify(underscore.extend(underscore.omit(entry, [ 'faviconURL', 'window' ]), - { faviconURL: entry.faviconURL && '... ' }), null, 2)) + console.log('\n' + publisher.site + ' synopsis=' + + JSON.stringify(underscore.extend(underscore.omit(publisher, [ 'faviconURL', 'window' ]), + { faviconURL: publisher.faviconURL && '... ' }), null, 2)) } - entry.faviconURL = blob + publisher.faviconURL = blob updatePublisherInfo() }) } -var updatePublisherInfo = (changedPublisher) => { - var data - muonWriter(pathName(synopsisPath), synopsis) - if (!publisherInfo._internal.enabled) return +/* + * publisher utilities + */ - publisherInfo.synopsis = synopsisNormalizer(changedPublisher) - publisherInfo.synopsisOptions = synopsis.options +/* + * update ledger information + */ - if (publisherInfo._internal.debugP) { - data = [] - publisherInfo.synopsis.forEach((entry) => { - data.push(underscore.extend(underscore.omit(entry, [ 'faviconURL' ]), { faviconURL: entry.faviconURL && '...' })) - }) +var ledgerInfo = { + creating: false, + created: false, - console.log('\nupdatePublisherInfo: ' + JSON.stringify({ options: publisherInfo.synopsisOptions, synopsis: data }, null, 2)) - } + reconcileFrequency: undefined, + reconcileStamp: undefined, - appActions.updatePublisherInfo(underscore.omit(publisherInfo, [ '_internal' ])) -} + transactions: + [ +/* + { + viewingId: undefined, + surveyorId: undefined, + contribution: { + fiat: { + amount: undefined, + currency: undefined + }, + rates: { + [currency]: undefined // bitcoin value in + }, + satoshis: undefined, + fee: undefined + }, + submissionStamp: undefined, + submissionId: undefined, + count: undefined, + satoshis: undefined, + votes: undefined, + ballots: { + [publisher]: undefined + } + , ... + */ + ], + + // set from ledger client's state.paymentInfo OR client's getWalletProperties + // Bitcoin wallet address + address: undefined, -var blockedP = (publisher) => { - var siteSetting = appStore.getState().get('siteSettings').get(`https?://${publisher}`) + // Bitcoin wallet balance (truncated BTC and satoshis) + balance: undefined, + unconfirmed: undefined, + satoshis: undefined, - return ((!!siteSetting) && (siteSetting.get('ledgerPaymentsShown') === false)) -} + // the desired contribution (the btc value approximates the amount/currency designation) + btc: undefined, + amount: undefined, + currency: undefined, -var stickyP = (publisher) => { - var siteSettings = appStore.getState().get('siteSettings') - var pattern = `https?://${publisher}` - var siteSetting = siteSettings.get(pattern) - var result = (siteSetting) && (siteSetting.get('ledgerPayments')) + paymentURL: undefined, + buyURL: undefined, + bravery: undefined, - // NB: legacy clean-up - if ((typeof result === 'undefined') && (typeof synopsis.publishers[publisher].options.stickyP !== 'undefined')) { - result = synopsis.publishers[publisher].options.stickyP - appActions.changeSiteSetting(pattern, 'ledgerPayments', result) - } + // wallet credentials + paymentId: undefined, + passphrase: undefined, - if (synopsis.publishers[publisher] && - synopsis.publishers[publisher].options && - synopsis.publishers[publisher].options.stickyP) { - delete synopsis.publishers[publisher].options.stickyP - } + // advanced ledger settings + minPublisherDuration: undefined, + minPublisherVisits: undefined, + showOnlyVerified: undefined, - return (result === undefined || result) -} + hasBitcoinHandler: false, -var eligibleP = (publisher) => { - if (!synopsis.options.minPublisherDuration && process.env.NODE_ENV !== 'test') { - synopsis.options.minPublisherDuration = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) - } + // geoIP/exchange information + countryCode: undefined, + exchangeInfo: undefined, - return ((synopsis.publishers[publisher].scores[synopsis.options.scorekeeper] > 0) && - (synopsis.publishers[publisher].duration >= synopsis.options.minPublisherDuration) && - (synopsis.publishers[publisher].visits >= synopsis.options.minPublisherVisits)) + _internal: { + exchangeExpiry: 0, + exchanges: {}, + geoipExpiry: 0 + }, + error: null } -var visibleP = (publisher) => { - if (synopsis.options.showOnlyVerified === undefined) { - synopsis.options.showOnlyVerified = getSetting(settings.PAYMENTS_ALLOW_NON_VERIFIED) - } - - const publisherOptions = synopsis.publishers[publisher].options - const onlyVerified = !synopsis.options.showOnlyVerified - - // Publisher Options - const excludedByUser = blockedP(publisher) - const eligibleByPublisherToggle = stickyP(publisher) != null - const eligibleByNumberOfVisits = eligibleP(publisher) - const isInExclusionList = publisherOptions && publisherOptions.exclude - const verifiedPublisher = publisherOptions && publisherOptions.verified - - // websites not included in exclusion list are eligible by number of visits - // but can be enabled by user action in the publisher toggle - const isEligible = (eligibleByNumberOfVisits && !isInExclusionList) || eligibleByPublisherToggle - - // If user decide to remove the website, don't show it. - if (excludedByUser) { - return false - } - - // Unless user decided to enable publisher with publisherToggle, - // do not show exclusion list. - if (!eligibleByPublisherToggle && isInExclusionList) { - return false - } - - // If verified option is set, only show verified publishers - if (isEligible && onlyVerified) { - return verifiedPublisher - } - - return isEligible -} - -var contributeP = (publisher) => { - return (((stickyP(publisher)) || (synopsis.publishers[publisher].options.exclude !== true)) && - (eligibleP(publisher)) && - (!blockedP(publisher))) -} - -var synopsisNormalizer = (changedPublisher) => { - // courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 - const roundToTarget = (l, target, property) => { - let off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x[property]) }, 0) - - return underscore.sortBy(l, (x) => Math.round(x[property]) - x[property]) - .map((x, i) => { - x[property] = Math.round(x[property]) + (off > i) - (i >= (l.length + off)) - return x - }) - } - - const normalizePinned = (dataPinned, total, target, setOne) => dataPinned.map((publisher) => { - let newPer - let floatNumber - - if (setOne) { - newPer = 1 - floatNumber = 1 - } else { - floatNumber = (publisher.pinPercentage / total) * target - newPer = Math.floor(floatNumber) - if (newPer < 1) { - newPer = 1 - } - } - - publisher.weight = floatNumber - publisher.pinPercentage = newPer - return publisher - }) - - const getPublisherData = (result) => { - let duration = result.duration - - let data = { - verified: result.options.verified || false, - site: result.publisher, - views: result.visits, - duration: duration, - daysSpent: 0, - hoursSpent: 0, - minutesSpent: 0, - secondsSpent: 0, - faviconURL: result.faviconURL, - score: result.scores[scorekeeper], - pinPercentage: result.pinPercentage, - weight: result.pinPercentage - } - // HACK: Protocol is sometimes blank here, so default to http:// so we can - // still generate publisherURL. - data.publisherURL = (result.protocol || 'http:') + '//' + result.publisher - - if (duration >= msecs.day) { - data.daysSpent = Math.max(Math.round(duration / msecs.day), 1) - } else if (duration >= msecs.hour) { - data.hoursSpent = Math.max(Math.floor(duration / msecs.hour), 1) - data.minutesSpent = Math.round((duration % msecs.hour) / msecs.minute) - } else if (duration >= msecs.minute) { - data.minutesSpent = Math.max(Math.round(duration / msecs.minute), 1) - data.secondsSpent = Math.round((duration % msecs.minute) / msecs.second) - } else { - data.secondsSpent = Math.max(Math.round(duration / msecs.second), 1) - } - - return data - } - - let results - let dataPinned = [] - let dataUnPinned = [] - let dataExcluded = [] - let pinnedTotal = 0 - let unPinnedTotal = 0 - const scorekeeper = synopsis.options.scorekeeper - - results = [] - underscore.keys(synopsis.publishers).forEach((publisher) => { - if (!visibleP(publisher)) return - - results.push(underscore.extend({publisher: publisher}, underscore.omit(synopsis.publishers[publisher], 'window'))) - }, synopsis) - results = underscore.sortBy(results, (entry) => { return -entry.scores[scorekeeper] }) - - // move publisher to the correct array and get totals - results.forEach((result) => { - if (result.pinPercentage && result.pinPercentage > 0) { - // pinned - pinnedTotal += result.pinPercentage - dataPinned.push(getPublisherData(result)) - } else if (stickyP(result.publisher)) { - // unpinned - unPinnedTotal += result.scores[scorekeeper] - dataUnPinned.push(result) - } else { - // excluded - let publisher = getPublisherData(result) - publisher.percentage = 0 - publisher.weight = 0 - dataExcluded.push(publisher) - } - }) - - // round if over 100% of pinned publishers - if (pinnedTotal > 100) { - if (changedPublisher) { - const changedObject = dataPinned.filter(publisher => publisher.site === changedPublisher)[0] - const setOne = changedObject.pinPercentage > (100 - dataPinned.length - 1) - - if (setOne) { - changedObject.pinPercentage = 100 - dataPinned.length + 1 - changedObject.weight = changedObject.pinPercentage - } - - const pinnedRestTotal = pinnedTotal - changedObject.pinPercentage - dataPinned = dataPinned.filter(publisher => publisher.site !== changedPublisher) - dataPinned = normalizePinned(dataPinned, pinnedRestTotal, (100 - changedObject.pinPercentage), setOne) - dataPinned = roundToTarget(dataPinned, (100 - changedObject.pinPercentage), 'pinPercentage') - - dataPinned.push(changedObject) - } else { - dataPinned = normalizePinned(dataPinned, pinnedTotal, 100) - dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') - } - - dataUnPinned = dataUnPinned.map((result) => { - let publisher = getPublisherData(result) - publisher.percentage = 0 - publisher.weight = 0 - return publisher - }) - - // sync app store - appActions.changeLedgerPinnedPercentages(dataPinned) - } else if (dataUnPinned.length === 0 && pinnedTotal < 100) { - // when you don't have any unpinned sites and pinned total is less then 100 % - dataPinned = normalizePinned(dataPinned, pinnedTotal, 100, false) - dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') - - // sync app store - appActions.changeLedgerPinnedPercentages(dataPinned) - } else { - // unpinned publishers - dataUnPinned = dataUnPinned.map((result) => { - let publisher = getPublisherData(result) - const floatNumber = (publisher.score / unPinnedTotal) * (100 - pinnedTotal) - publisher.percentage = Math.round(floatNumber) - publisher.weight = floatNumber - return publisher - }) - - // normalize unpinned values - dataUnPinned = roundToTarget(dataUnPinned, (100 - pinnedTotal), 'percentage') - } - - const newData = dataPinned.concat(dataUnPinned, dataExcluded) - - // sync synopsis - newData.forEach((item) => { - synopsis.publishers[item.site].weight = item.weight - synopsis.publishers[item.site].pinPercentage = item.pinPercentage - }) - - return newData -} - -/* - * publisher utilities - */ - -var currentLocation = 'NOOP' -var currentTimestamp = underscore.now() - -var visit = (location, timestamp, tabId) => { - var setLocation = () => { - var duration, publisher, revisitP - - if (!synopsis) return - - if (publisherInfo._internal.verboseP) { - console.log('locations[' + currentLocation + ']=' + JSON.stringify(locations[currentLocation], null, 2) + - ' duration=' + (timestamp - currentTimestamp) + ' msec' + ' tabId=' + tabId) - } - if ((location === currentLocation) || (!locations[currentLocation]) || (!tabId)) return - - publisher = locations[currentLocation].publisher - if (!publisher) return - - if (!publishers[publisher]) publishers[publisher] = {} - if (!publishers[publisher][currentLocation]) publishers[publisher][currentLocation] = { tabIds: [] } - publishers[publisher][currentLocation].timestamp = timestamp - revisitP = publishers[publisher][currentLocation].tabIds.indexOf(tabId) !== -1 - if (!revisitP) publishers[publisher][currentLocation].tabIds.push(tabId) - - duration = timestamp - currentTimestamp - if (publisherInfo._internal.verboseP) { - console.log('\nadd publisher ' + publisher + ': ' + duration + ' msec' + ' revisitP=' + revisitP + ' state=' + - JSON.stringify(underscore.extend({ location: currentLocation }, publishers[publisher][currentLocation]), - null, 2)) - } - synopsis.addPublisher(publisher, { duration: duration, revisitP: revisitP }) - updatePublisherInfo() - verifiedP(publisher) - } - - setLocation() - if (location === currentLocation) return - - currentLocation = location.match(/^about/) ? 'NOOP' : location - currentTimestamp = timestamp -} - -var cacheRuleSet = (ruleset) => { - var stewed, syncP - - if ((!ruleset) || (underscore.isEqual(publisherInfo._internal.ruleset.raw, ruleset))) return - - try { - stewed = [] - ruleset.forEach((rule) => { - var entry = { condition: acorn.parse(rule.condition) } - - if (rule.dom) { - if (rule.dom.publisher) { - entry.publisher = { selector: rule.dom.publisher.nodeSelector, - consequent: acorn.parse(rule.dom.publisher.consequent) - } - } - if (rule.dom.faviconURL) { - entry.faviconURL = { selector: rule.dom.faviconURL.nodeSelector, - consequent: acorn.parse(rule.dom.faviconURL.consequent) - } - } - } - if (!entry.publisher) entry.consequent = rule.consequent ? acorn.parse(rule.consequent) : rule.consequent - - stewed.push(entry) - }) - - publisherInfo._internal.ruleset.raw = ruleset - publisherInfo._internal.ruleset.cooked = stewed - if (!synopsis) return - - underscore.keys(synopsis.publishers).forEach((publisher) => { - var location = (synopsis.publishers[publisher].protocol || 'http:') + '//' + publisher - var ctx = urlParse(location, true) - - ctx.TLD = tldjs.getPublicSuffix(ctx.host) - if (!ctx.TLD) return - - ctx = underscore.mapObject(ctx, function (value, key) { if (!underscore.isFunction(value)) return value }) - ctx.URL = location - ctx.SLD = tldjs.getDomain(ctx.host) - ctx.RLD = tldjs.getSubdomain(ctx.host) - ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' - - stewed.forEach((rule) => { - if ((rule.consequent !== null) || (rule.dom)) return - if (!rulesolver.resolve(rule.condition, ctx)) return - - if (publisherInfo._internal.verboseP) console.log('\npurging ' + publisher) - delete synopsis.publishers[publisher] - delete publishers[publisher] - syncP = true - }) - }) - if (!syncP) return - - updatePublisherInfo() - } catch (ex) { - console.error('ruleset error: ', ex) - } -} - -var excludeP = (publisher, callback) => { - var doneP - - var done = (err, result) => { - doneP = true - if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && - (synopsis.publishers[publisher].options.exclude !== result)) { - synopsis.publishers[publisher].options.exclude = result - updatePublisherInfo() - } - - if (callback) callback(err, result) - } - - if (!v2RulesetDB) return setTimeout(() => { excludeP(publisher, callback) }, 5 * msecs.second) - - inspectP(v2RulesetDB, v2RulesetPath, publisher, 'exclude', 'domain:' + publisher, (err, result) => { - var props - - if (!err) return done(err, result.exclude) - - props = ledgerPublisher.getPublisherProps('https://' + publisher) - if (!props) return done() - - v2RulesetDB.createReadStream({ lt: 'domain:' }).on('data', (data) => { - var regexp, result, sldP, tldP - - if (doneP) return - - sldP = data.key.indexOf('SLD:') === 0 - tldP = data.key.indexOf('TLD:') === 0 - if ((!tldP) && (!sldP)) return - - if (underscore.intersection(data.key.split(''), - [ '^', '$', '*', '+', '?', '[', '(', '{', '|' ]).length === 0) { - if ((data.key !== ('TLD:' + props.TLD)) && (props.SLD && data.key !== ('SLD:' + props.SLD.split('.')[0]))) return - } else { - try { - regexp = new RegExp(data.key.substr(4)) - if (!regexp.test(props[tldP ? 'TLD' : 'SLD'])) return - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid regexp ' + data.key + ': ' + ex.toString()) - } - } - - try { - result = JSON.parse(data.value) - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid JSON ' + data.entry + ': ' + data.value) - } - - done(null, result.exclude) - }).on('error', (err) => { - console.error(v2RulesetPath + ' stream error: ' + JSON.stringify(err, null, 2)) - }).on('close', () => { - }).on('end', () => { - if (!doneP) done(null, false) - }) - }) -} - -var verifiedP = (publisher, callback) => { - inspectP(v2PublishersDB, v2PublishersPath, publisher, 'verified', null, callback) - - if (process.env.NODE_ENV === 'test') { - testVerifiedPublishers.forEach((publisher) => { - if (synopsis.publishers[publisher]) { - if (!synopsis.publishers[publisher].options) { - synopsis.publishers[publisher].options = {} - } - - synopsis.publishers[publisher].options.verified = true - } - }) - updatePublisherInfo() - } -} - -var inspectP = (db, path, publisher, property, key, callback) => { - var done = (err, result) => { - if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && - (synopsis.publishers[publisher].options[property] !== result[property])) { - synopsis.publishers[publisher].options[property] = result[property] - updatePublisherInfo() - } - - if (callback) callback(err, result) - } - - if (!key) key = publisher - db.get(key, (err, value) => { - var result - - if (err) { - if (!err.notFound) console.error(path + ' get ' + key + ' error: ' + JSON.stringify(err, null, 2)) - return done(err) - } - - try { - result = JSON.parse(value) - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid JSON ' + key + ': ' + value) - result = {} - } - - done(null, result) - }) -} - -/* - * update ledger information - */ - -var ledgerInfo = { - creating: false, - created: false, - - reconcileFrequency: undefined, - reconcileStamp: undefined, - - transactions: - [ -/* - { - viewingId: undefined, - surveyorId: undefined, - contribution: { - fiat: { - amount: undefined, - currency: undefined - }, - rates: { - [currency]: undefined // bitcoin value in - }, - satoshis: undefined, - fee: undefined - }, - submissionStamp: undefined, - submissionId: undefined, - count: undefined, - satoshis: undefined, - votes: undefined, - ballots: { - [publisher]: undefined - } - , ... - */ - ], - - // set from ledger client's state.paymentInfo OR client's getWalletProperties - // Bitcoin wallet address - address: undefined, - - // Bitcoin wallet balance (truncated BTC and satoshis) - balance: undefined, - unconfirmed: undefined, - satoshis: undefined, - - // the desired contribution (the btc value approximates the amount/currency designation) - btc: undefined, - amount: undefined, - currency: undefined, - - paymentURL: undefined, - buyURL: undefined, - bravery: undefined, - - // wallet credentials - paymentId: undefined, - passphrase: undefined, - - // advanced ledger settings - minPublisherDuration: undefined, - minPublisherVisits: undefined, - showOnlyVerified: undefined, - - hasBitcoinHandler: false, - - // geoIP/exchange information - countryCode: undefined, - exchangeInfo: undefined, - - _internal: { - exchangeExpiry: 0, - exchanges: {}, - geoipExpiry: 0 - }, - error: null -} - -var updateLedgerInfo = () => { - var info = ledgerInfo._internal.paymentInfo - var now = underscore.now() - - if (info) { - underscore.extend(ledgerInfo, - underscore.pick(info, [ 'address', 'passphrase', 'balance', 'unconfirmed', 'satoshis', 'btc', 'amount', - 'currency' ])) - if ((!info.buyURLExpires) || (info.buyURLExpires > now)) { - ledgerInfo.buyURL = info.buyURL - ledgerInfo.buyMaximumUSD = 6 - } - if (typeof process.env.ADDFUNDS_URL !== 'undefined') { - ledgerInfo.buyURLFrame = true - ledgerInfo.buyURL = process.env.ADDFUNDS_URL + '?' + - querystring.stringify({ currency: ledgerInfo.currency, - amount: getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT), - address: ledgerInfo.address }) - ledgerInfo.buyMaximumUSD = false - } - - underscore.extend(ledgerInfo, ledgerInfo._internal.cache || {}) - } - - if ((client) && (now > ledgerInfo._internal.geoipExpiry)) { - ledgerInfo._internal.geoipExpiry = now + (5 * msecs.minute) - - if (!ledgerGeoIP) ledgerGeoIP = require('ledger-geoip') - return ledgerGeoIP.getGeoIP(client.options, (err, provider, result) => { - if (err) console.warn('ledger geoip warning: ' + JSON.stringify(err, null, 2)) - if (result) ledgerInfo.countryCode = result - - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - - if (now <= ledgerInfo._internal.exchangeExpiry) return updateLedgerInfo() - - ledgerInfo._internal.exchangeExpiry = now + msecs.day - roundtrip({ path: '/v1/exchange/providers' }, client.options, (err, response, body) => { - if (err) console.error('ledger exchange error: ' + JSON.stringify(err, null, 2)) - - ledgerInfo._internal.exchanges = body || {} - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - updateLedgerInfo() - }) - }) - } - - if (ledgerInfo._internal.debugP) { - console.log('\nupdateLedgerInfo: ' + JSON.stringify(underscore.omit(ledgerInfo, [ '_internal' ]), null, 2)) - } - - appActions.updateLedgerInfo(underscore.omit(ledgerInfo, [ '_internal' ])) -} - -/* - * ledger client callbacks - */ - -var callback = (err, result, delayTime) => { - var results - var entries = client && client.report() - - if (clientOptions.verboseP) { - console.log('\nledger client callback: clientP=' + (!!client) + ' errP=' + (!!err) + ' resultP=' + (!!result) + - ' delayTime=' + delayTime) - } - - if (err) { - console.log('ledger client error(1): ' + JSON.stringify(err, null, 2) + (err.stack ? ('\n' + err.stack) : '')) - if (!client) return - - if (typeof delayTime === 'undefined') delayTime = random.randomInt({ min: msecs.minute, max: 10 * msecs.minute }) - } - - if (!result) return run(delayTime) - - if ((client) && (result.properties.wallet)) { - if (!ledgerInfo.created) setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) - - getStateInfo(result) - getPaymentInfo() - } - cacheRuleSet(result.ruleset) - if (result.rulesetV2) { - results = result.rulesetV2 - delete result.rulesetV2 - - entries = [] - results.forEach((entry) => { - var key = entry.facet + ':' + entry.publisher - - if (entry.exclude !== false) { - entries.push({ type: 'put', key: key, value: JSON.stringify(underscore.omit(entry, [ 'facet', 'publisher' ])) }) - } else { - entries.push({ type: 'del', key: key }) - } - }) - - v2RulesetDB.batch(entries, (err) => { - if (err) return console.error(v2RulesetPath + ' error: ' + JSON.stringify(err, null, 2)) - - if (entries.length === 0) return - - underscore.keys(synopsis.publishers).forEach((publisher) => { -// be safe... - if (synopsis.publishers[publisher]) delete synopsis.publishers[publisher].options.exclude - - excludeP(publisher) - }) - }) - } - if (result.publishersV2) { - results = result.publishersV2 - delete result.publishersV2 - - entries = [] - results.forEach((entry) => { - entries.push({ type: 'put', - key: entry.publisher, - value: JSON.stringify(underscore.omit(entry, [ 'publisher' ])) - }) - if ((synopsis.publishers[entry.publisher]) && - (synopsis.publishers[entry.publisher].options.verified !== entry.verified)) { - synopsis.publishers[entry.publisher].options.verified = entry.verified - updatePublisherInfo() - } - }) - v2PublishersDB.batch(entries, (err) => { - if (err) return console.error(v2PublishersPath + ' error: ' + JSON.stringify(err, null, 2)) - }) - } - - muonWriter(pathName(statePath), result) - run(delayTime) -} - -var roundtrip = (params, options, callback) => { - var i - var parts = typeof params.server === 'string' ? urlParse(params.server) - : typeof params.server !== 'undefined' ? params.server - : typeof options.server === 'string' ? urlParse(options.server) : options.server - var rawP = options.rawP - - if (!params.method) params.method = 'GET' - parts = underscore.extend(underscore.pick(parts, [ 'protocol', 'hostname', 'port' ]), - underscore.omit(params, [ 'headers', 'payload', 'timeout' ])) - -// TBD: let the user configure this via preferences [MTR] - if ((parts.hostname === 'ledger.brave.com') && (params.useProxy)) parts.hostname = 'ledger-proxy.privateinternetaccess.com' - - i = parts.path.indexOf('?') - if (i !== -1) { - parts.pathname = parts.path.substring(0, i) - parts.search = parts.path.substring(i) - } else { - parts.pathname = parts.path - } - - options = { - url: urlFormat(parts), - method: params.method, - payload: params.payload, - responseType: 'text', - headers: underscore.defaults(params.headers || {}, { 'content-type': 'application/json; charset=utf-8' }), - verboseP: options.verboseP - } - request.request(options, (err, response, body) => { - var payload - - if ((response) && (options.verboseP)) { - console.log('[ response for ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path + ' ]') - console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + - ' ' + (response.statusMessage || '')) - underscore.keys(response.headers).forEach((header) => { console.log('>>> ' + header + ': ' + response.headers[header]) }) - console.log('>>>') - console.log('>>> ' + (body || '').split('\n').join('\n>>> ')) - } - - if (err) return callback(err) - - if (Math.floor(response.statusCode / 100) !== 2) { - return callback(new Error('HTTP response ' + response.statusCode) + ' for ' + params.method + ' ' + params.path) - } - - try { - payload = rawP ? body : (response.statusCode !== 204) ? JSON.parse(body) : null - } catch (err) { - return callback(err) - } - - try { - callback(null, response, payload) - } catch (err0) { - if (options.verboseP) console.log('\ncallback: ' + err0.toString() + '\n' + err0.stack) - } - }) - - if (!options.verboseP) return - - console.log('<<< ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path) - underscore.keys(options.headers).forEach((header) => { console.log('<<< ' + header + ': ' + options.headers[header]) }) - console.log('<<<') - if (options.payload) console.log('<<< ' + JSON.stringify(params.payload, null, 2).split('\n').join('\n<<< ')) -} - -var runTimeoutId = false - -var run = (delayTime) => { - if (clientOptions.verboseP) { - var entries - - console.log('\nledger client run: clientP=' + (!!client) + ' delayTime=' + delayTime) - - var line = (fields) => { - var result = '' - - fields.forEach((field) => { - var spaces - var max = (result.length > 0) ? 9 : 19 - - if (typeof field !== 'string') field = field.toString() - if (field.length < max) { - spaces = ' '.repeat(max - field.length) - field = spaces + field - } else { - field = field.substr(0, max) - } - result += ' ' + field - }) - - console.log(result.substr(1)) - } - - line([ 'publisher', - 'blockedP', 'stickyP', 'verified', - 'excluded', 'eligibleP', 'visibleP', - 'contribP', - 'duration', 'visits' - ]) - entries = synopsis.topN() || [] - entries.forEach((entry) => { - var publisher = entry.publisher - - line([ publisher, - blockedP(publisher), stickyP(publisher), synopsis.publishers[publisher].options.verified === true, - synopsis.publishers[publisher].options.exclude === true, eligibleP(publisher), visibleP(publisher), - contributeP(publisher), - Math.round(synopsis.publishers[publisher].duration / 1000), synopsis.publishers[publisher].visits ]) - }) - } - - if ((typeof delayTime === 'undefined') || (!client)) return - - var active, state, weights, winners - var ballots = client.ballots() - var data = (synopsis) && (ballots > 0) && synopsisNormalizer() - - if (data) { - weights = [] - data.forEach((datum) => { weights.push({ publisher: datum.site, weight: datum.weight / 100.0 }) }) - winners = synopsis.winners(ballots, weights) - } - if (!winners) winners = [] - - try { - winners.forEach((winner) => { - var result - - if (!contributeP(winner)) return - - result = client.vote(winner) - if (result) state = result - }) - if (state) muonWriter(pathName(statePath), state) - } catch (ex) { - console.log('ledger client error(2): ' + ex.toString() + (ex.stack ? ('\n' + ex.stack) : '')) - } - - if (delayTime === 0) { - try { - delayTime = client.timeUntilReconcile() - } catch (ex) { - delayTime = false - } - if (delayTime === false) delayTime = random.randomInt({ min: msecs.minute, max: 10 * msecs.minute }) - } - if (delayTime > 0) { - if (runTimeoutId) return - - active = client - if (delayTime > (1 * msecs.hour)) delayTime = random.randomInt({ min: 3 * msecs.minute, max: msecs.hour }) - - runTimeoutId = setTimeout(() => { - runTimeoutId = false - if (active !== client) return - - if (!client) return console.log('\n\n*** MTR says this can\'t happen(1)... please tell him that he\'s wrong!\n\n') - - if (client.sync(callback) === true) return run(0) - }, delayTime) - return - } - - if (client.isReadyToReconcile()) return client.reconcile(uuid.v4().toLowerCase(), callback) - - console.log('what? wait, how can this happen?') -} - -var getStateInfo = (state) => { - var ballots, i, transaction - var info = state.paymentInfo - var then = underscore.now() - msecs.year - - if (!state.properties.wallet) return - - ledgerInfo.paymentId = state.properties.wallet.paymentId - ledgerInfo.passphrase = state.properties.wallet.keychains.passphrase - - ledgerInfo.created = !!state.properties.wallet - ledgerInfo.creating = !ledgerInfo.created - - ledgerInfo.reconcileFrequency = state.properties.days - ledgerInfo.reconcileStamp = state.reconcileStamp - - if (info) { - ledgerInfo._internal.paymentInfo = info - cacheReturnValue() - } - - ledgerInfo.transactions = [] - if (!state.transactions) return updateLedgerInfo() - - for (i = state.transactions.length - 1; i >= 0; i--) { - transaction = state.transactions[i] - if (transaction.stamp < then) break - - if ((!transaction.ballots) || (transaction.ballots.length < transaction.count)) continue - - ballots = underscore.clone(transaction.ballots || {}) - state.ballots.forEach((ballot) => { - if (ballot.viewingId !== transaction.viewingId) return - - if (!ballots[ballot.publisher]) ballots[ballot.publisher] = 0 - ballots[ballot.publisher]++ - }) - - ledgerInfo.transactions.push(underscore.extend(underscore.pick(transaction, - [ 'viewingId', 'contribution', 'submissionStamp', 'count' ]), - { ballots: ballots })) - } - - observeTransactions(state.transactions) - updateLedgerInfo() -} - -// Observe ledger client state.transactions for changes. -// Called by getStateInfo(). Updated state provided by ledger-client. -var cachedTransactions = null -var observeTransactions = (transactions) => { - if (underscore.isEqual(cachedTransactions, transactions)) { - return - } - // Notify the user of new transactions. - if (getSetting(settings.PAYMENTS_NOTIFICATIONS) && cachedTransactions !== null) { - const newTransactions = underscore.difference(transactions, cachedTransactions) - if (newTransactions.length > 0) { - const newestTransaction = newTransactions[newTransactions.length - 1] - showNotificationPaymentDone(newestTransaction.contribution.fiat) - } - } - cachedTransactions = underscore.clone(transactions) -} - -var balanceTimeoutId = false - -var getBalance = () => { - if (!client) return - - balanceTimeoutId = setTimeout(getBalance, 1 * msecs.minute) - if (!ledgerInfo.address) return - - if (!ledgerBalance) ledgerBalance = require('ledger-balance') - ledgerBalance.getBalance(ledgerInfo.address, underscore.extend({ balancesP: true }, client.options), - (err, provider, result) => { - var unconfirmed - var info = ledgerInfo._internal.paymentInfo - - if (err) return console.warn('ledger balance warning: ' + JSON.stringify(err, null, 2)) - - if (typeof result.unconfirmed === 'undefined') return - - if (result.unconfirmed > 0) { - unconfirmed = (result.unconfirmed / 1e8).toFixed(4) - if ((info || ledgerInfo).unconfirmed === unconfirmed) return - - ledgerInfo.unconfirmed = unconfirmed - if (info) info.unconfirmed = ledgerInfo.unconfirmed - if (clientOptions.verboseP) console.log('\ngetBalance refreshes ledger info: ' + ledgerInfo.unconfirmed) - return updateLedgerInfo() - } - - if (ledgerInfo.unconfirmed === '0.0000') return - - if (clientOptions.verboseP) console.log('\ngetBalance refreshes payment info') - getPaymentInfo() - }) -} - -var logError = (err, caller) => { - if (err) { - ledgerInfo.error = { - caller: caller, - error: err - } - console.error('Error in %j: %j', caller, err) - return true - } else { - ledgerInfo.error = null - return false - } -} - -var getPaymentInfo = () => { - var amount, currency - - if (!client) return - - try { - ledgerInfo.bravery = client.getBraveryProperties() - if (ledgerInfo.bravery.fee) { - amount = ledgerInfo.bravery.fee.amount - currency = ledgerInfo.bravery.fee.currency - } - - client.getWalletProperties(amount, currency, function (err, body) { - var info = ledgerInfo._internal.paymentInfo || {} - - if (logError(err, 'getWalletProperties')) { - return - } - - info = underscore.extend(info, underscore.pick(body, [ 'buyURL', 'buyURLExpires', 'balance', 'unconfirmed', 'satoshis' ])) - info.address = client.getWalletAddress() - if ((amount) && (currency)) { - info = underscore.extend(info, { amount: amount, currency: currency }) - if ((body.rates) && (body.rates[currency])) { - info.btc = (amount / body.rates[currency]).toFixed(8) - } - } - ledgerInfo._internal.paymentInfo = info - updateLedgerInfo() - cacheReturnValue() - }) - } catch (ex) { - console.error('properties error: ' + ex.toString()) - } -} - -var setPaymentInfo = (amount) => { - var bravery - - if (!client) return - - try { bravery = client.getBraveryProperties() } catch (ex) { -// wallet being created... - - return setTimeout(function () { setPaymentInfo(amount) }, 2 * msecs.second) - } - - amount = parseInt(amount, 10) - if (isNaN(amount) || (amount <= 0)) return - - underscore.extend(bravery.fee, { amount: amount }) - client.setBraveryProperties(bravery, (err, result) => { - if (err) return console.error('ledger setBraveryProperties: ' + err.toString()) - - if (result) muonWriter(pathName(statePath), result) - }) - if (ledgerInfo.created) getPaymentInfo() -} - -var cacheReturnValue = () => { - var chunks, cache, paymentURL - var info = ledgerInfo._internal.paymentInfo - - if (!info) return - - if (!ledgerInfo._internal.cache) ledgerInfo._internal.cache = {} - cache = ledgerInfo._internal.cache - - paymentURL = 'bitcoin:' + info.address + '?amount=' + info.btc + '&label=' + encodeURI('Brave Software') - if (cache.paymentURL === paymentURL) return - - cache.paymentURL = paymentURL - updateLedgerInfo() - try { - chunks = [] - - qr.image(paymentURL, { type: 'png' }).on('data', (chunk) => { chunks.push(chunk) }).on('end', () => { - cache.paymentIMG = 'data:image/png;base64,' + Buffer.concat(chunks).toString('base64') - updateLedgerInfo() - }) - } catch (ex) { - console.error('qr.imageSync error: ' + ex.toString()) - } -} - -var networkConnected = underscore.debounce(() => { - if (!client) return - - if (runTimeoutId) { - clearTimeout(runTimeoutId) - runTimeoutId = false - } - if (client.sync(callback) === true) run(random.randomInt({ min: msecs.minute, max: 10 * msecs.minute })) - - if (balanceTimeoutId) clearTimeout(balanceTimeoutId) - balanceTimeoutId = setTimeout(getBalance, 5 * msecs.second) -}, 1 * msecs.minute, true) +/* + * ledger client callbacks + */ /* * low-level utilities */ -var muonWriter = (path, payload) => { - muon.file.writeImportant(path, JSON.stringify(payload, null, 2), (success) => { - if (!success) return console.error('write error: ' + path) - - if ((quitP) && (!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { - if (ledgerInfo._internal.debugP) console.log('\ndeleting ' + path) - return fs.unlink(path, (err) => { if (err) console.error('unlink error: ' + err.toString()) }) - } - - if (ledgerInfo._internal.debugP) console.log('\nwrote ' + path) - }) -} - -var pathName = (name) => { - var parts = path.parse(name) - - return path.join(app.getPath('userData'), parts.name + parts.ext) -} - -/* - * UI controller functionality - */ - -const showNotifications = () => { - if (getSetting(settings.PAYMENTS_ENABLED)) { - if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) showEnabledNotifications() - } else { - showDisabledNotifications() - } -} - -// When Payments is disabled -const showDisabledNotifications = () => { - if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { - const firstRunTimestamp = appStore.getState().get('firstRunTimestamp') - if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) { - return - } - notificationTryPaymentsMessage = locale.translation('notificationTryPayments') - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: notificationTryPaymentsMessage, - buttons: [ - {text: locale.translation('noThanks')}, - {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) - } -} - -/** -* Show message that it's time to add funds if reconciliation is less than -* a day in the future and balance is too low. -* 24 hours prior to reconciliation, show message asking user to review -* their votes. -*/ -const showEnabledNotifications = () => { - const reconcileStamp = ledgerInfo.reconcileStamp - - if (!reconcileStamp) return - - if (reconcileStamp - underscore.now() < msecs.day) { - if (sufficientBalanceToReconcile()) { - if (shouldShowNotificationReviewPublishers()) { - showNotificationReviewPublishers(reconcileStamp + ((ledgerInfo.reconcileFrequency - 2) * msecs.day)) - } - } else if (shouldShowNotificationAddFunds()) { - showNotificationAddFunds() - } - } else if (reconcileStamp - underscore.now() < 2 * msecs.day) { - if (sufficientBalanceToReconcile() && (shouldShowNotificationReviewPublishers())) { - showNotificationReviewPublishers(underscore.now() + msecs.day) - } - } -} - -const sufficientBalanceToReconcile = () => { - const balance = Number(ledgerInfo.balance || 0) - const unconfirmed = Number(ledgerInfo.unconfirmed || 0) - return ledgerInfo.btc && - (balance + unconfirmed > 0.9 * Number(ledgerInfo.btc)) -} - -const shouldShowNotificationAddFunds = () => { - const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP) - return !nextTime || (underscore.now() > nextTime) -} -const showNotificationAddFunds = () => { - const nextTime = underscore.now() + (3 * msecs.day) - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) - - addFundsMessage = addFundsMessage || locale.translation('addFundsNotification') - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: addFundsMessage, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('updateLater')}, - {text: locale.translation('addFunds'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} - -const shouldShowNotificationReviewPublishers = () => { - const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP) - return !nextTime || (underscore.now() > nextTime) -} - -const showNotificationReviewPublishers = (nextTime) => { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) - - reconciliationMessage = reconciliationMessage || locale.translation('reconciliationNotification') - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: reconciliationMessage, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('dismiss')}, - {text: locale.translation('reviewSites'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} - -// Called from observeTransactions() when we see a new payment (transaction). -const showNotificationPaymentDone = (transactionContributionFiat) => { - notificationPaymentDoneMessage = locale.translation('notificationPaymentDone') - .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount) - .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency) - // Hide the 'waiting for deposit' message box if it exists - appActions.hideNotification(addFundsMessage) - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: notificationPaymentDoneMessage, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('Ok'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} module.exports = { init: init, diff --git a/app/sessionStore.js b/app/sessionStore.js index 5b8568883db..506daab1967 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -393,6 +393,7 @@ module.exports.cleanAppData = (immutableData, isShutdown) => { } immutableData = immutableData.delete('menu') + immutableData = immutableData.delete('pageData') try { immutableData = tabState.getPersistentState(immutableData) @@ -956,7 +957,17 @@ module.exports.defaultAppState = () => { count: 0 }, defaultWindowParams: {}, - searchDetail: null + searchDetail: null, + pageData: { + info: {}, + last: { + info: '', + url: '', + tabId: -1 + }, + load: [], + view: {} + } } } diff --git a/docs/state.md b/docs/state.md index 5e51d830c95..761abfaaa5f 100644 --- a/docs/state.md +++ b/docs/state.md @@ -174,6 +174,115 @@ AppStore themeColor: string } }, + ledger: { + info: { + address: string, // the BTC wallet address (in base58) + amount: number, // fiat amount to contribute per reconciliation period + balance: string, // confirmed balance in BTC.toFixed(4) + bravery: { + fee: { + amount: number, // set from `amount` above + currency: string // set from `currency` above + } + }, // values round-tripped through the ledger-client + btc: string, // BTC to contribute per reconciliation period + buyURL: string, // URL to buy bitcoin using debit/credit card + countryCode: string, // ISO3166 2-letter code for country of browser's location + created: boolean, // wallet is created + creating: boolean, // wallet is being created + currency: string, // fiat currency denominating the amount + error: { + caller: string, // function in which error was handled + error: object // error object returned + }, // non-null if the last updateLedgerInfo happened concurrently with an error + exchangeInfo: { + exchangeName: string, // the name of the BTC exchange + exchangeURL: string // the URL of the BTC exchange + }, // information about the corresponding "friendliest" BTC exchange (suggestions welcome!) + hasBitcoinHandler: boolean, // brave browser has a `bitcoin:` URI handler + passphrase: string, // the BTC wallet passphrase + paymentIMG: string, // the QR code equivalent of `paymentURL` expressed as "data:image/...;base64,..." + paymentURL: string, // bitcoin:...?amount={btc}&label=Brave%20Software + reconcileFrequency: number, // duration between each reconciliation in days + reconcileStamp: number, // timestamp for the next reconcilation + recoverySucceeded: boolean, // the status of an attempted recovery + satoshis: number, // confirmed balance as an integer number of satoshis + transactions: [{ + ballots: { + [publisher]: number // e.g., "wikipedia.org": 3 + }, // number of ballots cast for each publisher + contribution: { + fee: number, // bitcoin transaction fee + fiat: { + amount: number, // e.g., 5 + currency: string // e.g., "USD" + }, // roughly-equivalent fiat amount + rates: { + [currency]: number //e.g., { "USD": 575.45 } + }, // exchange rate + satoshis: number, // actual number of satoshis transferred + }, + count: number, // total number of ballots allowed to be cast + submissionStamp: number, // timestamp for this contribution + viewingId: string, // UUIDv4 for this contribution + }], // contributions reconciling/reconciled + unconfirmed: string // unconfirmed balance in BTC.toFixed(4) + }, + isBooting: boolean, // flag which telll us if wallet is still creating or not + isQuiting: boolan, // flag which tell us if we are closing ledger (because of browser close) + locations: { + [url]: { + publisher: string, // url of the publisher in question + verified: boolean, // wheter or not site is a verified publisher + exclude: boolean, // wheter or not site is in the excluded list + stickyP: boolean, // wheter or not site was added using addFunds urlbar toggle + timestamp: number // timestamp in milliseconds + } + } + synopsis: { + options: { + emptyScores: { + concave: number, + visits: number + }, + frameSize: number, + minPublisherDuration: number, + minPublisherVisits: number, + numFrames: number, + scorekeeper: string, // concave or visits + scorekeepers: Array, // concave and visits + showOnlyVerified: boolean + }, + publishers: { + [publisherId]: { + duration: number, + faviconUrl: string, + options: { + exclude: boolean, + verified: boolean, + stickyP: boolean + }, + pinPercentage: number, + protocol: string, + scores: { + concave: number, + visits: number + }, + visits: number, + weight: number, + window: [{ + timestamp: number, + visits: number, + duration: number, + scores: { + concave: number, + visits: number + } + }] + } + } + } + }, menu: { template: object // used on Windows and by our tests: template object with Menubar control }, @@ -194,6 +303,39 @@ AppStore noScript: { enabled: boolean // enable noscript }, + pageData: { + info: [{ + faviconURL: string, + protocol: string, + publisher: string, + timestamp: number, + url: string, + }], + last: { + info: string, // last added info + tabId: number, // last active tabId + url: string // last active URL + }, + load: [{ + timestamp: number, + url: string, + tabId: number, + details: { + status: boolean, + newURL: string, + originalURL: string, + httpResponseCode: number, + requestMethod: string, + referrer: string, + resourceType: string + } + }], + view: { + timestamp: number, + url: string, + tabId: number + } // we save only the last view + }, pinnedSites: { [siteKey]: { location: string, @@ -285,9 +427,6 @@ AppStore 'advanced.minimum-visits': number, 'advanced.auto-suggest-sites': boolean // show auto suggestion }, - locationSiteKeyCache: { - [location]: Array. // location -> site keys - }, siteSettings: { [hostPattern]: { adControl: string, // (showBraveAds | blockAds | allowAdsAndTracking) @@ -575,59 +714,6 @@ WindowStore type: number }, lastAppVersion: string, // version of the last file that was saved - ledgerInfo: { - address: string, // the BTC wallet address (in base58) - amount: number, // fiat amount to contribute per reconciliation period - balance: string, // confirmed balance in BTC.toFixed(4) - bravery: { - fee: { - amount: number, // set from `amount` above - currency: string // set from `currency` above - } - }, // values round-tripped through the ledger-client - btc: string, // BTC to contribute per reconciliation period - buyURL: string, // URL to buy bitcoin using debit/credit card - countryCode: string, // ISO3166 2-letter code for country of browser's location - created: boolean, // wallet is created - creating: boolean, // wallet is being created - currency: string, // fiat currency denominating the amount - error: { - caller: string, // function in which error was handled - error: object // error object returned - }, // non-null if the last updateLedgerInfo happened concurrently with an error - exchangeInfo: { - exchangeName: string, // the name of the BTC exchange - exchangeURL: string // the URL of the BTC exchange - }, // information about the corresponding "friendliest" BTC exchange (suggestions welcome!) - hasBitcoinHandler: boolean, // brave browser has a `bitcoin:` URI handler - passphrase: string, // the BTC wallet passphrase - paymentIMG: string, // the QR code equivalent of `paymentURL` expressed as "data:image/...;base64,..." - paymentURL: string, // bitcoin:...?amount={btc}&label=Brave%20Software - reconcileFrequency: number, // duration between each reconciliation in days - reconcileStamp: number, // timestamp for the next reconcilation - recoverySucceeded: boolean, // the status of an attempted recovery - satoshis: number, // confirmed balance as an integer number of satoshis - transactions: [{ - ballots: { - [publisher]: number // e.g., "wikipedia.org": 3 - }, // number of ballots cast for each publisher - contribution: { - fee: number, // bitcoin transaction fee - fiat: { - amount: number, // e.g., 5 - currency: string // e.g., "USD" - }, // roughly-equivalent fiat amount - rates: { - [currency]: number //e.g., { "USD": 575.45 } - }, // exchange rate - satoshis: number, // actual number of satoshis transferred - }, - count: number, // total number of ballots allowed to be cast - submissionStamp: number, // timestamp for this contribution - viewingId: string, // UUIDv4 for this contribution - }], // contributions reconciling/reconciled - unconfirmed: string // unconfirmed balance in BTC.toFixed(4) - }, modalDialogDetail: { [className]: { object // props @@ -643,38 +729,6 @@ WindowStore top: number // the top position of the popup window }, previewFrameKey: number, - locationInfo: { - [url]: { - publisher: string, // url of the publisher in question - verified: boolean, // wheter or not site is a verified publisher - exclude: boolean, // wheter or not site is in the excluded list - stickyP: boolean, // wheter or not site was added using addFunds urlbar toggle - timestamp: number // timestamp in milliseconds - } - }, - publisherInfo: { - synopsis: [{ - daysSpent: number, // e.g., 1 - duration: number, // total millisecond-views, e.g., 93784000 = 1 day, 2 hours, 3 minutes, 4 seconds - faviconURL: string, // i.e., "data:image/...;base64,..." - hoursSpent: number, // e.g., 2 - minutesSpent: number, // e.g., 3 - percentage: number, // i.e., 0, 1, ... 100 - pinPercentage: number, // i.e., 0, 1, ... 100 - publisherURL: string, // publisher site, e.g., "https://wikipedia.org/" - rank: number, // i.e., 1, 2, 3, ... - score: number, // float indicating the current score - secondsSpent: number, // e.g., 4 - site: string, // publisher name, e.g., "wikipedia.org" - verified: boolean, // there is a verified wallet for this publisher - views: number, // total page-views, - weight: number // float indication of the ration - }], // one entry for each publisher having a non-zero `score` - synopsisOptions: { - minPublisherDuration: number, // e.g., 8000 for 8 seconds - minPublisherVisits: number // e.g., 0 - } - }, searchResults: array, // autocomplete server results if enabled ui: { bookmarksToolbar: { diff --git a/js/actions/appActions.js b/js/actions/appActions.js index dc5d2ae4b09..eb9ae07f17e 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -536,17 +536,6 @@ const appActions = { }) }, - /** - * Updates publisher information for the payments pane - * @param {object} publisherInfo - the current publisher synopsis - */ - updatePublisherInfo: function (publisherInfo) { - dispatch({ - actionType: appConstants.APP_UPDATE_PUBLISHER_INFO, - publisherInfo - }) - }, - /** * Shows a message in the notification bar * @param {{message: string, buttons: Array., frameOrigin: string, options: Object}} detail @@ -1116,30 +1105,6 @@ const appActions = { }) }, - /** - - * Change all undefined publishers in site settings to defined sites - * also change all undefined ledgerPayments to value true - * @param publishers {Object} publishers from the synopsis - */ - enableUndefinedPublishers: function (publishers) { - dispatch({ - actionType: appConstants.APP_ENABLE_UNDEFINED_PUBLISHERS, - publishers - }) - }, - - /** - * Update ledger publishers pinned percentages according to the new synopsis - * @param publishers {Object} updated publishers - */ - changeLedgerPinnedPercentages: function (publishers) { - dispatch({ - actionType: appConstants.APP_CHANGE_LEDGER_PINNED_PERCENTAGES, - publishers - }) - }, - /** * Dispatches a message to change a the pinned status of a tab * @param {number} tabId - The tabId of the tab to pin diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 36e646766df..5a1e00366f7 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -33,9 +33,7 @@ const appConstants = { APP_ON_CLEAR_BROWSING_DATA: _, APP_IMPORT_BROWSER_DATA: _, APP_UPDATE_LEDGER_INFO: _, - APP_LEDGER_RECOVERY_STATUS_CHANGED: _, APP_UPDATE_LOCATION_INFO: _, - APP_UPDATE_PUBLISHER_INFO: _, APP_SHOW_NOTIFICATION: _, /** @param {Object} detail */ APP_HIDE_NOTIFICATION: _, /** @param {string} message */ APP_BACKUP_KEYS: _, @@ -111,8 +109,6 @@ const appConstants = { APP_URL_BAR_SUGGESTIONS_CHANGED: _, APP_SEARCH_SUGGESTION_RESULTS_AVAILABLE: _, APP_DEFAULT_SEARCH_ENGINE_LOADED: _, - APP_CHANGE_LEDGER_PINNED_PERCENTAGES: _, - APP_ENABLE_UNDEFINED_PUBLISHERS: _, APP_TAB_PINNED: _, APP_DRAG_STARTED: _, APP_DRAG_ENDED: _, diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 35f7fa77b42..7039fc699cf 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -10,7 +10,6 @@ const appDispatcher = require('../dispatcher/appDispatcher') const settings = require('../constants/settings') const {STATE_SITES} = require('../constants/stateConstants') const syncUtil = require('../state/syncUtil') -const siteSettings = require('../state/siteSettings') const electron = require('electron') const app = electron.app const messages = require('../constants/messages') @@ -202,7 +201,9 @@ const handleAppAction = (action) => { require('../../app/browser/reducers/topSitesReducer'), require('../../app/browser/reducers/braverySettingsReducer'), require('../../app/browser/reducers/bookmarkToolbarReducer'), - require('../../app/ledger').doAction, + require('../../app/browser/reducers/siteSettingsReducer'), + require('../../app/browser/reducers/pageDataReducer'), + require('../../app/browser/reducers/ledgerReducer'), require('../../app/browser/menu') ] initialized = true @@ -288,56 +289,6 @@ const handleAppAction = (action) => { appState = appState.setIn(['settings', action.key], action.value) appState = handleChangeSettingAction(appState, action.key, action.value) break - case appConstants.APP_ALLOW_FLASH_ONCE: - { - const propertyName = action.isPrivate ? 'temporarySiteSettings' : 'siteSettings' - appState = appState.set(propertyName, - siteSettings.mergeSiteSetting(appState.get(propertyName), urlUtil.getOrigin(action.url), 'flash', 1)) - break - } - case appConstants.APP_ALLOW_FLASH_ALWAYS: - { - const propertyName = action.isPrivate ? 'temporarySiteSettings' : 'siteSettings' - const expirationTime = Date.now() + (7 * 24 * 3600 * 1000) - appState = appState.set(propertyName, - siteSettings.mergeSiteSetting(appState.get(propertyName), urlUtil.getOrigin(action.url), 'flash', expirationTime)) - break - } - case appConstants.APP_CHANGE_SITE_SETTING: - { - let propertyName = action.temporary ? 'temporarySiteSettings' : 'siteSettings' - let newSiteSettings = siteSettings.mergeSiteSetting(appState.get(propertyName), action.hostPattern, action.key, action.value) - if (action.skipSync) { - newSiteSettings = newSiteSettings.setIn([action.hostPattern, 'skipSync'], true) - } - appState = appState.set(propertyName, newSiteSettings) - break - } - case appConstants.APP_REMOVE_SITE_SETTING: - { - let propertyName = action.temporary ? 'temporarySiteSettings' : 'siteSettings' - let newSiteSettings = siteSettings.removeSiteSetting(appState.get(propertyName), - action.hostPattern, action.key) - if (action.skipSync) { - newSiteSettings = newSiteSettings.setIn([action.hostPattern, 'skipSync'], true) - } - appState = appState.set(propertyName, newSiteSettings) - break - } - case appConstants.APP_CLEAR_SITE_SETTINGS: - { - let propertyName = action.temporary ? 'temporarySiteSettings' : 'siteSettings' - let newSiteSettings = new Immutable.Map() - appState.get(propertyName).map((entry, hostPattern) => { - let newEntry = entry.delete(action.key) - if (action.skipSync) { - newEntry = newEntry.set('skipSync', true) - } - newSiteSettings = newSiteSettings.set(hostPattern, newEntry) - }) - appState = appState.set(propertyName, newSiteSettings) - break - } case appConstants.APP_SET_SKIP_SYNC: { if (appState.getIn(action.path)) { @@ -345,30 +296,6 @@ const handleAppAction = (action) => { } break } - case appConstants.APP_ADD_NOSCRIPT_EXCEPTIONS: - { - const propertyName = action.temporary ? 'temporarySiteSettings' : 'siteSettings' - // Note that this is always cleared on restart or reload, so should not - // be synced or persisted. - const key = 'noScriptExceptions' - if (!action.origins || !action.origins.size) { - // Clear the exceptions - appState = appState.setIn([propertyName, action.hostPattern, key], new Immutable.Map()) - } else { - const currentExceptions = appState.getIn([propertyName, action.hostPattern, key]) || new Immutable.Map() - appState = appState.setIn([propertyName, action.hostPattern, key], currentExceptions.merge(action.origins)) - } - } - break - case appConstants.APP_UPDATE_LEDGER_INFO: - appState = appState.set('ledgerInfo', Immutable.fromJS(action.ledgerInfo)) - break - case appConstants.APP_UPDATE_LOCATION_INFO: - appState = appState.set('locationInfo', Immutable.fromJS(action.locationInfo)) - break - case appConstants.APP_UPDATE_PUBLISHER_INFO: - appState = appState.set('publisherInfo', Immutable.fromJS(action.publisherInfo)) - break case appConstants.APP_SHOW_NOTIFICATION: let notifications = appState.get('notifications') notifications = notifications.filterNot((notification) => { @@ -422,13 +349,6 @@ const handleAppAction = (action) => { } } break - case appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED: - { - const date = new Date().getTime() - appState = appState.setIn(['about', 'preferences', 'recoverySucceeded'], action.recoverySucceeded) - appState = appState.setIn(['about', 'preferences', 'updatedStamp'], date) - } - break case appConstants.APP_ON_CLEAR_BROWSING_DATA: const defaults = appState.get('clearBrowsingDataDefaults') const temp = appState.get('tempClearBrowsingData', Immutable.Map()) @@ -665,26 +585,6 @@ const handleAppAction = (action) => { case appConstants.APP_HIDE_DOWNLOAD_DELETE_CONFIRMATION: appState = appState.set('deleteConfirmationVisible', false) break - case appConstants.APP_ENABLE_UNDEFINED_PUBLISHERS: - const sitesObject = appState.get('siteSettings') - Object.keys(action.publishers).map((item) => { - const pattern = `https?://${item}` - const siteSetting = sitesObject.get(pattern) - const result = (siteSetting) && (siteSetting.get('ledgerPayments')) - - if (result === undefined) { - let newSiteSettings = siteSettings.mergeSiteSetting(appState.get('siteSettings'), pattern, 'ledgerPayments', true) - appState = appState.set('siteSettings', newSiteSettings) - } - }) - break - case appConstants.APP_CHANGE_LEDGER_PINNED_PERCENTAGES: - Object.keys(action.publishers).map((item) => { - const pattern = `https?://${item}` - let newSiteSettings = siteSettings.mergeSiteSetting(appState.get('siteSettings'), pattern, 'ledgerPinPercentage', action.publishers[item].pinPercentage) - appState = appState.set('siteSettings', newSiteSettings) - }) - break case appConstants.APP_DEFAULT_SEARCH_ENGINE_LOADED: appState = appState.set('searchDetail', action.searchDetail) break diff --git a/js/stores/eventStore.js b/js/stores/eventStore.js deleted file mode 100644 index c33068cff38..00000000000 --- a/js/stores/eventStore.js +++ /dev/null @@ -1,156 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -const appConstants = require('../constants/appConstants') -const appDispatcher = require('../dispatcher/appDispatcher') -const AppStore = require('./appStore') -const EventEmitter = require('events').EventEmitter -const Immutable = require('immutable') -const windowConstants = require('../constants/windowConstants') -const debounce = require('../lib/debounce') -const {getWebContents} = require('../../app/browser/webContentsCache') -const {isSourceAboutUrl} = require('../lib/appUrlUtil') -const {responseHasContent} = require('../../app/common/lib/httpUtil') - -const electron = require('electron') -const BrowserWindow = electron.BrowserWindow - -let eventState = Immutable.fromJS({ - page_load: [], - page_view: [], - page_info: [] -}) - -const CHANGE_EVENT = 'change' - -class EventStore extends EventEmitter { - getState () { - return eventState - } - - emitChanges () { - this.emit(CHANGE_EVENT) - } - - addChangeListener (callback) { - this.on(CHANGE_EVENT, callback) - } - - removeChangeListener (callback) { - this.removeListener(CHANGE_EVENT, callback) - } -} - -const eventStore = new EventStore() -const emitChanges = debounce(eventStore.emitChanges.bind(eventStore), 5) - -let lastActivePageUrl = null -let lastActiveTabId = null - -const addPageView = (url, tabId) => { - const tab = getWebContents(tabId) - const isPrivate = !tab || - tab.isDestroyed() || - !tab.session.partition.startsWith('persist:') - - if ((url && isSourceAboutUrl(url)) || isPrivate) { - url = null - } - - if (lastActivePageUrl === url) { - return - } - - let pageViewEvent = Immutable.fromJS({ - timestamp: new Date().getTime(), - url, - tabId - }) - eventState = eventState.set('page_view', eventState.get('page_view').slice(-100).push(pageViewEvent)) - lastActivePageUrl = url -} - -const windowBlurred = (windowId) => { - let windowCount = BrowserWindow.getAllWindows().filter((win) => win.isFocused()).length - if (windowCount === 0) { - addPageView(null, null) - } -} - -const windowClosed = (windowId) => { - let windowCount = BrowserWindow.getAllWindows().length - let win = BrowserWindow.getFocusedWindow() - // window may not be closed yet - if (windowCount > 0 && win && win.id === windowId) { - win.once('closed', () => { - windowClosed(windowId) - }) - } - - if (!win || windowCount === 0) { - addPageView(null, null) - } -} - -// Register callback to handle all updates -const doAction = (action) => { - switch (action.actionType) { - case windowConstants.WINDOW_SET_FOCUSED_FRAME: - if (action.location) { - addPageView(action.location, action.tabId) - } - break - case appConstants.APP_WINDOW_BLURRED: - windowBlurred(action.windowId) - break - case appConstants.APP_IDLE_STATE_CHANGED: - if (action.idleState !== 'active') { - addPageView(null, null) - } else { - addPageView(lastActivePageUrl, lastActiveTabId) - } - break - case appConstants.APP_CLOSE_WINDOW: - appDispatcher.waitFor([AppStore.dispatchToken], () => { - windowClosed(action.windowId) - }) - break - case 'event-set-page-info': - // retains all past pages, not really sure that's needed... [MTR] - eventState = eventState.set('page_info', eventState.get('page_info').slice(-100).push(action.pageInfo)) - break - case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: - // Only capture response for the page (not subresources, like images, JavaScript, etc) - if (action.details && action.details.resourceType === 'mainFrame') { - const pageUrl = action.details.newURL - - // create a page view event if this is a page load on the active tabId - if (!lastActiveTabId || action.tabId === lastActiveTabId) { - addPageView(pageUrl, action.tabId) - } - - const responseCode = action.details.httpResponseCode - if (isSourceAboutUrl(pageUrl) || !responseHasContent(responseCode)) { - break - } - - const pageLoadEvent = Immutable.fromJS({ - timestamp: new Date().getTime(), - url: pageUrl, - tabId: action.tabId, - details: action.details - }) - eventState = eventState.set('page_load', eventState.get('page_load').slice(-100).push(pageLoadEvent)) - } - break - default: - return - } - - emitChanges() -} - -appDispatcher.register(doAction) - -module.exports = eventStore diff --git a/test/unit/app/browser/reducers/pageDataReducerTest.js b/test/unit/app/browser/reducers/pageDataReducerTest.js new file mode 100644 index 00000000000..48bfd8e7c61 --- /dev/null +++ b/test/unit/app/browser/reducers/pageDataReducerTest.js @@ -0,0 +1,436 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* global describe, it, before, beforeEach, after, afterEach */ +const mockery = require('mockery') +const Immutable = require('immutable') +const assert = require('assert') +const sinon = require('sinon') + +const appConstants = require('../../../../../js/constants/appConstants') +const windowConstants = require('../../../../../js/constants/windowConstants') + +describe('pageDataReducer unit tests', function () { + let pageDataReducer, pageDataState, isFocused + + const state = Immutable.fromJS({ + pageData: { + view: {}, + load: [], + info: {}, + last: { + info: '', + tabId: null + } + } + }) + + before(function () { + this.clock = sinon.useFakeTimers() + this.clock.tick(0) + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', { + BrowserWindow: { + getAllWindows: function () { + return [{ + id: 1, + isFocused: () => isFocused + }] + } + } + }) + mockery.registerMock('../../browser/webContentsCache', { + getWebContents: (tabId) => { + if (tabId == null) return null + + return { + isDestroyed: () => false, + session: { + partition: 'persist:0' + } + } + } + }) + pageDataState = require('../../../../../app/common/state/pageDataState') + pageDataReducer = require('../../../../../app/browser/reducers/pageDataReducer') + }) + + beforeEach(function () { + isFocused = false + }) + + after(function () { + mockery.disable() + this.clock.restore() + }) + + describe('WINDOW_SET_FOCUSED_FRAME', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('null case', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_SET_FOCUSED_FRAME + }) + + assert.equal(spy.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_SET_FOCUSED_FRAME, + location: 'https://brave.com', + tabId: 1 + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('APP_WINDOW_BLURRED', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('there is one focused window', function () { + isFocused = true + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_WINDOW_BLURRED + }) + + assert.equal(spy.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('there is no focused windows', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_WINDOW_BLURRED + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: null, + tabId: null + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('APP_IDLE_STATE_CHANGED', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('null case', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_IDLE_STATE_CHANGED + }) + + assert.equal(spy.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('idleState is active', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_IDLE_STATE_CHANGED, + idleState: 'active' + }) + + assert.equal(spy.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('idleState is not active', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_IDLE_STATE_CHANGED, + idleState: 'nonactive' + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: null, + tabId: null + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('APP_WINDOW_CLOSED', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('data is ok', function () { + spy = sinon.spy(pageDataState, 'addView') + const result = pageDataReducer(state, { + actionType: appConstants.APP_WINDOW_CLOSED + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: null, + tabId: null + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('event-set-page-info', function () { + let spy + + afterEach(function () { + spy.restore() + }) + + it('data is ok', function () { + spy = sinon.spy(pageDataState, 'addInfo') + const result = pageDataReducer(state, { + actionType: 'event-set-page-info', + pageInfo: { + timestamp: 0, + url: 'https://brave.com' + } + }) + + const expectedState = state + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + .setIn(['pageData', 'info', 'https://brave.com/'], Immutable.fromJS({ + key: 'https://brave.com/', + timestamp: 0, + url: 'https://brave.com' + })) + + assert.equal(spy.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('WINDOW_GOT_RESPONSE_DETAILS', function () { + let spyView, spyActiveTab, spyLoad + + afterEach(function () { + spyView.restore() + spyActiveTab.restore() + spyLoad.restore() + }) + + it('null case', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS + }) + + assert.equal(spyView.notCalled, true) + assert.equal(spyActiveTab.notCalled, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('add view if we dont have last active tab', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + const result = pageDataReducer(state, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: { + resourceType: 'mainFrame', + newURL: 'https://brave.com' + }, + tabId: 1 + }) + + const expectedState = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + })) + + assert.equal(spyView.calledOnce, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + + it('add view if tabId is the same as last active tab', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + + const newState = state + .setIn(['pageData', 'last', 'tabId'], 1) + + const result = pageDataReducer(newState, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: { + resourceType: 'mainFrame', + newURL: 'https://brave.com' + }, + tabId: 1 + }) + + const expectedState = newState + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + })) + + assert.equal(spyView.calledOnce, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + + it('dont add view if tabId is different as last active tab', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + + const newState = state + .setIn(['pageData', 'last', 'tabId'], 2) + + const result = pageDataReducer(newState, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: { + resourceType: 'mainFrame', + newURL: 'https://brave.com' + }, + tabId: 1 + }) + + assert.equal(spyView.notCalled, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), newState.toJS()) + }) + + it('dont add load if response is not successful', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + + const newState = state + .setIn(['pageData', 'last', 'tabId'], 2) + + const result = pageDataReducer(newState, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: { + resourceType: 'mainFrame', + newURL: 'https://brave.com', + httpResponseCode: 500 + }, + tabId: 1 + }) + + assert.equal(spyView.notCalled, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), newState.toJS()) + }) + + it('dont add load if URL is about page', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + + const newState = state + .setIn(['pageData', 'last', 'tabId'], 2) + + const result = pageDataReducer(newState, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: { + resourceType: 'mainFrame', + newURL: 'about:history', + httpResponseCode: 200 + }, + tabId: 1 + }) + + assert.equal(spyView.notCalled, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.notCalled, true) + assert.deepEqual(result.toJS(), newState.toJS()) + }) + + it('add load', function () { + spyView = sinon.spy(pageDataState, 'addView') + spyActiveTab = sinon.spy(pageDataState, 'getLastActiveTabId') + spyLoad = sinon.spy(pageDataState, 'addLoad') + + const details = { + resourceType: 'mainFrame', + newURL: 'https://brave.com', + httpResponseCode: 200 + } + const newState = state + .setIn(['pageData', 'last', 'tabId'], 2) + + const result = pageDataReducer(newState, { + actionType: windowConstants.WINDOW_GOT_RESPONSE_DETAILS, + details: details, + tabId: 1 + }) + + const expectedState = newState + .setIn(['pageData', 'load'], Immutable.fromJS([{ + timestamp: 0, + url: 'https://brave.com', + tabId: 1, + details: details + }])) + + assert.equal(spyView.notCalled, true) + assert.equal(spyActiveTab.calledOnce, true) + assert.equal(spyLoad.calledOnce, true) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) +}) diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index df643fb74ba..9210b698665 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -1,15 +1,42 @@ /* global describe, it */ const ledgerUtil = require('../../../../../app/common/lib/ledgerUtil') const assert = require('assert') +const Immutable = require('immutable') require('../../../braveUnit') describe('ledgerUtil test', function () { describe('shouldTrackView', function () { - const validView = { tabId: 1, url: 'https://brave.com/' } - const validResponseList = [{ tabId: validView.tabId, details: { newURL: validView.url, httpResponseCode: 200 } }] - const noMatchResponseList = [{ tabId: 3, details: { newURL: 'https://not-brave.com' } }] - const matchButErrored = [{ tabId: validView.tabId, details: { newURL: validView.url, httpResponseCode: 404 } }] + const validView = Immutable.fromJS({ + tabId: 1, + url: 'https://brave.com/' + }) + const validResponseList = Immutable.fromJS([ + { + tabId: validView.get('tabId'), + details: { + newURL: validView.get('url'), + httpResponseCode: 200 + } + } + ]) + const noMatchResponseList = Immutable.fromJS([ + { + tabId: 3, + details: { + newURL: 'https://not-brave.com' + } + } + ]) + const matchButErrored = Immutable.fromJS([ + { + tabId: validView.get('tabId'), + details: { + newURL: validView.get('url'), + httpResponseCode: 404 + } + } + ]) describe('input validation', function () { it('returns false if view is falsey', function () { diff --git a/test/unit/app/common/lib/pageDataUtilTest.js b/test/unit/app/common/lib/pageDataUtilTest.js new file mode 100644 index 00000000000..70744ec882d --- /dev/null +++ b/test/unit/app/common/lib/pageDataUtilTest.js @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global describe, it */ +const pageDataUtil = require('../../../../../app/common/lib/pageDataUtil') +const assert = require('assert') + +require('../../../braveUnit') + +describe('pageDataUtil unit tests', () => { + describe('getInfoKey', () => { + it('null case', () => { + const result = pageDataUtil.getInfoKey() + assert.equal(result, null) + }) + + it('url is converted to location', () => { + const result = pageDataUtil.getInfoKey('https://brave.com') + assert.equal(result, 'https://brave.com/') + }) + }) +}) diff --git a/test/unit/app/common/state/pageDataStateTest.js b/test/unit/app/common/state/pageDataStateTest.js new file mode 100644 index 00000000000..3ef0a42d0e5 --- /dev/null +++ b/test/unit/app/common/state/pageDataStateTest.js @@ -0,0 +1,304 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global describe, it, before, beforeEach, after */ +const Immutable = require('immutable') +const assert = require('assert') +const sinon = require('sinon') +const mockery = require('mockery') + +describe('pageDataState unit tests', function () { + let pageDataState, isPrivate, clock, now + + const state = Immutable.fromJS({ + pageData: { + view: {}, + load: [], + info: {}, + last: { + info: '', + tabId: null + } + } + }) + + const stateWithData = Immutable.fromJS({ + pageData: { + view: { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + }, + load: [ + { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + } + ], + info: { + 'https://brave.com/': { + timestamp: 0, + url: 'https://brave.com', + tabId: 1 + } + }, + last: { + info: '', + tabId: 1 + } + } + }) + + before(function () { + clock = sinon.useFakeTimers() + now = new Date(0) + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('../../browser/webContentsCache', { + getWebContents: (tabId) => { + if (tabId == null) return null + + return { + isDestroyed: () => false, + session: { + partition: isPrivate ? '' : 'persist:0' + } + } + } + }) + pageDataState = require('../../../../../app/common/state/pageDataState') + }) + + beforeEach(function () { + isPrivate = false + }) + + after(function () { + mockery.disable() + clock.restore() + }) + + describe('addView', function () { + it('null case', function () { + const result = pageDataState.addView(state) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: null + })) + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is the same as last one', function () { + const newState = state + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + })) + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = newState + .setIn(['pageData', 'last', 'tabId'], 1) + + assert.deepEqual(result, expectedResult) + }) + + it('url is private', function () { + isPrivate = true + + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is about page', function () { + const result = pageDataState.addView(state, 'about:history', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: null, + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + + it('url is ok', function () { + const result = pageDataState.addView(state, 'https://brave.com', 1) + const expectedResult = state + .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'view'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + })) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('addInfo', function () { + it('null case', function () { + const result = pageDataState.addInfo(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const data = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com' + }) + + const result = pageDataState.addInfo(state, data) + const expectedResult = state + .setIn(['pageData', 'info', 'https://brave.com/'], data.set('key', 'https://brave.com/')) + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('addLoad', function () { + it('null case', function () { + const result = pageDataState.addLoad(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const result = pageDataState.addLoad(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('we only take last 100 views', function () { + let newState = state + + for (let i = 0; i < 100; i++) { + const data = Immutable.fromJS([{ + timestamp: now.getTime(), + url: `https://page${i}.com`, + tabId: 1 + }]) + newState = newState.setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).push(data)) + } + + const newLoad = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + }) + + const result = pageDataState.addLoad(newState, newLoad) + const expectedResult = newState + .setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).shift()) + .setIn(['pageData', 'load'], newState.getIn(['pageData', 'load']).push(newLoad)) + + assert.deepEqual(result.toJS(), expectedResult.toJS()) + }) + }) + + describe('getView', function () { + it('null case', function () { + const result = pageDataState.getView(state) + assert.deepEqual(result, Immutable.Map()) + }) + + it('data is ok', function () { + const result = pageDataState.getView(stateWithData) + assert.deepEqual(result.toJS(), stateWithData.getIn(['pageData', 'view']).toJS()) + }) + }) + + describe('getLastInfo', function () { + it('null case', function () { + const result = pageDataState.getLastInfo(state) + assert.deepEqual(result, Immutable.Map()) + }) + + it('key is provided, but data is not there', function () { + const newState = state + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + .setIn(['pageData', 'info', 'https://test.com/'], Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://test.com', + tabId: 1 + })) + + const result = pageDataState.getLastInfo(newState) + assert.deepEqual(result, Immutable.Map()) + }) + + it('key is provided and data is there', function () { + const info = Immutable.fromJS({ + timestamp: now.getTime(), + url: 'https://brave.com', + tabId: 1 + }) + + const newState = state + .setIn(['pageData', 'last', 'info'], 'https://brave.com/') + .setIn(['pageData', 'info', 'https://brave.com/'], info) + + const result = pageDataState.getLastInfo(newState) + assert.deepEqual(result.toJS(), info.toJS()) + }) + }) + + describe('getLoad', function () { + it('null case', function () { + const result = pageDataState.getLoad(state) + assert.deepEqual(result, Immutable.List()) + }) + + it('data is there', function () { + const result = pageDataState.getLoad(stateWithData) + assert.deepEqual(result.toJS(), stateWithData.getIn(['pageData', 'load']).toJS()) + }) + }) + + describe('getLastActiveTabId', function () { + it('null case', function () { + const result = pageDataState.getLastActiveTabId(state) + assert.deepEqual(result, null) + }) + + it('data is there', function () { + const result = pageDataState.getLastActiveTabId(stateWithData) + assert.deepEqual(result, 1) + }) + }) + + describe('setLastActiveTabId', function () { + it('id is saved', function () { + const result = pageDataState.setLastActiveTabId(state, 10) + const expectedState = state.setIn(['pageData', 'last', 'tabId'], 10) + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) + + describe('setPublisher', function () { + it('null case', function () { + const result = pageDataState.setPublisher(state) + assert.deepEqual(result.toJS(), state.toJS()) + }) + + it('data is ok', function () { + const result = pageDataState.setPublisher(stateWithData, 'https://brave.com/', 'https://brave.com') + const expectedState = stateWithData.setIn(['pageData', 'info', 'https://brave.com/', 'publisher'], 'https://brave.com') + assert.deepEqual(result.toJS(), expectedState.toJS()) + }) + }) +}) From 2ec3ea8bc91db0b65f38a467cf6963a708b115a2 Mon Sep 17 00:00:00 2001 From: Brian Clifton Date: Thu, 21 Sep 2017 23:54:09 -0700 Subject: [PATCH 2/6] Add / update unit tests - siteSettingsReducerTest (should be 100% coverage now) - add more tests for ledgerUtil (btcToCurrencyString, etc) Auditors: @NejcZdovc --- app/common/lib/ledgerUtil.js | 4 +- .../reducers/siteSettingsReducerTest.js | 296 ++++++++++++++++++ test/unit/app/common/lib/ledgerUtilTest.js | 119 ++++++- 3 files changed, 414 insertions(+), 5 deletions(-) create mode 100644 test/unit/app/browser/reducers/siteSettingsReducerTest.js diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 89625ae8b92..cf19c28d732 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -142,13 +142,13 @@ const shouldTrackView = (view, responseList) => { const btcToCurrencyString = (btc, ledgerData) => { const balance = Number(btc || 0) - const currency = ledgerData.get('currency') || 'USD' + const currency = (ledgerData && ledgerData.get('currency')) || 'USD' if (balance === 0) { return `0 ${currency}` } - if (ledgerData.get('btc') && typeof ledgerData.get('amount') === 'number') { + if (ledgerData && ledgerData.get('btc') && typeof ledgerData.get('amount') === 'number') { const btcValue = ledgerData.get('btc') / ledgerData.get('amount') const fiatValue = (balance / btcValue).toFixed(2) let roundedValue = Math.floor(fiatValue) diff --git a/test/unit/app/browser/reducers/siteSettingsReducerTest.js b/test/unit/app/browser/reducers/siteSettingsReducerTest.js new file mode 100644 index 00000000000..a590810e991 --- /dev/null +++ b/test/unit/app/browser/reducers/siteSettingsReducerTest.js @@ -0,0 +1,296 @@ +/* global describe, it, before, beforeEach, after */ +const mockery = require('mockery') +const sinon = require('sinon') +const Immutable = require('immutable') +const assert = require('assert') +const fakeElectron = require('../../../lib/fakeElectron') + +const appConstants = require('../../../../../js/constants/appConstants') +require('../../../braveUnit') + +describe('siteSettingsReducer unit tests', function () { + let siteSettingsReducer + let siteSettings + let clock + let fakeAppState + let mergeSiteSettingSpy + let removeSiteSettingSpy + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + clock = sinon.useFakeTimers() + fakeAppState = Immutable.fromJS({ + siteSettings: { + 'https://example.com': { + regularTabSetting: '' + } + }, + temporarySiteSettings: { + 'https://example.com': { + privateTabSetting: '' + } + } + }) + + mockery.registerMock('electron', fakeElectron) + siteSettings = require('../../../../../js/state/siteSettings') + mockery.registerMock('../../../js/state/siteSettings', siteSettings) + + mergeSiteSettingSpy = sinon.spy(siteSettings, 'mergeSiteSetting') + removeSiteSettingSpy = sinon.spy(siteSettings, 'removeSiteSetting') + + siteSettingsReducer = require('../../../../../app/browser/reducers/siteSettingsReducer') + }) + + after(function () { + clock.restore() + mergeSiteSettingSpy.restore() + mockery.disable() + }) + + describe('APP_ALLOW_FLASH_ONCE', function () { + beforeEach(function () { + mergeSiteSettingSpy.reset() + }) + it('merges setting into siteSettings if regular tab', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ALLOW_FLASH_ONCE, + isPrivate: false, + url: 'https://example.com/test/page.html' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('siteSettings'), + 'https://example.com', + 'flash', + 1).calledOnce) + }) + it('merges setting into temporarySiteSettings if private tab', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ALLOW_FLASH_ONCE, + isPrivate: true, + url: 'https://example.com/test/page.html' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('temporarySiteSettings'), + 'https://example.com', + 'flash', + 1).calledOnce) + }) + }) + + describe('APP_ALLOW_FLASH_ALWAYS', function () { + beforeEach(function () { + mergeSiteSettingSpy.reset() + }) + it('merges setting into siteSettings if regular tab', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ALLOW_FLASH_ALWAYS, + isPrivate: false, + url: 'https://example.com/test/page.html' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('siteSettings'), + 'https://example.com', + 'flash', + 604800000).calledOnce) + }) + it('merges setting into temporarySiteSettings if private tab', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ALLOW_FLASH_ALWAYS, + isPrivate: true, + url: 'https://example.com/test/page.html' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('temporarySiteSettings'), + 'https://example.com', + 'flash', + 604800000).calledOnce) + }) + }) + + describe('APP_CHANGE_SITE_SETTING', function () { + beforeEach(function () { + mergeSiteSettingSpy.reset() + }) + it('merges setting into siteSettings', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CHANGE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + value: 'keyValueHere' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('siteSettings'), + 'https://example.com', + 'keyNameHere', + 'keyValueHere').calledOnce) + }) + it('merges setting into temporarySiteSettings if temporary', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CHANGE_SITE_SETTING, + temporary: true, + hostPattern: 'https://example.com', + key: 'keyNameHere', + value: 'keyValueHere' + })) + assert(mergeSiteSettingSpy.withArgs( + fakeAppState.get('temporarySiteSettings'), + 'https://example.com', + 'keyNameHere', + 'keyValueHere').calledOnce) + }) + + describe('with skipSync', function () { + it('sets skipSync when action includes it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CHANGE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + value: 'keyValueHere', + skipSync: true + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), true) + }) + it('does not set skipSync when action does not include it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CHANGE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + value: 'keyValueHere' + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), undefined) + }) + }) + }) + + describe('APP_REMOVE_SITE_SETTING', function () { + beforeEach(function () { + removeSiteSettingSpy.reset() + }) + it('merges setting into siteSettings', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_REMOVE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere' + })) + assert(removeSiteSettingSpy.withArgs( + fakeAppState.get('siteSettings'), + 'https://example.com', + 'keyNameHere').calledOnce) + }) + it('merges setting into temporarySiteSettings if temporary', function () { + siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_REMOVE_SITE_SETTING, + temporary: true, + hostPattern: 'https://example.com', + key: 'keyNameHere' + })) + assert(removeSiteSettingSpy.withArgs( + fakeAppState.get('temporarySiteSettings'), + 'https://example.com', + 'keyNameHere').calledOnce) + }) + describe('with skipSync', function () { + it('sets skipSync when action includes it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_REMOVE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + skipSync: true + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), true) + }) + it('does not set skipSync when action does not include it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_REMOVE_SITE_SETTING, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere' + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), undefined) + }) + }) + }) + + describe('APP_CLEAR_SITE_SETTINGS', function () { + it('removes the site from siteSettings', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CLEAR_SITE_SETTINGS, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + skipSync: true + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), true) + }) + it('removes the site from temporarySiteSettings if temporary', function () { + const beforeState = fakeAppState.setIn(['temporarySiteSettings'], Immutable.fromJS({ + 'https://example.com': { + keyNameHere: 'keyValueHere' + } + })) + const afterState = siteSettingsReducer(beforeState, Immutable.fromJS({ + actionType: appConstants.APP_CLEAR_SITE_SETTINGS, + temporary: true, + hostPattern: 'https://example.com', + key: 'keyNameHere' + })) + assert.equal(afterState.getIn(['siteSettings', 'https://example.com', 'keyNameHere']), undefined) + }) + + describe('with skipSync', function () { + it('sets skipSync when action includes it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CLEAR_SITE_SETTINGS, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere', + skipSync: true + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), true) + }) + it('does not set skipSync when action does not include it', function () { + const newState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_CLEAR_SITE_SETTINGS, + temporary: false, + hostPattern: 'https://example.com', + key: 'keyNameHere' + })) + assert.equal(newState.getIn(['siteSettings', 'https://example.com', 'skipSync']), undefined) + }) + }) + }) + + describe('APP_ADD_NOSCRIPT_EXCEPTIONS', function () { + it('adds the `noScriptExceptions` entry to siteSettings', function () { + const afterState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ADD_NOSCRIPT_EXCEPTIONS, + temporary: false, + hostPattern: 'https://example.com', + origins: [{'https://example.com': {}}], + key: 'keyNameHere' + })) + assert(afterState.getIn(['siteSettings', 'https://example.com', 'noScriptExceptions'])) + }) + it('adds the `noScriptExceptions` entry to temporarySiteSettings if temporary', function () { + const afterState = siteSettingsReducer(fakeAppState, Immutable.fromJS({ + actionType: appConstants.APP_ADD_NOSCRIPT_EXCEPTIONS, + temporary: true, + hostPattern: 'https://example.com', + origins: null, + key: 'keyNameHere' + })) + assert(afterState.getIn(['temporarySiteSettings', 'https://example.com', 'noScriptExceptions'])) + }) + }) +}) diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index 9210b698665..0920732c73f 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -1,11 +1,36 @@ -/* global describe, it */ -const ledgerUtil = require('../../../../../app/common/lib/ledgerUtil') +/* global describe, before, after, it */ +const mockery = require('mockery') const assert = require('assert') const Immutable = require('immutable') - require('../../../braveUnit') describe('ledgerUtil test', function () { + let ledgerUtil + let fakeLevel + const fakeElectron = require('../../../lib/fakeElectron') + const fakeAdBlock = require('../../../lib/fakeAdBlock') + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + fakeLevel = () => { + } + + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('ad-block', fakeAdBlock) + mockery.registerMock('level', fakeLevel) + + ledgerUtil = require('../../../../../app/common/lib/ledgerUtil') + }) + + after(function () { + mockery.disable() + }) + describe('shouldTrackView', function () { const validView = Immutable.fromJS({ tabId: 1, @@ -71,4 +96,92 @@ describe('ledgerUtil test', function () { }) }) }) + + describe('btcToCurrencyString', function () { + let ledgerData + + before(function () { + ledgerData = Immutable.fromJS({ + paymentId: 'f5240e31-7df6-466d-9606-adc759298731', + countryCode: 'US', + unconfirmed: '0.0000', + hasBitcoinHandler: false, + bravery: { + setting: 'adFree', + days: 30, + fee: { + currency: 'USD', + amount: 10 + } + }, + error: null, + created: true, + satoshis: 2097027, + buyURL: undefined, + paymentURL: 'bitcoin:btc-address-goes-here?amount=0.00277334&label=Brave%20Software', + passphrase: 'd588b7e3-352d-49ce-8d0f-a4cae1fa4c76', + buyMaximumUSD: 6, + reconcileFrequency: 30, + currency: 'USD', + btc: '0.00277334', + address: 'btc-address-goes-here', + reconcileStamp: 1405324210587, + transactions: [], + amount: 10, + creating: false, + balance: 0.0210, + paymentIMG: undefined + }) + }) + + it('defaults to 0 as balance and "USD" as currency symbol', function () { + const result = ledgerUtil.btcToCurrencyString() + assert.equal(result, '0 USD') + }) + it('will mark currency with different symbol (if present)', function () { + const result = ledgerUtil.btcToCurrencyString(0, ledgerData.set('currency', 'Sealand dollars')) + assert.equal(result, '0 Sealand dollars') + }) + it('will convert value to USD', function () { + const result = ledgerUtil.btcToCurrencyString(1, ledgerData) + assert.equal(result, '3605.75 USD') + }) + describe('when rounding values', function () { + it('will round 4.97 down to 4.75 (cent values greater than .74)', function () { + const ledgerDataCopy = ledgerData.set('btc', '0.00279000') + const result = ledgerUtil.btcToCurrencyString(0.00138667, ledgerDataCopy) + assert.equal(result, '4.75 USD') + }) + it('will round 4.64 down to 4.50 (cent values greater than .49)', function () { + const ledgerDataCopy = ledgerData.set('btc', '0.00299000') + const result = ledgerUtil.btcToCurrencyString(0.00138667, ledgerDataCopy) + assert.equal(result, '4.50 USD') + }) + it('will round 4.33 down to 4.25 (cent values greater than .24)', function () { + const ledgerDataCopy = ledgerData.set('btc', '0.00320000') + const result = ledgerUtil.btcToCurrencyString(0.00138667, ledgerDataCopy) + assert.equal(result, '4.25 USD') + }) + it('will round 4.08 down to 4.00 (cent values less than .24)', function () { + const ledgerDataCopy = ledgerData.set('btc', '0.00340000') + const result = ledgerUtil.btcToCurrencyString(0.00138667, ledgerDataCopy) + assert.equal(result, '4.00 USD') + }) + }) + describe('when ledgerData does not contain exchange information', function () { + it('returns the raw balance formatted as BTC', function () { + const result = ledgerUtil.btcToCurrencyString(0.00138667, undefined) + assert.equal(result, '0.00138667 BTC') + }) + }) + }) + + describe('formattedTimeFromNow', function () { + }) + + describe('formattedDateFromTimestamp', function () { + }) + + describe('walletStatus', function () { + }) }) From d5bbac254b0b115fcd72044c26a0f11c4b2b49c0 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Sat, 23 Sep 2017 12:21:19 +0200 Subject: [PATCH 3/6] Moving ledger into appState part 2 Part of #11037 PR --- app/browser/api/ledger.js | 2187 +++++++++++++++++ app/browser/reducers/ledgerReducer.js | 180 +- app/browser/reducers/pageDataReducer.js | 5 +- app/browser/tabs.js | 21 +- app/common/lib/ledgerUtil.js | 1799 +------------- app/common/lib/publisherUtil.js | 47 +- app/common/state/ledgerState.js | 295 ++- app/common/state/pageDataState.js | 25 +- app/common/state/siteSettingsState.js | 4 +- .../brave/locales/en-US/app.properties | 3 +- app/ledger.js | 520 ---- app/pdf.js | 23 +- .../components/navigation/navigationBar.js | 3 +- .../components/navigation/publisherToggle.js | 40 +- app/renderer/components/navigation/urlBar.js | 3 +- .../preferences/payment/enabledContent.js | 6 +- .../preferences/payment/ledgerRecovery.js | 3 +- .../preferences/payment/ledgerTable.js | 3 +- app/sessionStore.js | 12 + docs/state.md | 2 - js/about/aboutActions.js | 14 - js/about/preferences.js | 4 +- js/actions/appActions.js | 167 +- js/constants/appConstants.js | 22 +- js/constants/messages.js | 3 - test/about/ledgerTableTest.js | 6 +- test/lib/brave.js | 3 +- .../browser/reducers/pageDataReducerTest.js | 6 + test/unit/app/common/lib/publisherUtilTest.js | 62 - .../app/common/state/pageDataStateTest.js | 8 +- .../navigation/navigationBarTest.js | 22 +- .../navigation/publisherToggleTest.js | 52 +- 32 files changed, 2929 insertions(+), 2621 deletions(-) create mode 100644 app/browser/api/ledger.js delete mode 100644 app/ledger.js diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js new file mode 100644 index 00000000000..a25ad3b9038 --- /dev/null +++ b/app/browser/api/ledger.js @@ -0,0 +1,2187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const acorn = require('acorn') +const moment = require('moment') +const Immutable = require('immutable') +const electron = require('electron') +const ipc = electron.ipcMain +const path = require('path') +const os = require('os') +const qr = require('qr-image') +const underscore = require('underscore') +const tldjs = require('tldjs') +const urlFormat = require('url').format +const queryString = require('queryString') +const levelUp = require('level') +const random = require('random-lib') +const uuid = require('uuid') + +// Actions +const appActions = require('../../../js/actions/appActions') + +// State +const ledgerState = require('../../common/state/ledgerState') +const pageDataState = require('../../common/state/pageDataState') + +// Constants +const settings = require('../../../js/constants/settings') +const messages = require('../../../js/constants/messages') + +// Utils +const tabs = require('../../browser/tabs') +const locale = require('../../locale') +const appConfig = require('../../../js/constants/appConfig') +const getSetting = require('../../../js/settings').getSetting +const {fileUrl, getSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const urlParse = require('../../common/urlParse') +const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation') +const request = require('../../../js/lib/request') +const ledgerUtil = require('../../common/lib/ledgerUtil') + +// Caching +let locationDefault = 'NOOP' +let currentUrl = locationDefault +let currentTimestamp = new Date().getTime() +let visitsByPublisher = {} +let bootP +let quitP +let notificationPaymentDoneMessage +const _internal = { + verboseP: true, + debugP: true, + ruleset: { + raw: [], + cooked: [] + } +} + +// Libraries +let ledgerPublisher +let ledgerClient +let client +let synopsis +let ledgerBalance + +// Timers +let balanceTimeoutId = false +let notificationTimeout +let runTimeoutId + +// Database +let v2RulesetDB +const v2RulesetPath = 'ledger-rulesV2.leveldb' +let v2PublishersDB +const v2PublishersPath = 'ledger-publishersV2.leveldb' +const statePath = 'ledger-state.json' + +// Definitions +const miliseconds = { + year: 365 * 24 * 60 * 60 * 1000, + week: 7 * 24 * 60 * 60 * 1000, + day: 24 * 60 * 60 * 1000, + hour: 60 * 60 * 1000, + minute: 60 * 1000, + second: 1000 +} +const clientOptions = { + debugP: process.env.LEDGER_DEBUG, + loggingP: process.env.LEDGER_LOGGING, + rulesTestP: process.env.LEDGER_RULES_TESTING, + verboseP: process.env.LEDGER_VERBOSE, + server: process.env.LEDGER_SERVER_URL, + createWorker: electron.app.createWorker +} +const fileTypes = { + bmp: new Buffer([0x42, 0x4d]), + gif: new Buffer([0x47, 0x49, 0x46, 0x38, [0x37, 0x39], 0x61]), + ico: new Buffer([0x00, 0x00, 0x01, 0x00]), + jpeg: new Buffer([0xff, 0xd8, 0xff]), + png: new Buffer([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) +} + +let signatureMax = 0 +underscore.keys(fileTypes).forEach((fileType) => { + if (signatureMax < fileTypes[fileType].length) signatureMax = fileTypes[fileType].length +}) +signatureMax = Math.ceil(signatureMax * 1.5) + +// TODO is it ok to have IPC here or is there better place +if (ipc) { + ipc.on(messages.LEDGER_PUBLISHER, (event, location) => { + if (!synopsis || event.sender.session === electron.session.fromPartition('default') || !tldjs.isValid(location)) { + event.returnValue = {} + return + } + + let ctx = urlParse(location, true) + ctx.TLD = tldjs.getPublicSuffix(ctx.host) + if (!ctx.TLD) { + if (_internal.verboseP) console.log('\nno TLD for:' + ctx.host) + event.returnValue = {} + return + } + + ctx = underscore.mapObject(ctx, function (value) { + if (!underscore.isFunction(value)) return value + }) + ctx.URL = location + ctx.SLD = tldjs.getDomain(ctx.host) + ctx.RLD = tldjs.getSubdomain(ctx.host) + ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' + + if (!event.sender.isDestroyed()) { + event.sender.send(messages.LEDGER_PUBLISHER_RESPONSE + '-' + location, { + context: ctx, + rules: _internal.ruleset.cooked + }) + } + }) + + ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => { + const win = electron.BrowserWindow.getActiveWindow() + if (message === locale.translation('addFundsNotification')) { + appActions.hideNotification(message) + // See showNotificationAddFunds() for buttons. + // buttonIndex === 1 is "Later"; the timestamp until which to delay is set + // in showNotificationAddFunds() when triggering this notification. + if (buttonIndex === 0) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } else if (buttonIndex === 2 && win) { + // Add funds: Open payments panel + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: win.id + }) + } + } else if (message === locale.translation('reconciliationNotification')) { + appActions.hideNotification(message) + // buttonIndex === 1 is Dismiss + if (buttonIndex === 0) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } else if (buttonIndex === 2 && win) { + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: win.id + }) + } + } else if (message === notificationPaymentDoneMessage) { + appActions.hideNotification(message) + if (buttonIndex === 0) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } + } else if (message === locale.translation('notificationTryPayments')) { + appActions.hideNotification(message) + if (buttonIndex === 1 && win) { + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: win.id + }) + } + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) + } + }) +} + +let ledgerPaymentsPresent = {} +const paymentPresent = (state, tabId, present) => { + if (present) { + ledgerPaymentsPresent[tabId] = present + } else { + delete ledgerPaymentsPresent[tabId] + } + + if (Object.keys(ledgerPaymentsPresent).length > 0 && getSetting(settings.PAYMENTS_ENABLED)) { + if (!balanceTimeoutId) { + getBalance(state) + } + } else if (balanceTimeoutId) { + clearTimeout(balanceTimeoutId) + balanceTimeoutId = false + } +} + +const addFoundClosed = (state) => { + if (balanceTimeoutId) { + clearTimeout(balanceTimeoutId) + } + const balanceFn = getBalance.bind(null, state) + balanceTimeoutId = setTimeout(balanceFn, 5 * miliseconds.second) +} + +const boot = () => { + if (bootP || client) { + return + } + + bootP = true + const fs = require('fs') + fs.access(pathName(statePath), fs.FF_OK, (err) => { + if (!err) return + + if (err.code !== 'ENOENT') console.error('statePath read error: ' + err.toString()) + + appActions.onBootStateFile() + }) +} + +const onBootStateFile = (state) => { + state = ledgerState.setInfoProp(state, 'creating', true) + + try { + clientprep() + client = ledgerClient(null, underscore.extend({roundtrip: roundtrip}, clientOptions), null) + } catch (ex) { + state = ledgerState.resetInfo(state) + bootP = false + console.error('ledger client boot error: ', ex) + return state + } + + if (client.sync(callback) === true) { + run(random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute})) + } + + getBalance(state) + + bootP = false + + return state +} + +const promptForRecoveryKeyFile = () => { + const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + let files + if (process.env.SPECTRON) { + // skip the dialog for tests + console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) + files = [defaultRecoveryKeyFilePath] + } else { + const dialog = electron.dialog + files = dialog.showOpenDialog({ + properties: ['openFile'], + defaultPath: defaultRecoveryKeyFilePath, + filters: [{ + name: 'TXT files', + extensions: ['txt'] + }] + }) + } + + return (files && files.length ? files[0] : null) +} + +const logError = (state, err, caller) => { + if (err) { + console.error('Error in %j: %j', caller, err) + state = ledgerState.setLedgerError(state, err, caller) + } else { + state = ledgerState.setLedgerError(state) + } + + return state +} + +const loadKeysFromBackupFile = (state, filePath) => { + let keys = null + const fs = require('fs') + let data = fs.readFileSync(filePath) + + if (!data || !data.length || !(data.toString())) { + state = logError(state, 'No data in backup file', 'recoveryWallet') + } else { + try { + const recoveryFileContents = data.toString() + + let messageLines = recoveryFileContents.split(os.EOL) + + let paymentIdLine = '' || messageLines[3] + let passphraseLine = '' || messageLines[4] + + const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' ')) + const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] + + const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' ')) + const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] + + keys = { + paymentId, + passphrase + } + } catch (exc) { + state = logError(state, exc, 'recoveryWallet') + } + } + + return { + state, + keys + } +} + +const getPublisherData = (result, scorekeeper) => { + let duration = result.duration + + let data = { + verified: result.options.verified || false, + site: result.publisherKey, + views: result.visits, + duration: duration, + daysSpent: 0, + hoursSpent: 0, + minutesSpent: 0, + secondsSpent: 0, + faviconURL: result.faviconURL, + score: result.scores[scorekeeper], + pinPercentage: result.pinPercentage, + weight: result.pinPercentage + } + // HACK: Protocol is sometimes blank here, so default to http:// so we can + // still generate publisherURL. + data.publisherURL = (result.protocol || 'http:') + '//' + result.publisherKey + + if (duration >= miliseconds.day) { + data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1) + } else if (duration >= miliseconds.hour) { + data.hoursSpent = Math.max(Math.floor(duration / miliseconds.hour), 1) + data.minutesSpent = Math.round((duration % miliseconds.hour) / miliseconds.minute) + } else if (duration >= miliseconds.minute) { + data.minutesSpent = Math.max(Math.round(duration / miliseconds.minute), 1) + data.secondsSpent = Math.round((duration % miliseconds.minute) / miliseconds.second) + } else { + data.secondsSpent = Math.max(Math.round(duration / miliseconds.second), 1) + } + + return data +} + +const normalizePinned = (dataPinned, total, target, setOne) => dataPinned.map((publisher) => { + let newPer + let floatNumber + + if (setOne) { + newPer = 1 + floatNumber = 1 + } else { + floatNumber = (publisher.pinPercentage / total) * target + newPer = Math.floor(floatNumber) + if (newPer < 1) { + newPer = 1 + } + } + + publisher.weight = floatNumber + publisher.pinPercentage = newPer + return publisher +}) + +// courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 +const roundToTarget = (l, target, property) => { + let off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x[property]) }, 0) + + return underscore.sortBy(l, (x) => Math.round(x[property]) - x[property]) + .map((x, i) => { + x[property] = Math.round(x[property]) + (off > i) - (i >= (l.length + off)) + return x + }) +} + +// TODO we should convert this function and all related ones into immutable +// TODO merge publishers and publisherData that is created in getPublisherData +// so that we don't need to create new Map every single time +const synopsisNormalizer = (state, changedPublisher) => { + let dataPinned = [] // change to list + let dataUnPinned = [] // change to list + let dataExcluded = [] // change to list + const scorekeeper = ledgerState.getSynopsisOption(state, 'scorekeeper') + + let results = [] + let publishers = ledgerState.getPublishers(state) + for (let item of publishers) { + const publisherKey = item[0] + let publisher = item[1] + if (!ledgerUtil.visibleP(state, publisherKey)) { + continue + } + + publisher = publisher.set('publisherKey', publisherKey) + results.push(publisher.toJS()) + } + + if (results.length === 0) { + return ledgerState.saveAboutSynopsis(state, Immutable.List()) + } + + results = underscore.sortBy(results, (entry) => -entry.scores[scorekeeper]) + + let pinnedTotal = 0 + let unPinnedTotal = 0 + // move publisher to the correct array and get totals + results.forEach((result) => { + if (result.pinPercentage && result.pinPercentage > 0) { + // pinned + pinnedTotal += result.pinPercentage + dataPinned.push(getPublisherData(result, scorekeeper)) + } else if (ledgerUtil.stickyP(state, result.publisherKey)) { + // unpinned + unPinnedTotal += result.scores[scorekeeper] + dataUnPinned.push(result) + } else { + // excluded + let publisher = getPublisherData(result, scorekeeper) + publisher.percentage = 0 + publisher.weight = 0 + dataExcluded.push(publisher) + } + }) + + // round if over 100% of pinned publishers + if (pinnedTotal > 100) { + if (changedPublisher) { + let changedObject = dataPinned.filter(publisher => publisher.site === changedPublisher)[0] // TOOD optimize to find from filter + const setOne = changedObject.pinPercentage > (100 - dataPinned.length - 1) + + if (setOne) { + changedObject.pinPercentage = 100 - dataPinned.length + 1 + changedObject.weight = changedObject.pinPercentage + } + + const pinnedRestTotal = pinnedTotal - changedObject.pinPercentage + dataPinned = dataPinned.filter(publisher => publisher.site !== changedPublisher) + dataPinned = normalizePinned(dataPinned, pinnedRestTotal, (100 - changedObject.pinPercentage), setOne) + dataPinned = roundToTarget(dataPinned, (100 - changedObject.pinPercentage), 'pinPercentage') + + dataPinned.push(changedObject) + } else { + dataPinned = normalizePinned(dataPinned, pinnedTotal, 100) + dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') + } + + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result, scorekeeper) + publisher.percentage = 0 + publisher.weight = 0 + return publisher + }) + + // sync app store + state = ledgerState.changePinnedValues(state, dataPinned) + } else if (dataUnPinned.length === 0 && pinnedTotal < 100) { + // when you don't have any unpinned sites and pinned total is less then 100 % + dataPinned = normalizePinned(dataPinned, pinnedTotal, 100, false) + dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') + + // sync app store + state = ledgerState.changePinnedValues(state, dataPinned) + } else { + // unpinned publishers + dataUnPinned = dataUnPinned.map((result) => { + let publisher = getPublisherData(result, scorekeeper) + const floatNumber = (publisher.score / unPinnedTotal) * (100 - pinnedTotal) + publisher.percentage = Math.round(floatNumber) + publisher.weight = floatNumber + return publisher + }) + + // normalize unpinned values + dataUnPinned = roundToTarget(dataUnPinned, (100 - pinnedTotal), 'percentage') + } + + const newData = dataPinned.concat(dataUnPinned, dataExcluded) + + // sync synopsis + newData.forEach((item) => { + const publisherKey = item.site + const weight = item.weight + const pinPercentage = item.pinPercentage + savePublisherOption(publisherKey, 'weight', weight) + savePublisherOption(publisherKey, 'pinPercentage', pinPercentage) + state = ledgerState.setPublishersProp(state, publisherKey, 'weight', weight) + state = ledgerState.setPublishersProp(state, publisherKey, 'pinPercentage', pinPercentage) + }) + + return ledgerState.saveAboutSynopsis(state, newData) +} + +const updatePublisherInfo = (state, changedPublisher) => { + if (!getSetting(settings.PAYMENTS_ENABLED)) { + return state + } + + // const options = synopsis.options + state = synopsisNormalizer(state, changedPublisher) + + return state +} + +const inspectP = (db, path, publisher, property, key, callback) => { + const done = (err, result) => { + if (callback) { + if (err) { + callback(err, null) + return + } + + callback(err, result[property]) + } + } + + if (!key) key = publisher + db.get(key, (err, value) => { + let result + + if (err) { + if (!err.notFound) console.error(path + ' get ' + key + ' error: ' + JSON.stringify(err, null, 2)) + return done(err) + } + + try { + result = JSON.parse(value) + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid JSON ' + key + ': ' + value) + result = {} + } + + done(null, result) + }) +} + +// TODO rename function name +const verifiedP = (state, publisherKey, callback) => { + inspectP(v2PublishersDB, v2PublishersPath, publisherKey, 'verified', null, (err, result) => { + if (!err && callback) { + callback(null, result) + } + }) + + if (process.env.NODE_ENV === 'test') { + ['brianbondy.com', 'clifton.io'].forEach((key) => { + if (ledgerState.hasPublisher(state, key)) { + state = ledgerState.setPublisherOption(state, key, 'verified', true) + savePublisherOption(publisherKey, 'verified', true) + } + }) + state = updatePublisherInfo(state) + } + + return state +} + +// TODO rename function +const excludeP = (publisherKey, callback) => { + let doneP + + const done = (err, result) => { + doneP = true + callback(err, result) + } + + if (!v2RulesetDB) { + return setTimeout(() => excludeP(publisherKey, callback), 5 * miliseconds.second) + } + + inspectP(v2RulesetDB, v2RulesetPath, publisherKey, 'exclude', 'domain:' + publisherKey, (err, result) => { + if (!err) { + return done(err, result) + } + + let props = ledgerPublisher.getPublisherProps('https://' + publisherKey) + if (!props) return done() + + v2RulesetDB.createReadStream({lt: 'domain:'}).on('data', (data) => { + if (doneP) return + + const sldP = data.key.indexOf('SLD:') === 0 + const tldP = data.key.indexOf('TLD:') === 0 + if (!tldP && !sldP) return + + if (underscore.intersection(data.key.split(''), + ['^', '$', '*', '+', '?', '[', '(', '{', '|']).length === 0) { + if (data.key !== ('TLD:' + props.TLD) && (props.SLD && data.key !== ('SLD:' + props.SLD.split('.')[0]))) { + return + } + } else { + try { + const regexp = new RegExp(data.key.substr(4)) + if (!regexp.test(props[tldP ? 'TLD' : 'SLD'])) return + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid regexp ' + data.key + ': ' + ex.toString()) + } + } + + let result + try { + result = JSON.parse(data.value) + } catch (ex) { + console.error(v2RulesetPath + ' stream invalid JSON ' + data.entry + ': ' + data.value) + } + + done(null, result.exclude) + }).on('error', (err) => { + console.error(v2RulesetPath + ' stream error: ' + JSON.stringify(err, null, 2)) + }).on('close', () => { + }).on('end', () => { + if (!doneP) done(null, false) + }) + }) +} + +const setLocation = (state, timestamp, tabId) => { + if (!synopsis) { + return state + } + + const locationData = ledgerState.getLocation(state, currentUrl) + if (_internal.verboseP) { + console.log( + `locations[${currentUrl}]=${JSON.stringify(locationData, null, 2)} ` + + `duration=${(timestamp - currentTimestamp)} msec tabId= ${tabId}` + ) + } + if (locationData.isEmpty() || !tabId) { + return state + } + + let publisherKey = locationData.get('publisher') + if (!publisherKey) { + return state + } + + if (!visitsByPublisher[publisherKey]) { + visitsByPublisher[publisherKey] = {} + } + + if (!visitsByPublisher[publisherKey][currentUrl]) { + visitsByPublisher[publisherKey][currentUrl] = { + tabIds: [] + } + } + + const revisitP = visitsByPublisher[publisherKey][currentUrl].tabIds.indexOf(tabId) !== -1 + if (!revisitP) { + visitsByPublisher[publisherKey][currentUrl].tabIds.push(tabId) + } + + let duration = timestamp - currentTimestamp + if (_internal.verboseP) { + console.log('\nadd publisher ' + publisherKey + ': ' + duration + ' msec' + ' revisitP=' + revisitP + ' state=' + + JSON.stringify(underscore.extend({location: currentUrl}, visitsByPublisher[publisherKey][currentUrl]), + null, 2)) + } + + synopsis.addPublisher(publisherKey, {duration: duration, revisitP: revisitP}) + state = ledgerState.setPublisher(state, publisherKey, synopsis.publishers[publisherKey]) + state = updatePublisherInfo(state) + state = verifiedP(state, publisherKey, (error, result) => { + if (!error) { + appActions.onPublisherOptionUpdate(publisherKey, 'verified', result) + savePublisherOption(publisherKey, 'verified', result) + } + }) + + return state +} + +const addVisit = (state, location, timestamp, tabId) => { + if (location === currentUrl) { + return state + } + + state = setLocation(state, timestamp, tabId) + + const lastUrl = pageDataState.getLastUrl(state) + const aboutUrl = getSourceAboutUrl(lastUrl) || lastUrl + if (aboutUrl.match(/^about/)) { + state = pageDataState.resetInfo(state) + } + + location = getSourceAboutUrl(location) || location + + currentUrl = location.match(/^about/) ? locationDefault : location + currentTimestamp = timestamp + return state +} + +const getFavIcon = (state, publisherKey, page) => { + let publisher = ledgerState.getPublisher(state, publisherKey) + const protocol = page.get('protocol') + if (protocol && !publisher.get('protocol')) { + publisher = publisher.set('protocol', protocol) + state = ledgerState.setPublishersProp(state, publisherKey, 'protocol', protocol) + } + + if (publisher.get('faviconURL') == null && (page.get('faviconURL') || publisher.get('protocol'))) { + let faviconURL = page.get('faviconURL') || publisher.get('protocol') + '//' + urlParse(page.get('key')).host + '/favicon.ico' + if (_internal.debugP) { + console.log('\nrequest: ' + faviconURL) + } + + state = ledgerState.setPublishersProp(state, publisherKey, 'faviconURL', null) + fetchFavIcon(publisherKey, faviconURL) + } + + return state +} + +const fetchFavIcon = (publisherKey, url, redirects) => { + if (typeof redirects === 'undefined') { + redirects = 0 + } + + request.request({url: url, responseType: 'blob'}, (err, response, blob) => { + let matchP, prefix, tail + + if (response && _internal.verboseP) { + console.log('[ response for ' + url + ' ]') + console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + + ' ' + (response.statusMessage || '')) + underscore.keys(response.headers).forEach((header) => { + console.log('>>> ' + header + ': ' + response.headers[header]) + }) + console.log('>>>') + console.log('>>> ' + (blob || '').substr(0, 80)) + } + + if (_internal.debugP) { + console.log('\nresponse: ' + url + + ' errP=' + (!!err) + ' blob=' + (blob || '').substr(0, 80) + '\nresponse=' + + JSON.stringify(response, null, 2)) + } + + if (err) { + console.error('response error: ' + err.toString() + '\n' + err.stack) + return null + } + + if (response.statusCode === 301 && response.headers.location) { + if (redirects < 3) fetchFavIcon(publisherKey, response.headers.location, redirects++) + return null + } + + if (response.statusCode !== 200 || response.headers['content-length'] === '0') { + return null + } + + tail = blob.indexOf(';base64,') + if (blob.indexOf('data:image/') !== 0) { + // NB: for some reason, some sites return an image, but with the wrong content-type... + if (tail <= 0) { + return null + } + + prefix = new Buffer(blob.substr(tail + 8, signatureMax), 'base64') + underscore.keys(fileTypes).forEach((fileType) => { + if (matchP) return + if ( + prefix.length >= fileTypes[fileType].length || + fileTypes[fileType].compare(prefix, 0, fileTypes[fileType].length) !== 0 + ) { + return + } + + blob = 'data:image/' + fileType + blob.substr(tail) + matchP = true + }) + if (!matchP) { + return + } + } else if (tail > 0 && (tail + 8 >= blob.length)) return + + appActions.onFavIconReceived(publisherKey, blob) + + if (synopsis.publishers && synopsis.publishers[publisherKey]) { + synopsis.publishers[publisherKey].faviconURL = blob + } + }) +} + +const updateLocation = (state, location, publisherKey) => { + const locationData = ledgerState.getLocation(state, location) + + if (locationData.get('stickyP') == null) { + state = ledgerState.setLocationProp(state, location, 'stickyP', ledgerUtil.stickyP(state, publisherKey)) + } + + if (locationData.get('verified') != null) { + return state + } + + const publisher = ledgerState.getPublisher(state, publisherKey) + const verified = publisher.getIn(['options', 'verified']) + if (verified != null) { + state = ledgerState.setLocationProp(state, location, 'verified', (verified || false)) + } else { + state = verifiedP(state, publisherKey, (err, result) => { + if (err && !err.notFound) { + return + } + + const value = (result && result.verified) || false + appActions.onLedgerLocationUpdate(location, 'verified', value) + }) + } + + const exclude = publisher.getIn(['options', 'exclude']) + if (exclude != null) { + state = ledgerState.setLocationProp(state, location, 'exclude', (exclude || false)) + } else { + excludeP(publisherKey, (err, result) => { + if (err && !err.notFound) { + return + } + + const value = (result && result.exclude) || false + appActions.onLedgerLocationUpdate(location, 'exclude', value) + }) + } + + return state +} + +const pageDataChanged = (state) => { + // NB: in theory we have already seen every element in info except for (perhaps) the last one... + let info = pageDataState.getLastInfo(state) + + if (!synopsis || info.isEmpty()) { + return state + } + + if (info.get('url', '').match(/^about/)) { + return state + } + + const location = info.get('key') + const locationData = ledgerState.getLocation(state, location) + let publisherKey = locationData.get('publisher') + let publisher = ledgerState.getPublisher(state, publisherKey) + if (!publisher.isEmpty()) { + if (publisher.get('faviconURL') == null) { + state = getFavIcon(state, publisherKey, info) + } + + state = updateLocation(state, location, publisherKey) + } else { + try { + publisherKey = ledgerPublisher.getPublisher(location, _internal.ruleset.raw) + if (!publisherKey || (publisherKey && ledgerUtil.blockedP(state, publisherKey))) { + publisherKey = null + } + } catch (ex) { + console.error('getPublisher error for ' + location + ': ' + ex.toString()) + } + + state = ledgerState.setLocationProp(state, info.get('key'), 'publisher', publisherKey) + } + + if (publisherKey && publisher.isEmpty()) { + const initP = !ledgerState.hasPublisher(state, publisherKey) + synopsis.initPublisher(publisherKey) + + if (synopsis.publishers[publisherKey]) { + state = ledgerState.setPublisher(state, publisherKey, synopsis.publishers[publisherKey]) + } + + if (initP) { + excludeP(publisherKey, (unused, exclude) => { + if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { + exclude = false + } else { + exclude = !exclude + } + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude, true) + }) + } + + state = updateLocation(state, location, publisherKey) + state = getFavIcon(state, publisherKey, info) + } + + const pageLoad = pageDataState.getLoad(state) + const view = pageDataState.getView(state) + + if (ledgerUtil.shouldTrackView(view, pageLoad)) { + state = addVisit( + state, + view.get('url', locationDefault), + view.get('timestamp', new Date().getTime()), + view.get('tabId') + ) + } + + return state +} + +const backupKeys = (state, backupAction) => { + const date = moment().format('L') + const paymentId = ledgerState.getInfoProp(state, 'paymentId') + const passphrase = ledgerState.getInfoProp(state, 'passphrase') + + const messageLines = [ + locale.translation('ledgerBackupText1'), + [locale.translation('ledgerBackupText2'), date].join(' '), + '', + [locale.translation('ledgerBackupText3'), paymentId].join(' '), + [locale.translation('ledgerBackupText4'), passphrase].join(' '), + '', + locale.translation('ledgerBackupText5') + ] + + const message = messageLines.join(os.EOL) + const filePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') + + const fs = require('fs') + fs.writeFile(filePath, message, (err) => { + if (err) { + console.error(err) + } else { + tabs.create({url: fileUrl(filePath)}, (webContents) => { + if (backupAction === 'print') { + webContents.print({silent: false, printBackground: false}) + } else { + webContents.downloadURL(fileUrl(filePath), true) + } + }) + } + }) +} + +const recoverKeys = (state, useRecoveryKeyFile, firstKey, secondKey) => { + let firstRecoveryKey, secondRecoveryKey + + if (useRecoveryKeyFile) { + let recoveryKeyFile = promptForRecoveryKeyFile() + if (!recoveryKeyFile) { + // user canceled from dialog, we abort without error + return state + } + + if (recoveryKeyFile) { + const result = loadKeysFromBackupFile(state, recoveryKeyFile) + const keys = result.keys || {} + state = result.state + + if (keys) { + firstRecoveryKey = keys.paymentId + secondRecoveryKey = keys.passphrase + } + } + } + + if (!firstRecoveryKey || !secondRecoveryKey) { + firstRecoveryKey = firstKey + secondRecoveryKey = secondKey + } + + const UUID_REGEX = /^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/ + if ( + typeof firstRecoveryKey !== 'string' || + !firstRecoveryKey.match(UUID_REGEX) || + typeof secondRecoveryKey !== 'string' || + !secondRecoveryKey.match(UUID_REGEX) + ) { + // calling logError sets the error object + state = logError(state, true, 'recoverKeys') + state = ledgerState.setRecoveryStatus(state, false) + return state + } + + client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, result) => { + appActions.onWalletRecovery(err, result) + }) + + return state +} + +const onWalletRecovery = (state, error, result) => { + let existingLedgerError = ledgerState.getInfoProp(state, 'error') + + if (error) { + // we reset ledgerInfo.error to what it was before (likely null) + // if ledgerInfo.error is not null, the wallet info will not display in UI + // logError sets ledgerInfo.error, so we must we clear it or UI will show an error + state = logError(error, 'recoveryWallet') + state = ledgerState.setInfoProp(state, 'error', existingLedgerError) + state = ledgerState.setRecoveryStatus(state, false) + } else { + callback(error, result) + + if (balanceTimeoutId) { + clearTimeout(balanceTimeoutId) + } + getBalance(state) + state = ledgerState.setRecoveryStatus(state, true) + } + + return state +} + +const quit = (state) => { + quitP = true + state = addVisit(state, locationDefault, new Date().getTime(), null) + + if (!getSetting(settings.PAYMENTS_ENABLED) && getSetting(settings.SHUTDOWN_CLEAR_HISTORY)) { + state = ledgerState.resetSynopsis(state, true) + } + + return state +} + +const initSynopsis = (state) => { + state = ledgerState.saveSynopsis(state, null, synopsis.options) + let value = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) + if (!value) { + value = 8 * 1000 + appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME, value) + } + + // for earlier versions of the code... + if (value > 0 && value < 1000) { + value = value * 1000 + } + + synopsis.options.minPublisherDuration = value + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + + value = getSetting(settings.PAYMENTS_MINIMUM_VISITS) + if (!value) { + value = 1 + appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISITS, value) + } + + if (value > 0) { + synopsis.options.minPublisherVisits = value + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + } + + if (process.env.NODE_ENV === 'test') { + synopsis.options.minPublisherDuration = 0 + synopsis.options.minPublisherVisits = 0 + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', 0) + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', 0) + } else { + if (process.env.LEDGER_PUBLISHER_MIN_DURATION) { + value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_DURATION) + synopsis.options.minPublisherDuration = value + state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + } + if (process.env.LEDGER_PUBLISHER_MIN_VISITS) { + value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_VISITS) + synopsis.options.minPublisherVisits = value + state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + } + } + + const publishers = ledgerState.getPublishers(state) + for (let item of publishers) { + const publisherKey = item[0] + excludeP(publisherKey, (unused, exclude) => { + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + }) + + state = verifiedP(state, publisherKey, (error, result) => { + if (!error) { + appActions.onPublisherOptionUpdate(publisherKey, 'verified', result) + savePublisherOption(publisherKey, 'verified', result) + } + }) + } + + state = updatePublisherInfo(state) + + return state +} + +const enable = (state, paymentsEnabled) => { + if (paymentsEnabled && !getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) + } + + if (synopsis) { + return updatePublisherInfo(state) + } + + if (!ledgerPublisher) { + ledgerPublisher = require('ledger-publisher') + } + synopsis = new (ledgerPublisher.Synopsis)() + const stateSynopsis = ledgerState.getSynopsis(state) + + if (_internal.verboseP) { + console.log('\nstarting up ledger publisher integration') + } + + if (stateSynopsis.isEmpty()) { + return initSynopsis(state) + } + + try { + synopsis = new (ledgerPublisher.Synopsis)(stateSynopsis.toJS()) + } catch (ex) { + console.error('synopsisPath parse error: ' + ex.toString()) + } + + state = initSynopsis(state) + + // synopsis cleanup + underscore.keys(synopsis.publishers).forEach((publisher) => { + if (synopsis.publishers[publisher].faviconURL === null) { + delete synopsis.publishers[publisher].faviconURL + } + }) + + // change undefined include publishers to include publishers + state = ledgerState.enableUndefinedPublishers(state, stateSynopsis.get('publishers')) + + return state +} + +const pathName = (name) => { + const parts = path.parse(name) + return path.join(electron.app.getPath('userData'), parts.name + parts.ext) +} + +const sufficientBalanceToReconcile = (state) => { + const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) + const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0) + const btc = ledgerState.getInfoProp(state, 'btc') + return btc && (balance + unconfirmed > 0.9 * Number(btc)) +} + +const shouldShowNotificationReviewPublishers = () => { + const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP) + return !nextTime || (new Date().getTime() > nextTime) +} + +const shouldShowNotificationAddFunds = () => { + const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP) + return !nextTime || (new Date().getTime() > nextTime) +} + +const showNotificationReviewPublishers = (nextTime) => { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('reconciliationNotification'), + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('dismiss')}, + {text: locale.translation('reviewSites'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +const showNotificationAddFunds = () => { + const nextTime = new Date().getTime() + (3 * miliseconds.day) + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('addFundsNotification'), + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('updateLater')}, + {text: locale.translation('addFunds'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +/** + * Show message that it's time to add funds if reconciliation is less than + * a day in the future and balance is too low. + * 24 hours prior to reconciliation, show message asking user to review + * their votes. + */ +const showEnabledNotifications = (state) => { + const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp') + + if (!reconcileStamp) { + return + } + + if (reconcileStamp - new Date().getTime() < miliseconds.day) { + if (sufficientBalanceToReconcile(state)) { + if (shouldShowNotificationReviewPublishers()) { + const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency') + showNotificationReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day)) + } + } else if (shouldShowNotificationAddFunds()) { + showNotificationAddFunds() + } + } else if (reconcileStamp - new Date().getTime() < 2 * miliseconds.day) { + if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { + showNotificationReviewPublishers(new Date().getTime() + miliseconds.day) + } + } +} + +const showDisabledNotifications = (state) => { + if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { + const firstRunTimestamp = state.get('firstRunTimestamp') + if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) { + return + } + + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: locale.translation('notificationTryPayments'), + buttons: [ + {text: locale.translation('noThanks')}, + {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) + } +} + +const showNotifications = (state) => { + if (getSetting(settings.PAYMENTS_ENABLED)) { + if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + showEnabledNotifications(state) + } + } else { + showDisabledNotifications(state) + } +} + +const cacheRuleSet = (state, ruleset) => { + if (!ruleset || underscore.isEqual(_internal.ruleset.raw, ruleset)) { + return state + } + + try { + let stewed = [] + ruleset.forEach((rule) => { + let entry = {condition: acorn.parse(rule.condition)} + + if (rule.dom) { + if (rule.dom.publisher) { + entry.publisher = { + selector: rule.dom.publisher.nodeSelector, + consequent: acorn.parse(rule.dom.publisher.consequent) + } + } + if (rule.dom.faviconURL) { + entry.faviconURL = { + selector: rule.dom.faviconURL.nodeSelector, + consequent: acorn.parse(rule.dom.faviconURL.consequent) + } + } + } + if (!entry.publisher) entry.consequent = rule.consequent ? acorn.parse(rule.consequent) : rule.consequent + + stewed.push(entry) + }) + + _internal.ruleset.raw = ruleset + _internal.ruleset.cooked = stewed + if (!synopsis) { + return state + } + + let syncP = false + const publishers = ledgerState.getPublishers(state) + for (let item of publishers) { + const publisherKey = item[0] + const publisher = item[1] + const location = (publisher.get('protocol') || 'http:') + '//' + publisherKey + let ctx = urlParse(location) + + ctx.TLD = tldjs.getPublicSuffix(ctx.host) + if (!ctx.TLD) { + return state + } + + ctx = underscore.mapObject(ctx, function (value) { + if (!underscore.isFunction(value)) return value + }) + ctx.URL = location + ctx.SLD = tldjs.getDomain(ctx.host) + ctx.RLD = tldjs.getSubdomain(ctx.host) + ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' + + stewed.forEach((rule) => { + if (rule.consequent !== null || rule.dom) return + if (!ruleSolver.resolve(rule.condition, ctx)) return + + if (_internal.verboseP) console.log('\npurging ' + publisherKey) + delete synopsis.publishers[publisher] + state = ledgerState.deletePublishers(state, publisherKey) + syncP = true + }) + } + + if (!syncP) { + return state + } + + return updatePublisherInfo(state) + } catch (ex) { + console.error('ruleset error: ', ex) + return state + } +} + +const clientprep = () => { + if (!ledgerClient) ledgerClient = require('ledger-client') + _internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_DEBUG) + _internal.verboseP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_VERBOSE) +} + +const roundtrip = (params, options, callback) => { + let parts = typeof params.server === 'string' ? urlParse(params.server) + : typeof params.server !== 'undefined' ? params.server + : typeof options.server === 'string' ? urlParse(options.server) : options.server + const rawP = options.rawP + + if (!params.method) params.method = 'GET' + parts = underscore.extend(underscore.pick(parts, ['protocol', 'hostname', 'port']), + underscore.omit(params, ['headers', 'payload', 'timeout'])) + +// TBD: let the user configure this via preferences [MTR] + if (parts.hostname === 'ledger.brave.com' && params.useProxy) { + parts.hostname = 'ledger-proxy.privateinternetaccess.com' + } + + const i = parts.path.indexOf('?') + if (i !== -1) { + parts.pathname = parts.path.substring(0, i) + parts.search = parts.path.substring(i) + } else { + parts.pathname = parts.path + } + + options = { + url: urlFormat(parts), + method: params.method, + payload: params.payload, + responseType: 'text', + headers: underscore.defaults(params.headers || {}, {'content-type': 'application/json; charset=utf-8'}), + verboseP: options.verboseP + } + request.request(options, (err, response, body) => { + let payload + + if (response && options.verboseP) { + console.log('[ response for ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path + ' ]') + console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + + ' ' + (response.statusMessage || '')) + underscore.keys(response.headers).forEach((header) => { + console.log('>>> ' + header + ': ' + response.headers[header]) + }) + console.log('>>>') + console.log('>>> ' + (body || '').split('\n').join('\n>>> ')) + } + + if (err) return callback(err) + + if (Math.floor(response.statusCode / 100) !== 2) { + return callback(new Error('HTTP response ' + response.statusCode) + ' for ' + params.method + ' ' + params.path) + } + + try { + payload = rawP ? body : (response.statusCode !== 204) ? JSON.parse(body) : null + } catch (err) { + return callback(err) + } + + try { + callback(null, response, payload) + } catch (err0) { + if (options.verboseP) console.log('\ncallback: ' + err0.toString() + '\n' + err0.stack) + } + }) + + if (!options.verboseP) return + + console.log('<<< ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path) + underscore.keys(options.headers).forEach((header) => { + console.log('<<< ' + header + ': ' + options.headers[header]) + }) + console.log('<<<') + if (options.payload) console.log('<<< ' + JSON.stringify(params.payload, null, 2).split('\n').join('\n<<< ')) +} + +const updateLedgerInfo = (state) => { + const ledgerInfo = ledgerState.getInfoProps(state) + const now = new Date().getTime() + + if (ledgerInfo.get('buyURLExpires') > now) { + state = ledgerState.setInfoProp(state, 'buyMaximumUSD', 6) + } + if (typeof process.env.ADDFUNDS_URL !== 'undefined') { + state = ledgerState.setInfoProp(state, 'buyURLFrame', true) + const buyURL = process.env.ADDFUNDS_URL + '?' + + queryString.stringify({ + currency: ledgerInfo.get('currency'), + amount: getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT), + address: ledgerInfo.get('address') + }) + state = ledgerState.setInfoProp(state, 'buyURL', buyURL) + state = ledgerState.setInfoProp(state, 'buyMaximumUSD', false) + } + + // TODO remove when BAT is implemented, we don't need this for BAT + /* + if ((client) && (now > ledgerInfo._internal.geoipExpiry)) { + ledgerInfo._internal.geoipExpiry = now + (5 * miliseconds.minute) + + if (!ledgerGeoIP) ledgerGeoIP = require('ledger-geoip') + return ledgerGeoIP.getGeoIP(client.options, (err, provider, result) => { + if (err) console.warn('ledger geoip warning: ' + JSON.stringify(err, null, 2)) + if (result) ledgerInfo.countryCode = result + + ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] + + if (now <= ledgerInfo._internal.exchangeExpiry) return updateLedgerInfo() + + ledgerInfo._internal.exchangeExpiry = now + miliseconds.day + roundtrip({ path: '/v1/exchange/providers' }, client.options, (err, response, body) => { + if (err) console.error('ledger exchange error: ' + JSON.stringify(err, null, 2)) + + ledgerInfo._internal.exchanges = body || {} + ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] + updateLedgerInfo() + }) + }) + } + */ + + return state +} + +// Called from observeTransactions() when we see a new payment (transaction). +const showNotificationPaymentDone = (transactionContributionFiat) => { + notificationPaymentDoneMessage = locale.translation('notificationPaymentDone') + .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount) + .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency) + // Hide the 'waiting for deposit' message box if it exists + appActions.hideNotification(locale.translation('addFundsNotification')) + appActions.showNotification({ + greeting: locale.translation('updateHello'), + message: notificationPaymentDoneMessage, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('Ok'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) +} + +const observeTransactions = (state, transactions) => { + const current = ledgerState.getInfoProp(state, 'transactions') + if (current && current.size === transactions.length) { + return + } + // Notify the user of new transactions. + if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + if (transactions.length > 0) { + const newestTransaction = transactions[transactions.length - 1] + showNotificationPaymentDone(newestTransaction.contribution.fiat) + } + } +} + +// TODO convert this function and related ones to immutable +const getStateInfo = (state, parsedData) => { + const info = parsedData.paymentInfo + const then = new Date().getTime() - miliseconds.year + + if (!parsedData.properties.wallet) { + return state + } + + const newInfo = { + paymentId: parsedData.properties.wallet.paymentId, + passphrase: parsedData.properties.wallet.keychains.passphrase, + created: !!parsedData.properties.wallet, + creating: !parsedData.properties.wallet, + reconcileFrequency: parsedData.properties.days, + reconcileStamp: parsedData.reconcileStamp + } + + state = ledgerState.mergeInfoProp(state, newInfo) + + if (info) { + state = ledgerState.mergeInfoProp(state, info) + state = generatePaymentData(state) + } + + let transactions = [] + if (!parsedData.transactions) { + return updateLedgerInfo(state) + } + + for (let i = parsedData.transactions.length - 1; i >= 0; i--) { + let transaction = parsedData.transactions[i] + if (transaction.stamp < then) break + + if (!transaction.ballots || transaction.ballots.length < transaction.count) continue + + let ballots = underscore.clone(transaction.ballots || {}) + parsedData.ballots.forEach((ballot) => { + if (ballot.viewingId !== transaction.viewingId) return + + if (!ballots[ballot.publisher]) ballots[ballot.publisher] = 0 + ballots[ballot.publisher]++ + }) + + transactions.push(underscore.extend(underscore.pick(transaction, + ['viewingId', 'contribution', 'submissionStamp', 'count']), + {ballots: ballots})) + } + + observeTransactions(state, transactions) + state = ledgerState.setInfoProp(state, 'transactions', Immutable.fromJS(transactions)) + return updateLedgerInfo(state) +} + +const generatePaymentData = (state) => { + const ledgerInfo = ledgerState.getInfoProps(state) + const paymentURL = `bitcoin:${ledgerInfo.get('address')}?amount=${ledgerInfo.get('btc')}&label=${encodeURI('Brave Software')}` + if (ledgerInfo.get('paymentURL') !== paymentURL) { + state = ledgerState.setInfoProp(state, 'paymentURL', paymentURL) + try { + let chunks = [] + qr.image(paymentURL, {type: 'png'}) + .on('data', (chunk) => { + chunks.push(chunk) + }) + .on('end', () => { + const paymentIMG = 'data:image/png;base64,' + Buffer.concat(chunks).toString('base64') + state = ledgerState.setInfoProp(state, 'paymentIMG', paymentIMG) + }) + } catch (ex) { + console.error('qr.imageSync error: ' + ex.toString()) + } + } + + return state +} + +const getPaymentInfo = (state) => { + let amount, currency + + if (!client) { + return state + } + + try { + const bravery = client.getBraveryProperties() + state = ledgerState.setInfoProp(state, 'bravery', Immutable.fromJS(bravery)) + if (bravery.fee) { + amount = bravery.fee.amount + currency = bravery.fee.currency + } + + client.getWalletProperties(amount, currency, function (err, body) { + if (err) { + logError(err, 'getWalletProperties') + return + } + + appActions.onWalletProperties(body) + }) + } catch (ex) { + console.error('properties error: ' + ex.toString()) + } + + return state +} + +const onWalletProperties = (state, body) => { + let newInfo = { + buyURL: body.get('buyURL'), + buyURLExpires: body.get('buyURLExpires'), + balance: body.get('balance'), + unconfirmed: body.get('unconfirmed'), + satoshis: body.get('satoshis') + } + + if (client) { + newInfo.address = client.getWalletAddress() + } + + state = ledgerState.mergeInfoProp(state, newInfo) + + const info = ledgerState.getInfoProps(state) + + const amount = info.getIn(['bravery', 'fee', 'amount']) + const currency = info.getIn(['bravery', 'fee', 'currency']) + + if (amount && currency) { + const bodyCurrency = body.getIn(['rates', 'currency']) + if (bodyCurrency) { + const btc = (amount / bodyCurrency).toFixed(8) + state = ledgerState.setInfoProp(state, 'btc', btc) + } + } + + state = generatePaymentData(state) + + return state +} + +const setPaymentInfo = (amount) => { + let bravery + + if (!client) return + + try { + bravery = client.getBraveryProperties() + } catch (ex) { + // wallet being created... + return setTimeout(function () { + setPaymentInfo(amount) + }, 2 * miliseconds.second) + } + + amount = parseInt(amount, 10) + if (isNaN(amount) || (amount <= 0)) return + + underscore.extend(bravery.fee, {amount: amount}) + client.setBraveryProperties(bravery, (err, result) => { + if (err) { + err = err.toString() + } + + appActions.onBraveryProperties(err, result) + }) +} + +const onBraveryProperties = (state, error, result) => { + const created = ledgerState.getInfoProp(state, 'created') + if (created) { + state = getPaymentInfo(state) + } + + if (error) { + console.error('ledger setBraveryProperties: ' + error) + return state + } + + if (result) { + muonWriter(pathName(statePath), result) + } + + return state +} + +const getBalance = (state) => { + if (!client) return + + const address = ledgerState.getInfoProp(state, 'address') + const balanceFn = getBalance.bind(null, state) + balanceTimeoutId = setTimeout(balanceFn, 1 * miliseconds.minute) + if (!address) { + return + } + + if (!ledgerBalance) ledgerBalance = require('ledger-balance') + ledgerBalance.getBalance(address, underscore.extend({balancesP: true}, client.options), + (err, provider, result) => { + if (err) { + return console.warn('ledger balance warning: ' + JSON.stringify(err, null, 2)) + } + appActions.onLedgerBalanceReceived(result.unconfirmed) + }) +} + +const balanceReceived = (state, unconfirmed) => { + if (typeof unconfirmed === 'undefined') { + return state + } + + if (unconfirmed > 0) { + const result = (unconfirmed / 1e8).toFixed(4) + if (ledgerState.getInfoProp(state, 'unconfirmed') === result) { + return state + } + + state = ledgerState.setInfoProp(state, 'unconfirmed', result) + if (clientOptions.verboseP) { + console.log('\ngetBalance refreshes ledger info: ' + ledgerState.getInfoProp(state, 'unconfirmed')) + } + return updateLedgerInfo(state) + } + + if (ledgerState.getInfoProp(state, 'unconfirmed') === '0.0000') { + return state + } + + if (clientOptions.verboseP) console.log('\ngetBalance refreshes payment info') + return getPaymentInfo(state) +} + +const callback = (err, result, delayTime) => { + if (clientOptions.verboseP) { + console.log('\nledger client callback: clientP=' + (!!client) + ' errP=' + (!!err) + ' resultP=' + (!!result) + + ' delayTime=' + delayTime) + } + + if (err) { + console.log('ledger client error(1): ' + JSON.stringify(err, null, 2) + (err.stack ? ('\n' + err.stack) : '')) + if (!client) return + + if (typeof delayTime === 'undefined') { + delayTime = random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute}) + } + } + + appActions.onLedgerCallback(result, delayTime) +} + +const onCallback = (state, result, delayTime) => { + let results + let entries = client && client.report() + + if (!result) { + return run(state, delayTime) + } + + const regularResults = result.toJS() + + if (client && result.getIn(['properties', 'wallet'])) { + if (!ledgerState.getInfoProp(state, 'created')) { + setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) + } + + state = getStateInfo(state, regularResults) // TODO optimize if possible + state = getPaymentInfo(state) + } + + state = cacheRuleSet(state, regularResults.ruleset) + if (result.has('rulesetV2')) { + results = regularResults.rulesetV2 // TODO optimize if possible + result = result.delete('rulesetV2') + + entries = [] + results.forEach((entry) => { + const key = entry.facet + ':' + entry.publisher + + if (entry.exclude !== false) { + entries.push({type: 'put', key: key, value: JSON.stringify(underscore.omit(entry, ['facet', 'publisher']))}) + } else { + entries.push({type: 'del', key: key}) + } + }) + + v2RulesetDB.batch(entries, (err) => { + if (err) return console.error(v2RulesetPath + ' error: ' + JSON.stringify(err, null, 2)) + + if (entries.length === 0) return + + const publishers = ledgerState.getPublishers(state) + for (let item of publishers) { + const publisherKey = item[0] + excludeP(publisherKey, (unused, exclude) => { + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + }) + } + }) + } + + if (result.has('publishersV2')) { + results = regularResults.publishersV2 // TODO optimize if possible + result = result.delete('publishersV2') + + entries = [] + results.forEach((entry) => { + const publisherKey = entry.publisher + entries.push({ + type: 'put', + key: publisherKey, + value: JSON.stringify(underscore.omit(entry, ['publisher'])) + }) + const publisher = ledgerState.getPublisher(state, publisherKey) + const newValue = entry.verified + if (!publisher.isEmpty() && publisher.getIn(['options', 'verified']) !== newValue) { + synopsis.publishers[publisherKey].options.verified = newValue + state = ledgerState.setPublisherOption(state, publisherKey, 'verified', newValue) + } + }) + state = updatePublisherInfo(state) + v2PublishersDB.batch(entries, (err) => { + if (err) return console.error(v2PublishersPath + ' error: ' + JSON.stringify(err, null, 2)) + }) + } + + muonWriter(pathName(statePath), regularResults) + run(state, delayTime) + + return state +} + +const initialize = (state, paymentsEnabled) => { + if (!v2RulesetDB) v2RulesetDB = levelUp(pathName(v2RulesetPath)) + if (!v2PublishersDB) v2PublishersDB = levelUp(pathName(v2PublishersPath)) + state = enable(state, paymentsEnabled) + + // Check if relevant browser notifications should be shown every 15 minutes + if (notificationTimeout) { + clearInterval(notificationTimeout) + } + notificationTimeout = setInterval((state) => { + showNotifications(state) + }, 15 * miliseconds.minute, state) + + if (!paymentsEnabled) { + client = null + return ledgerState.resetInfo(state) + } + + if (client) { + return state + } + + if (!ledgerPublisher) ledgerPublisher = require('ledger-publisher') + let ruleset = [] + ledgerPublisher.ruleset.forEach(rule => { + if (rule.consequent) ruleset.push(rule) + }) + state = cacheRuleSet(state, ruleset) + + try { + const fs = require('fs') + // TODO change this back to async + fs.accessSync(pathName(statePath), fs.FF_OK) + const data = fs.readFileSync(pathName(statePath)) + let parsedData + + try { + parsedData = JSON.parse(data) + if (clientOptions.verboseP) { + console.log('\nstarting up ledger client integration') + } + } catch (ex) { + console.error('statePath parse error: ' + ex.toString()) + return state + } + + state = getStateInfo(state, parsedData) + + try { + let timeUntilReconcile + clientprep() + client = ledgerClient(parsedData.personaId, + underscore.extend(parsedData.options, {roundtrip: roundtrip}, clientOptions), + parsedData) + + // Scenario: User enables Payments, disables it, waits 30+ days, then + // enables it again -> reconcileStamp is in the past. + // In this case reset reconcileStamp to the future. + try { + timeUntilReconcile = client.timeUntilReconcile() + } catch (ex) {} + + let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') + if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { + client.setTimeUntilReconcile(null, (err, stateResult) => { + if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) + + if (!stateResult) { + return + } + + appActions.onTimeUntilReconcile(stateResult) + }) + } + } catch (ex) { + console.error('ledger client creation error: ', ex) + return state + } + + // speed-up browser start-up by delaying the first synchronization action + setTimeout(() => { + if (!client) { + return + } + + appActions.onLedgerFirstSync(parsedData) + }, 3 * miliseconds.second) + + // Make sure bravery props are up-to-date with user settings + const address = ledgerState.getInfoProp(state, 'address') + if (address) { + state = ledgerState.setInfoProp(state, 'address', client.getWalletAddress()) + } + + setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) + getBalance(state) + + return state + } catch (err) { + if (err.code !== 'ENOENT') { + console.error('statePath read error: ' + err.toString()) + } + state = ledgerState.resetInfo(state) + return state + } +} + +const onTimeUntilReconcile = (state, stateResult) => { + state = getStateInfo(stateResult) + muonWriter(pathName(statePath), stateResult) + + return state +} + +const onLedgerFirstSync = (state, parsedData) => { + if (client.sync(callback) === true) { + run(state, random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute})) + } + + return cacheRuleSet(state, parsedData.ruleset) +} + +const init = (state) => { + return initialize(state, getSetting(settings.PAYMENTS_ENABLED)) +} + +const run = (state, delayTime) => { + if (clientOptions.verboseP) { + console.log('\nledger client run: clientP=' + (!!client) + ' delayTime=' + delayTime) + + const line = (fields) => { + let result = '' + + fields.forEach((field) => { + const max = (result.length > 0) ? 9 : 19 + + if (typeof field !== 'string') field = field.toString() + if (field.length < max) { + let spaces = ' '.repeat(max - field.length) + field = spaces + field + } else { + field = field.substr(0, max) + } + result += ' ' + field + }) + + console.log(result.substr(1)) + } + + line(['publisher', + 'blockedP', 'stickyP', 'verified', + 'excluded', 'eligibleP', 'visibleP', + 'contribP', + 'duration', 'visits' + ]) + let entries = synopsis.topN() || [] + entries.forEach((entry) => { + const publisherKey = entry.publisher + const publisher = ledgerState.getPublisher(state, publisherKey) + + line([publisherKey, + ledgerUtil.blockedP(state, publisherKey), ledgerUtil.stickyP(state, publisherKey), publisher.getIn(['options', 'verified']) === true, + publisher.getIn(['options', 'exclude']) === true, ledgerUtil.eligibleP(state, publisherKey), ledgerUtil.visibleP(state, publisherKey), + ledgerUtil.contributeP(state, publisherKey), + Math.round(publisher.get('duration') / 1000), publisher.get('visits')]) + }) + } + + if (typeof delayTime === 'undefined' || !client) { + return + } + + let winners + const ballots = client.ballots() + const data = (synopsis) && (ballots > 0) && synopsisNormalizer(state) + + if (data) { + let weights = [] + data.forEach((datum) => { + weights.push({publisher: datum.site, weight: datum.weight / 100.0}) + }) + winners = synopsis.winners(ballots, weights) + } + + if (!winners) winners = [] + + try { + let stateData + winners.forEach((winner) => { + if (!ledgerUtil.contributeP(state, winner)) return + + const result = client.vote(winner) + if (result) stateData = result + }) + if (stateData) muonWriter(pathName(statePath), stateData) + } catch (ex) { + console.log('ledger client error(2): ' + ex.toString() + (ex.stack ? ('\n' + ex.stack) : '')) + } + + if (delayTime === 0) { + try { + delayTime = client.timeUntilReconcile() + } catch (ex) { + delayTime = false + } + if (delayTime === false) { + delayTime = random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute}) + } + } + + if (delayTime > 0) { + if (runTimeoutId) return + + const active = client + if (delayTime > (1 * miliseconds.hour)) { + delayTime = random.randomInt({min: 3 * miliseconds.minute, max: miliseconds.hour}) + } + + runTimeoutId = setTimeout(() => { + runTimeoutId = false + if (active !== client) return + + if (!client) { + return console.log('\n\n*** MTR says this can\'t happen(1)... please tell him that he\'s wrong!\n\n') + } + + if (client.sync(callback) === true) { + appActions.onLedgerRun(0) + } + }, delayTime) + return + } + + if (client.isReadyToReconcile()) { + client.reconcile(uuid.v4().toLowerCase(), callback) + } +} + +const networkConnected = () => { + underscore.debounce(() => { + if (!client) return + + appActions.onNetworkConnected() + }, 1 * miliseconds.minute, true) +} + +const onNetworkConnected = (state) => { + if (runTimeoutId) { + clearTimeout(runTimeoutId) + runTimeoutId = false + } + + if (client.sync(callback) === true) { + const delayTime = random.randomInt({min: miliseconds.minute, max: 10 * miliseconds.minute}) + run(state, delayTime) + } + + if (balanceTimeoutId) clearTimeout(balanceTimeoutId) + const newBalance = getBalance.bind(null, state) + balanceTimeoutId = setTimeout(newBalance, 5 * miliseconds.second) +} + +const muonWriter = (path, payload) => { + muon.file.writeImportant(path, JSON.stringify(payload, null, 2), (success) => { + if (!success) return console.error('write error: ' + path) + + if (quitP && (!getSetting(settings.PAYMENTS_ENABLED) && getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { + const fs = require('fs') + return fs.unlink(path, (err) => { + if (err) console.error('unlink error: ' + err.toString()) + }) + } + }) +} + +const migration = (state) => { + const synopsisPath = 'ledger-synopsis.json' + + const synopsisOptions = ledgerState.getSynopsisOptions(state) + + if (getSetting(settings.PAYMENTS_ENABLED) && synopsisOptions.isEmpty()) { + // Move data from synopsis file into appState + const fs = require('fs') + try { + fs.accessSync(pathName(synopsisPath), fs.FF_OK) + const data = fs.readFileSync(pathName(synopsisPath)) + const parsed = JSON.parse(data) + state = ledgerState.saveSynopsis(state, parsed.publishers, parsed.options) + fs.unlink(pathName(synopsisPath), (err) => { + if (err && err.code !== 'ENOENT') { + console.error('error removing file ' + synopsisPath + ': ', err) + } + }) + } catch (err) { + console.log('Error migrating file', err.toString()) + } + + // Delete ledgerInfo + state = state.delete('ledgerInfo') + + // Move locationInfo into ledger + if (state.has('locationInfo')) { + const locationInfo = state.get('locationInfo') + state = state.setIn(['ledger', 'locations'], locationInfo) + state = state.delete('locationInfo') + } + } + + return state +} + +// for synopsis variable handling only +const deleteSynopsis = (publisherKey) => { + delete synopsis.publishers[publisherKey] +} + +const saveOptionSynopsis = (prop, value) => { + synopsis.options[prop] = value +} + +const savePublisherOption = (publisherKey, prop, value) => { + if (synopsis.publishers && synopsis.publishers[publisherKey]) { + synopsis.publishers[publisherKey].options[prop] = value + } +} + +module.exports = { + backupKeys, + recoverKeys, + quit, + addVisit, + pageDataChanged, + init, + initialize, + setPaymentInfo, + updatePublisherInfo, + networkConnected, + verifiedP, + boot, + onBootStateFile, + balanceReceived, + onWalletProperties, + paymentPresent, + addFoundClosed, + onWalletRecovery, + onBraveryProperties, + onLedgerFirstSync, + onCallback, + deleteSynopsis, + saveOptionSynopsis, + savePublisherOption, + onTimeUntilReconcile, + run, + onNetworkConnected, + migration +} diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index adfa8830a00..f9b313b97e7 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -3,7 +3,6 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const Immutable = require('immutable') -const underscore = require('underscore') // Constants const appConstants = require('../../../js/constants/appConstants') @@ -12,44 +11,31 @@ const settings = require('../../../js/constants/settings') // State const ledgerState = require('../../common/state/ledgerState') +const siteSettingsState = require('../../common/state/siteSettingsState') // Utils -const ledgerUtil = require('../../common/lib/ledgerUtil') +const ledgerApi = require('../../browser/api/ledger') +const urlUtil = require('../../../js/lib/urlutil') const {makeImmutable} = require('../../common/state/immutableUtil') const getSetting = require('../../../js/settings').getSetting const ledgerReducer = (state, action, immutableAction) => { action = immutableAction || makeImmutable(action) switch (action.get('actionType')) { - case appConstants.APP_UPDATE_LEDGER_INFO: - { - state = state.setIn(['ledger', 'info'], action.get('ledgerInfo')) - break - } - // TODO refactor - case appConstants.APP_UPDATE_LOCATION_INFO: - { - state = state.setIn(['ledger', 'locations'], action.get('locationInfo')) - break - } - case appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED: - { - state = ledgerState.setRecoveryStatus(state, action.get('recoverySucceeded')) - break - } case appConstants.APP_SET_STATE: { - state = ledgerUtil.init(state) + state = ledgerApi.migration(state) + state = ledgerApi.init(state) break } case appConstants.APP_BACKUP_KEYS: { - ledgerUtil.backupKeys(state, action.get('backupAction')) + ledgerApi.backupKeys(state, action.get('backupAction')) break } case appConstants.APP_RECOVER_WALLET: { - state = ledgerUtil.recoverKeys( + state = ledgerApi.recoverKeys( state, action.get('useRecoveryKeyFile'), action.get('firstRecoveryKey'), @@ -59,7 +45,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_SHUTTING_DOWN: { - state = ledgerUtil.quit(state) + state = ledgerApi.quit(state) break } case appConstants.APP_ON_CLEAR_BROWSING_DATA: @@ -72,11 +58,10 @@ const ledgerReducer = (state, action, immutableAction) => { } break } - // TODO not sure that we use APP_IDLE_STATE_CHANGED anymore case appConstants.APP_IDLE_STATE_CHANGED: { - state = ledgerUtil.pageDataChanged(state) - ledgerUtil.addVisit('NOOP', underscore.now(), null) + state = ledgerApi.pageDataChanged(state) + state = ledgerApi.addVisit(state, 'NOOP', new Date().getTime(), null) break } case appConstants.APP_CHANGE_SETTING: @@ -84,20 +69,21 @@ const ledgerReducer = (state, action, immutableAction) => { switch (action.get('key')) { case settings.PAYMENTS_ENABLED: { - state = ledgerUtil.initialize(state, action.get('value')) + state = ledgerApi.initialize(state, action.get('value')) break } case settings.PAYMENTS_CONTRIBUTION_AMOUNT: { - ledgerUtil.setPaymentInfo(action.get('value')) + ledgerApi.setPaymentInfo(action.get('value')) break } case settings.PAYMENTS_MINIMUM_VISIT_TIME: { const value = action.get('value') if (value <= 0) break - ledgerUtil.synopsis.options.minPublisherDuration = action.value + ledgerApi.saveOptionSynopsis('minPublisherDuration', value) state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) + state = ledgerApi.updatePublisherInfo(state) break } case settings.PAYMENTS_MINIMUM_VISITS: @@ -105,16 +91,18 @@ const ledgerReducer = (state, action, immutableAction) => { const value = action.get('value') if (value <= 0) break - ledgerUtil.synopsis.options.minPublisherVisits = value + ledgerApi.saveOptionSynopsis('minPublisherVisits', value) state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) + state = ledgerApi.updatePublisherInfo(state) break } case settings.PAYMENTS_ALLOW_NON_VERIFIED: { const value = action.get('value') - ledgerUtil.synopsis.options.showOnlyVerified = value + ledgerApi.saveOptionSynopsis('showOnlyVerified', value) state = ledgerState.setSynopsisOption(state, 'showOnlyVerified', value) + state = ledgerApi.updatePublisherInfo(state) break } } @@ -135,9 +123,9 @@ const ledgerReducer = (state, action, immutableAction) => { case 'ledgerPaymentsShown': { if (action.get('value') === false) { - delete ledgerUtil.synopsis.publishers[publisherKey] + ledgerApi.deleteSynopsis(publisherKey) state = ledgerState.deletePublishers(state, publisherKey) - state = ledgerUtil.updatePublisherInfo(state) + state = ledgerApi.updatePublisherInfo(state) } break } @@ -147,19 +135,19 @@ const ledgerReducer = (state, action, immutableAction) => { if (publisher.isEmpty()) { break } - state = ledgerUtil.updatePublisherInfo(state) - state = ledgerUtil.verifiedP(state, publisherKey) + state = ledgerApi.updatePublisherInfo(state) + state = ledgerApi.verifiedP(state, publisherKey) break } case 'ledgerPinPercentage': { + const value = action.get('value') const publisher = ledgerState.getPublisher(state, publisherKey) - if (publisher.isEmpty()) { + if (publisher.isEmpty() || publisher.get('pinPercentage') === value) { break } - - ledgerUtil.synopsis.publishers[publisherKey].pinPercentage = action.get('value') - state = ledgerUtil.updatePublisherInfo(state, publisherKey) + state = ledgerState.setPublishersProp(state, publisherKey, 'pinPercentage', value) + state = ledgerApi.updatePublisherInfo(state, publisherKey) break } } @@ -182,15 +170,13 @@ const ledgerReducer = (state, action, immutableAction) => { if (publisher.isEmpty()) { break } - state = ledgerUtil.updatePublisherInfo(state) + state = ledgerApi.updatePublisherInfo(state) } break } case appConstants.APP_NETWORK_CONNECTED: { - setTimeout((state) => { - ledgerUtil.networkConnected(state) - }, 1000, state) + ledgerApi.networkConnected(state) break } case appConstants.APP_NAVIGATOR_HANDLER_REGISTERED: @@ -211,7 +197,115 @@ const ledgerReducer = (state, action, immutableAction) => { case windowConstants.WINDOW_SET_FOCUSED_FRAME: case windowConstants.WINDOW_GOT_RESPONSE_DETAILS: { - state = ledgerUtil.pageDataChanged(state) + state = ledgerApi.pageDataChanged(state) + break + } + case appConstants.APP_ON_FAVICON_RECEIVED: + { + state = ledgerState.setPublishersProp(state, action.get('publisherKey'), 'faviconURL', action.get('blob')) + state = ledgerApi.updatePublisherInfo(state) + break + } + case appConstants.APP_ON_EXCLUSION_STATUS: + { + const key = action.get('publisherKey') + const pattern = urlUtil.getHostPattern(key) + const value = action.get('excluded') + ledgerApi.savePublisherOption(key, 'exclude', value) + state = siteSettingsState.setSettingsProp(state, pattern, 'ledgerPayments', value) + state = ledgerState.setPublishersProp(state, key, ['options', 'exclude'], value) + state = ledgerApi.updatePublisherInfo(state) + break + } + case appConstants.APP_ON_LEDGER_LOCATION_UPDATE: + { + const location = action.get('location') + state = ledgerState.setLocationProp(state, location, action.get('prop'), action.get('value')) + break + } + case appConstants.APP_ON_PUBLISHER_OPTION_UPDATE: + { + const value = action.get('value') + const key = action.get('publisherKey') + const prop = action.get('prop') + state = ledgerState.setPublisherOption(state, key, prop, value) + + if (action.get('saveIntoSettings')) { + const pattern = urlUtil.getHostPattern(key) + if (prop === 'exclude') { + state = siteSettingsState.setSettingsProp(state, pattern, 'ledgerPayments', value) + } + } + break + } + case appConstants.APP_ON_LEDGER_WALLET_CREATE: + { + ledgerApi.boot() + break + } + case appConstants.APP_ON_BOOT_STATE_FILE: + { + state = ledgerApi.onBootStateFile(state) + break + } + case appConstants.APP_ON_LEDGER_BALANCE_RECEIVED: + { + state = ledgerApi.balanceReceived(state, action.get('unconfirmed')) + break + } + case appConstants.APP_ON_WALLET_PROPERTIES: + { + state = ledgerApi.onWalletProperties(state, action.get('body')) + break + } + case appConstants.APP_LEDGER_PAYMENTS_PRESENT: + { + ledgerApi.paymentPresent(state, action.get('tabId'), action.get('present')) + break + } + case appConstants.APP_ON_ADD_FUNDS_CLOSED: + { + ledgerApi.addFoundClosed(state) + break + } + case appConstants.APP_ON_WALLET_RECOVERY: + { + state = ledgerApi.onWalletRecovery(state, action.get('error'), action.get('result')) + break + } + case appConstants.APP_ON_BRAVERY_PROPERTIES: + { + state = ledgerApi.onBraveryProperties(state, action.get('error'), action.get('result')) + break + } + case appConstants.APP_ON_FIRST_LEDGER_SYNC: + { + state = ledgerApi.onLedgerFirstSync(state, action.get('parsedData')) + break + } + case appConstants.APP_ON_LEDGER_CALLBACK: + { + state = ledgerApi.onCallback(state, action.get('result'), action.get('delayTime')) + break + } + case appConstants.APP_ON_TIME_UNTIL_RECONCILE: + { + state = ledgerApi.onTimeUntilReconcile(state, action.get('stateResult')) + break + } + case appConstants.APP_ON_LEDGER_RUN: + { + ledgerApi.run(state, action.get('delay')) + break + } + case appConstants.APP_ON_NETWORK_CONNECTED: + { + state = ledgerApi.onNetworkConnected(state) + break + } + case appConstants.APP_ON_RESET_RECOVERY_STATUS: + { + state = ledgerState.setRecoveryStatus(state, null) break } } diff --git a/app/browser/reducers/pageDataReducer.js b/app/browser/reducers/pageDataReducer.js index 74eaf9df48e..798bdf342ef 100644 --- a/app/browser/reducers/pageDataReducer.js +++ b/app/browser/reducers/pageDataReducer.js @@ -14,7 +14,7 @@ const pageDataState = require('../../common/state/pageDataState') // Utils const {makeImmutable} = require('../../common/state/immutableUtil') -const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') +const {isSourceAboutUrl, getTargetAboutUrl} = require('../../../js/lib/appUrlUtil') const {responseHasContent} = require('../../common/lib/httpUtil') const pageDataReducer = (state, action, immutableAction) => { @@ -68,7 +68,8 @@ const pageDataReducer = (state, action, immutableAction) => { } const responseCode = action.getIn(['details', 'httpResponseCode']) - if (isSourceAboutUrl(pageUrl) || !responseHasContent(responseCode)) { + const isNewTab = getTargetAboutUrl('about:new-tab') === pageUrl + if (isSourceAboutUrl(pageUrl) || isNewTab || !responseHasContent(responseCode)) { break } diff --git a/app/browser/tabs.js b/app/browser/tabs.js index 8e6101337cb..289fe974b70 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -33,6 +33,7 @@ const bookmarksState = require('../common/state/bookmarksState') const bookmarkFoldersState = require('../common/state/bookmarkFoldersState') const historyState = require('../common/state/historyState') const bookmarkOrderCache = require('../common/cache/bookmarkOrderCache') +const ledgerState = require('../common/state/ledgerState') const {getWindow} = require('./windows') let currentPartitionNumber = 0 @@ -154,8 +155,8 @@ const updateAboutDetails = (tab, tabValue) => { let location = getBaseUrl(url) // TODO(bridiver) - refactor these to use state helpers - const ledgerInfo = appState.get('ledgerInfo') - const publisherInfo = appState.get('publisherInfo') + const ledgerInfo = ledgerState.getInfoProps(appState) + const synopsis = appState.getIn(['ledger', 'about']) const preferencesData = appState.getIn(['about', 'preferences']) const appSettings = appState.get('settings') let allSiteSettings = appState.get('siteSettings') @@ -176,17 +177,23 @@ const updateAboutDetails = (tab, tabValue) => { const autofillAddresses = appState.getIn(['autofill', 'addresses']) const versionInformation = appState.getIn(['about', 'brave', 'versionInformation']) const aboutDetails = tabValue.get('aboutDetails') - // TODO(bridiver) - convert this to an action + + // TODO save this into values into the sate so that we don't call this app action on every state change + // this should be saved in app state when windows will be refactored #11151 + /* if (url === 'about:preferences#payments') { tab.on('destroyed', () => { - process.emit(messages.LEDGER_PAYMENTS_PRESENT, tabValue.get('tabId'), false) + appActions.ledgerPaymentsPresent(tabValue.get('tabId'), false) }) - process.emit(messages.LEDGER_PAYMENTS_PRESENT, tabValue.get('tabId'), true) + appActions.ledgerPaymentsPresent(tabValue.get('tabId'), false) } else { - process.emit(messages.LEDGER_PAYMENTS_PRESENT, tabValue.get('tabId'), false) + appActions.ledgerPaymentsPresent(tabValue.get('tabId'), false) } + */ if (location === 'about:preferences' || location === 'about:contributions' || location === aboutUrls.get('about:contributions')) { - const ledgerData = ledgerInfo.merge(publisherInfo).merge(preferencesData) + const ledgerData = ledgerInfo + .merge(synopsis) + .merge(preferencesData) tab.send(messages.LEDGER_UPDATED, ledgerData.toJS()) tab.send(messages.SETTINGS_UPDATED, appSettings.toJS()) tab.send(messages.SITE_SETTINGS_UPDATED, allSiteSettings.toJS()) diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index cf19c28d732..9293983c16e 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -4,98 +4,21 @@ 'use strict' -const acorn = require('acorn') -const moment = require('moment') const Immutable = require('immutable') -const electron = require('electron') -const path = require('path') -const os = require('os') -const qr = require('qr-image') -const underscore = require('underscore') -const tldjs = require('tldjs') -const urlFormat = require('url').format -const queryString = require('queryString') -const levelUp = require('level') -const random = require('random-lib') - -// Actions -const appActions = require('../../../js/actions/appActions') +const moment = require('moment') // State +const siteSettingsState = require('../state/siteSettingsState') const ledgerState = require('../state/ledgerState') -const pageDataState = require('../state/pageDataState') // Constants const settings = require('../../../js/constants/settings') // Utils const {responseHasContent} = require('./httpUtil') -const {makeImmutable} = require('../../common/state/immutableUtil') -const tabs = require('../../browser/tabs') -const locale = require('../../locale') -const siteSettingsState = require('../state/siteSettingsState') -const appConfig = require('../../../js/constants/appConfig') +const urlUtil = require('../../../js/lib/urlutil') +const {makeImmutable} = require('../state/immutableUtil') const getSetting = require('../../../js/settings').getSetting -const {fileUrl} = require('../../../js/lib/appUrlUtil') -const urlParse = require('../urlParse') -const ruleSolver = require('../../extensions/brave/content/scripts/pageInformation') -const request = require('../../../js/lib/request') - -let ledgerPublisher -let ledgerClient -let ledgerBalance -let client -let locationDefault = 'NOOP' -let currentUrl = locationDefault -let currentTimestamp = new Date().getTime() -let visitsByPublisher = {} -let synopsis -let notificationTimeout -let runTimeoutId - -// Database -let v2RulesetDB -const v2RulesetPath = 'ledger-rulesV2.leveldb' -let v2PublishersDB -const v2PublishersPath = 'ledger-publishersV2.leveldb' -const statePath = 'ledger-state.json' - -const miliseconds = { - year: 365 * 24 * 60 * 60 * 1000, - week: 7 * 24 * 60 * 60 * 1000, - day: 24 * 60 * 60 * 1000, - hour: 60 * 60 * 1000, - minute: 60 * 1000, - second: 1000 -} - -const clientOptions = { - debugP: process.env.LEDGER_DEBUG, - loggingP: process.env.LEDGER_LOGGING, - rulesTestP: process.env.LEDGER_RULES_TESTING, - verboseP: process.env.LEDGER_VERBOSE, - server: process.env.LEDGER_SERVER_URL, - createWorker: electron.app.createWorker -} - -const ledgerInfo = { - _internal: { - paymentInfo: {} - } -} - -// TODO only temporally so that standard is happy -const publisherInfo = { - _internal: { - verboseP: true, - debugP: true, - enabled: false, - ruleset: { - raw: [], - cooked: [] - } - } -} /** * Is page an actual page being viewed by the user? (not an error page, etc) @@ -105,12 +28,11 @@ const publisherInfo = { * @return {boolean} true if page should have usage collected, false if not */ const shouldTrackView = (view, responseList) => { - view = makeImmutable(view) - if (view == null) { return false } + view = makeImmutable(view) const tabId = view.get('tabId') const url = view.get('url') @@ -206,1714 +128,89 @@ const walletStatus = (ledgerData) => { return status } -const promptForRecoveryKeyFile = () => { - const defaultRecoveryKeyFilePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') - let files - if (process.env.SPECTRON) { - // skip the dialog for tests - console.log(`for test, trying to recover keys from path: ${defaultRecoveryKeyFilePath}`) - files = [defaultRecoveryKeyFilePath] - } else { - const dialog = electron.dialog - files = dialog.showOpenDialog({ - properties: ['openFile'], - defaultPath: defaultRecoveryKeyFilePath, - filters: [{ - name: 'TXT files', - extensions: ['txt'] - }] - }) - } - - return (files && files.length ? files[0] : null) -} - -const logError = (state, err, caller) => { - if (err) { - console.error('Error in %j: %j', caller, err) - state = ledgerState.setLedgerError(state, err, caller) - } else { - state = ledgerState.setLedgerError(state) - } - - return state -} - -const loadKeysFromBackupFile = (state, filePath) => { - let keys = null - const fs = require('fs') - let data = fs.readFileSync(filePath) - - if (!data || !data.length || !(data.toString())) { - state = logError(state, 'No data in backup file', 'recoveryWallet') - } else { - try { - const recoveryFileContents = data.toString() - - let messageLines = recoveryFileContents.split(os.EOL) - - let paymentIdLine = '' || messageLines[3] - let passphraseLine = '' || messageLines[4] - - const paymentIdPattern = new RegExp([locale.translation('ledgerBackupText3'), '([^ ]+)'].join(' ')) - const paymentId = (paymentIdLine.match(paymentIdPattern) || [])[1] - - const passphrasePattern = new RegExp([locale.translation('ledgerBackupText4'), '(.+)$'].join(' ')) - const passphrase = (passphraseLine.match(passphrasePattern) || [])[1] - - keys = { - paymentId, - passphrase - } - } catch (exc) { - state = logError(state, exc, 'recoveryWallet') - } - } - - return { - state, - keys - } -} - -const getPublisherData = (result, scorekeeper) => { - let duration = result.duration - - let data = { - verified: result.options.verified || false, - site: result.publisher, - views: result.visits, - duration: duration, - daysSpent: 0, - hoursSpent: 0, - minutesSpent: 0, - secondsSpent: 0, - faviconURL: result.faviconURL, - score: result.scores[scorekeeper], - pinPercentage: result.pinPercentage, - weight: result.pinPercentage - } - // HACK: Protocol is sometimes blank here, so default to http:// so we can - // still generate publisherURL. - data.publisherURL = (result.protocol || 'http:') + '//' + result.publisher - - if (duration >= miliseconds.day) { - data.daysSpent = Math.max(Math.round(duration / miliseconds.day), 1) - } else if (duration >= miliseconds.hour) { - data.hoursSpent = Math.max(Math.floor(duration / miliseconds.hour), 1) - data.minutesSpent = Math.round((duration % miliseconds.hour) / miliseconds.minute) - } else if (duration >= miliseconds.minute) { - data.minutesSpent = Math.max(Math.round(duration / miliseconds.minute), 1) - data.secondsSpent = Math.round((duration % miliseconds.minute) / miliseconds.second) - } else { - data.secondsSpent = Math.max(Math.round(duration / miliseconds.second), 1) - } - - return data -} - -const normalizePinned = (dataPinned, total, target, setOne) => { - return dataPinned.map((publisher) => { - let newPer - let floatNumber - - if (setOne) { - newPer = 1 - floatNumber = 1 - } else { - floatNumber = (publisher.pinPercentage / total) * target - newPer = Math.floor(floatNumber) - if (newPer < 1) { - newPer = 1 - } - } - - publisher.weight = floatNumber - publisher.pinPercentage = newPer - return publisher - }) -} - -// courtesy of https://stackoverflow.com/questions/13483430/how-to-make-rounded-percentages-add-up-to-100#13485888 -const roundToTarget = (l, target, property) => { - let off = target - underscore.reduce(l, (acc, x) => { return acc + Math.round(x[property]) }, 0) - - return underscore.sortBy(l, (x) => Math.round(x[property]) - x[property]) - .map((x, i) => { - x[property] = Math.round(x[property]) + (off > i) - (i >= (l.length + off)) - return x - }) -} - // TODO rename function const blockedP = (state, publisherKey) => { - const pattern = `https?://${publisherKey}` + const pattern = urlUtil.getHostPattern(publisherKey) const ledgerPaymentsShown = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPaymentsShown') return ledgerPaymentsShown === false } -// TODO rename function -const stickyP = (state, publisherKey) => { - const pattern = `https?://${publisherKey}` - let result = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPayments') - - // NB: legacy clean-up - if ((typeof result === 'undefined') && (typeof synopsis.publishers[publisherKey].options.stickyP !== 'undefined')) { - result = synopsis.publishers[publisherKey].options.stickyP - appActions.changeSiteSetting(pattern, 'ledgerPayments', result) - } - if (synopsis.publishers[publisherKey] && - synopsis.publishers[publisherKey].options && - synopsis.publishers[publisherKey].options.stickyP) { - delete synopsis.publishers[publisherKey].options.stickyP - } - - return (result === undefined || result) -} - -// TODO rename function -const eligibleP = (state, publisherKey) => { - if (!synopsis.options.minPublisherDuration && process.env.NODE_ENV !== 'test') { - // TODO make sure that appState has correct data in - synopsis.options.minPublisherDuration = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) - } - - const scorekeeper = ledgerState.getSynopsisOption(state, 'scorekeeper') - const minPublisherDuration = ledgerState.getSynopsisOption(state, 'minPublisherDuration') - const minPublisherVisits = ledgerState.getSynopsisOption(state, 'minPublisherVisits') +// TODO rename +const contributeP = (state, publisherKey) => { const publisher = ledgerState.getPublisher(state, publisherKey) - return ( - publisher.getIn(['scores', scorekeeper]) > 0 && - publisher.get('duration') >= minPublisherDuration && - publisher.get('visits') >= minPublisherVisits + (stickyP(state, publisherKey) || publisher.getIn(['options', 'exclude']) !== true) && + eligibleP(state, publisherKey) && + !blockedP(state, publisherKey) ) } // TODO rename function const visibleP = (state, publisherKey) => { const publisher = ledgerState.getPublisher(state, publisherKey) - // TODO you stopped here let showOnlyVerified = ledgerState.getSynopsisOption(state, 'showOnlyVerified') + if (showOnlyVerified == null) { showOnlyVerified = getSetting(settings.PAYMENTS_ALLOW_NON_VERIFIED) state = ledgerState.setSynopsisOption(state, 'showOnlyVerified', showOnlyVerified) - synopsis.options.showOnlyVerified = showOnlyVerified } const publisherOptions = publisher.get('options', Immutable.Map()) const onlyVerified = !showOnlyVerified // Publisher Options - const excludedByUser = blockedP(state, publisherKey) - const eligibleByPublisherToggle = stickyP(state, publisherKey) != null - const eligibleByNumberOfVisits = eligibleP(state, publisherKey) + const deletedByUser = blockedP(state, publisherKey) + const includeExclude = stickyP(state, publisherKey) + const eligibleByStats = eligibleP(state, publisherKey) // num of visits and time spent const isInExclusionList = publisherOptions.get('exclude') const verifiedPublisher = publisherOptions.get('verified') - // websites not included in exclusion list are eligible by number of visits - // but can be enabled by user action in the publisher toggle - const isEligible = (eligibleByNumberOfVisits && !isInExclusionList) || eligibleByPublisherToggle - - // If user decide to remove the website, don't show it. - if (excludedByUser) { - return false - } - - // Unless user decided to enable publisher with publisherToggle, - // do not show exclusion list. - if (!eligibleByPublisherToggle && isInExclusionList) { - return false - } - - // If verified option is set, only show verified publishers - if (isEligible && onlyVerified) { - return verifiedPublisher - } - - return isEligible -} - -const synopsisNormalizer = (state, publishers, options, changedPublisher) => { - let results - let dataPinned = [] - let dataUnPinned = [] - let dataExcluded = [] - let pinnedTotal = 0 - let unPinnedTotal = 0 - const scorekeeper = options.scorekeeper - - results = [] // TODO convert to Immutable.List - publishers.forEach((publisher, index) => { - if (!visibleP(state, index)) { - return - } - - publisher.publisher = index - results.push(publisher) - }) - results = underscore.sortBy(results, (entry) => { return -entry.scores[scorekeeper] }) - - // move publisher to the correct array and get totals - results.forEach((result) => { - if (result.pinPercentage && result.pinPercentage > 0) { - // pinned - pinnedTotal += result.pinPercentage - dataPinned.push(getPublisherData(result, scorekeeper)) - } else if (stickyP(result.publisher)) { - // unpinned - unPinnedTotal += result.scores[scorekeeper] - dataUnPinned.push(result) - } else { - // excluded - let publisher = getPublisherData(result, scorekeeper) - publisher.percentage = 0 - publisher.weight = 0 - dataExcluded.push(publisher) - } - }) - - // round if over 100% of pinned publishers - if (pinnedTotal > 100) { - if (changedPublisher) { - const changedObject = dataPinned.filter(publisher => publisher.site === changedPublisher)[0] - const setOne = changedObject.pinPercentage > (100 - dataPinned.length - 1) - - if (setOne) { - changedObject.pinPercentage = 100 - dataPinned.length + 1 - changedObject.weight = changedObject.pinPercentage - } - - const pinnedRestTotal = pinnedTotal - changedObject.pinPercentage - dataPinned = dataPinned.filter(publisher => publisher.site !== changedPublisher) - dataPinned = normalizePinned(dataPinned, pinnedRestTotal, (100 - changedObject.pinPercentage), setOne) - dataPinned = roundToTarget(dataPinned, (100 - changedObject.pinPercentage), 'pinPercentage') - - dataPinned.push(changedObject) - } else { - dataPinned = normalizePinned(dataPinned, pinnedTotal, 100) - dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') - } - - dataUnPinned = dataUnPinned.map((result) => { - let publisher = getPublisherData(result, scorekeeper) - publisher.percentage = 0 - publisher.weight = 0 - return publisher - }) - - // sync app store - state = ledgerState.changePinnedValues(dataPinned) - } else if (dataUnPinned.length === 0 && pinnedTotal < 100) { - // when you don't have any unpinned sites and pinned total is less then 100 % - dataPinned = normalizePinned(dataPinned, pinnedTotal, 100, false) - dataPinned = roundToTarget(dataPinned, 100, 'pinPercentage') - - // sync app store - state = ledgerState.changePinnedValues(dataPinned) - } else { - // unpinned publishers - dataUnPinned = dataUnPinned.map((result) => { - let publisher = getPublisherData(result, scorekeeper) - const floatNumber = (publisher.score / unPinnedTotal) * (100 - pinnedTotal) - publisher.percentage = Math.round(floatNumber) - publisher.weight = floatNumber - return publisher - }) - - // normalize unpinned values - dataUnPinned = roundToTarget(dataUnPinned, (100 - pinnedTotal), 'percentage') - } - - const newData = dataPinned.concat(dataUnPinned, dataExcluded) - - // sync synopsis - newData.forEach((item) => { - synopsis.publishers[item.site].weight = item.weight - synopsis.publishers[item.site].pinPercentage = item.pinPercentage - }) - - return ledgerState.saveSynopsis(state, newData, options) -} - -// TODO make sure that every call assign state -const updatePublisherInfo = (state, changedPublisher) => { - if (!getSetting(settings.PAYMENTS_ENABLED)) { - return - } - - const options = synopsis.options - state = synopsisNormalizer(state, synopsis.publishers, options, changedPublisher) - - if (publisherInfo._internal.debugP) { - const data = [] - synopsis.publishers.forEach((entry) => { - data.push(underscore.extend(underscore.omit(entry, [ 'faviconURL' ]), { faviconURL: entry.faviconURL && '...' })) - }) - - console.log('\nupdatePublisherInfo: ' + JSON.stringify({ options: options, synopsis: data }, null, 2)) - } - - return state -} - -// TODO rename function name -// TODO make sure that every call assign state -const verifiedP = (state, publisherKey, callback) => { - inspectP(v2PublishersDB, v2PublishersPath, publisherKey, 'verified', null, callback) - - if (process.env.NODE_ENV === 'test') { - ['brianbondy.com', 'clifton.io'].forEach((key) => { - const publisher = ledgerState.getPublisher(state, key) - if (!publisher.isEmpty()) { - state = ledgerState.setSynopsisOption(state, 'verified', true) - } - }) - state = updatePublisherInfo(state) - } - - return state -} - -// TODO refactor -const inspectP = (db, path, publisher, property, key, callback) => { - var done = (err, result) => { - if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && - (synopsis.publishers[publisher].options[property] !== result[property])) { - synopsis.publishers[publisher].options[property] = result[property] - updatePublisherInfo() - } - - if (callback) callback(err, result) - } - - if (!key) key = publisher - db.get(key, (err, value) => { - var result - - if (err) { - if (!err.notFound) console.error(path + ' get ' + key + ' error: ' + JSON.stringify(err, null, 2)) - return done(err) - } - - try { - result = JSON.parse(value) - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid JSON ' + key + ': ' + value) - result = {} - } - - done(null, result) - }) -} - -// TODO refactor -const excludeP = (publisher, callback) => { - var doneP - - var done = (err, result) => { - doneP = true - if ((!err) && (typeof result !== 'undefined') && (!!synopsis.publishers[publisher]) && - (synopsis.publishers[publisher].options.exclude !== result)) { - synopsis.publishers[publisher].options.exclude = result - updatePublisherInfo() - } - - if (callback) callback(err, result) - } - - if (!v2RulesetDB) return setTimeout(() => { excludeP(publisher, callback) }, 5 * miliseconds.second) - - inspectP(v2RulesetDB, v2RulesetPath, publisher, 'exclude', 'domain:' + publisher, (err, result) => { - var props - - if (!err) return done(err, result.exclude) - - props = ledgerPublisher.getPublisherProps('https://' + publisher) - if (!props) return done() - - v2RulesetDB.createReadStream({ lt: 'domain:' }).on('data', (data) => { - var regexp, result, sldP, tldP - - if (doneP) return - - sldP = data.key.indexOf('SLD:') === 0 - tldP = data.key.indexOf('TLD:') === 0 - if ((!tldP) && (!sldP)) return - - if (underscore.intersection(data.key.split(''), - [ '^', '$', '*', '+', '?', '[', '(', '{', '|' ]).length === 0) { - if ((data.key !== ('TLD:' + props.TLD)) && (props.SLD && data.key !== ('SLD:' + props.SLD.split('.')[0]))) return - } else { - try { - regexp = new RegExp(data.key.substr(4)) - if (!regexp.test(props[tldP ? 'TLD' : 'SLD'])) return - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid regexp ' + data.key + ': ' + ex.toString()) - } - } - - try { - result = JSON.parse(data.value) - } catch (ex) { - console.error(v2RulesetPath + ' stream invalid JSON ' + data.entry + ': ' + data.value) - } - - done(null, result.exclude) - }).on('error', (err) => { - console.error(v2RulesetPath + ' stream error: ' + JSON.stringify(err, null, 2)) - }).on('close', () => { - }).on('end', () => { - if (!doneP) done(null, false) - }) - }) -} - -const setLocation = (state, timestamp, tabId) => { - if (!synopsis) { - return - } - - const locationData = ledgerState.getLocation(currentUrl) - if (publisherInfo._internal.verboseP) { - console.log( - `locations[${currentUrl}]=${JSON.stringify(locationData, null, 2)} ` + - `duration=${(timestamp - currentTimestamp)} msec tabId= ${tabId}` - ) - } - if (!locationData || !tabId) { - return state - } - - let publisherKey = locationData.get('publisher') - if (!publisherKey) { - return state - } - - if (!visitsByPublisher[publisherKey]) { - visitsByPublisher[publisherKey] = {} - } - - if (!visitsByPublisher[publisherKey][currentUrl]) { - visitsByPublisher[publisherKey][currentUrl] = { - tabIds: [] - } - } - - const revisitP = visitsByPublisher[publisherKey][currentUrl].tabIds.indexOf(tabId) !== -1 - if (!revisitP) { - visitsByPublisher[publisherKey][currentUrl].tabIds.push(tabId) - } - - let duration = timestamp - currentTimestamp - if (publisherInfo._internal.verboseP) { - console.log('\nadd publisher ' + publisherKey + ': ' + duration + ' msec' + ' revisitP=' + revisitP + ' state=' + - JSON.stringify(underscore.extend({ location: currentUrl }, visitsByPublisher[publisherKey][currentUrl]), - null, 2)) - } - - synopsis.addPublisher(publisherKey, { duration: duration, revisitP: revisitP }) - state = updatePublisherInfo(state) - state = verifiedP(state, publisherKey) - - return state -} - -const addVisit = (state, location, timestamp, tabId) => { - if (location === currentUrl) { - return state - } - - state = setLocation(state, timestamp, tabId) - - currentUrl = location.match(/^about/) ? locationDefault : location - currentTimestamp = timestamp - return state -} - -// TODO refactor -const pageDataChanged = (state) => { - // NB: in theory we have already seen every element in info except for (perhaps) the last one... - const info = pageDataState.getLastInfo(state) - - if (!synopsis || info.isEmpty()) { - return - } - - if (info.get('url', '').match(/^about/)) { - return - } - - let publisher = info.get('publisher') - const location = info.get('key') - if (publisher) { - // TODO refactor - if (synopsis.publishers[publisher] && - (typeof synopsis.publishers[publisher].faviconURL === 'undefined' || synopsis.publishers[publisher].faviconURL === null)) { - getFavIcon(synopsis.publishers[publisher], info, location) - } - - // TODO refactor - return updateLocation(location, publisher) - } else { - try { - publisher = ledgerPublisher.getPublisher(location, publisherInfo._internal.ruleset.raw) - // TODO refactor - if (publisher && !blockedP(state, publisher)) { - state = pageDataState.setPublisher(state, location, publisher) - } else { - publisher = null - } - } catch (ex) { - console.error('getPublisher error for ' + location + ': ' + ex.toString()) - } - } - - if (!publisher) { - return - } - - const pattern = `https?://${publisher}` - const initP = !synopsis.publishers[publisher] - // TODO refactor - synopsis.initPublisher(publisher) - - if (initP) { - // TODO refactor - state = excludeP(state, publisher, (unused, exclude) => { - if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { - exclude = false - } else { - exclude = !exclude - } - appActions.changeSiteSetting(pattern, 'ledgerPayments', exclude) - updatePublisherInfo() - }) - } - // TODO refactor - updateLocation(location, publisher) - // TODO refactor - getFavIcon(synopsis.publishers[publisher], info, location) - - const pageLoad = pageDataState.getLoad(state) - const view = pageDataState.getView(state) - - if (shouldTrackView(view, pageLoad)) { - // TODO refactor - addVisit(view.get('url', 'NOOP'), view.get('timestamp', underscore.now()), view.get('tabId')) - } - - return state -} - -const backupKeys = (state, backupAction) => { - const date = moment().format('L') - const paymentId = state.getIn(['ledgerInfo', 'paymentId']) - const passphrase = state.getIn(['ledgerInfo', 'passphrase']) - - const messageLines = [ - locale.translation('ledgerBackupText1'), - [locale.translation('ledgerBackupText2'), date].join(' '), - '', - [locale.translation('ledgerBackupText3'), paymentId].join(' '), - [locale.translation('ledgerBackupText4'), passphrase].join(' '), - '', - locale.translation('ledgerBackupText5') - ] - - const message = messageLines.join(os.EOL) - const filePath = path.join(electron.app.getPath('userData'), '/brave_wallet_recovery.txt') - - const fs = require('fs') - fs.writeFile(filePath, message, (err) => { - if (err) { - console.error(err) - } else { - tabs.create({url: fileUrl(filePath)}, (webContents) => { - if (backupAction === 'print') { - webContents.print({silent: false, printBackground: false}) - } else { - webContents.downloadURL(fileUrl(filePath), true) - } - }) - } - }) -} - -const recoverKeys = (state, useRecoveryKeyFile, firstKey, secondKey) => { - let firstRecoveryKey, secondRecoveryKey - - if (useRecoveryKeyFile) { - let recoveryKeyFile = promptForRecoveryKeyFile() - if (!recoveryKeyFile) { - // user canceled from dialog, we abort without error - return - } - - if (recoveryKeyFile) { - const result = loadKeysFromBackupFile(state, recoveryKeyFile) - const keys = result.keys || {} - state = result.state - - if (keys) { - firstRecoveryKey = keys.paymentId - secondRecoveryKey = keys.passphrase - } - } - } - - if (!firstRecoveryKey || !secondRecoveryKey) { - firstRecoveryKey = firstKey - secondRecoveryKey = secondKey - } - - const UUID_REGEX = /^[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}$/ - if ( - typeof firstRecoveryKey !== 'string' || - !firstRecoveryKey.match(UUID_REGEX) || - typeof secondRecoveryKey !== 'string' || - !secondRecoveryKey.match(UUID_REGEX) - ) { - // calling logError sets the error object - state = logError(state, true, 'recoverKeys') - state = ledgerState.setRecoveryStatus(state, false) - return state - } - - // TODO should we change this to async await? - // TODO enable when ledger will work again - /* - client.recoverWallet(firstRecoveryKey, secondRecoveryKey, (err, result) => { - let existingLedgerError = ledgerInfo.error - - if (err) { - // we reset ledgerInfo.error to what it was before (likely null) - // if ledgerInfo.error is not null, the wallet info will not display in UI - // logError sets ledgerInfo.error, so we must we clear it or UI will show an error - state = logError(err, 'recoveryWallet') - appActions.updateLedgerInfoProp('error', existingLedgerError) - // appActions.ledgerRecoveryFailed() TODO update based on top comment (async) - } else { - callback(err, result) - - if (balanceTimeoutId) { - clearTimeout(balanceTimeoutId) - } - getBalance() - // appActions.ledgerRecoverySucceeded() TODO update based on top comment (async) - } - }) - */ - - return state + return ( + eligibleByStats && + ( + isInExclusionList !== true || + includeExclude + ) && + ( + (onlyVerified && verifiedPublisher) || + !onlyVerified + ) + ) && + !deletedByUser } -const quit = (state) => { - // quitP = true TODO remove if not needed - state = addVisit(state, locationDefault, new Date().getTime(), null) - - if ((!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { - state = ledgerState.resetSynopsis(state) - } +// TODO rename function +const eligibleP = (state, publisherKey) => { + const scorekeeper = ledgerState.getSynopsisOption(state, 'scorekeeper') + const minPublisherDuration = ledgerState.getSynopsisOption(state, 'minPublisherDuration') + const minPublisherVisits = ledgerState.getSynopsisOption(state, 'minPublisherVisits') + const publisher = ledgerState.getPublisher(state, publisherKey) - return state + return ( + publisher.getIn(['scores', scorekeeper]) > 0 && + publisher.get('duration') >= minPublisherDuration && + publisher.get('visits') >= minPublisherVisits + ) } -const initSynopsis = (state) => { - // cf., the `Synopsis` constructor, https://github.com/brave/ledger-publisher/blob/master/index.js#L167 - let value = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) - if (!value) { - value = 8 * 1000 - appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME, value) - } - - // for earlier versions of the code... - if ((value > 0) && (value < 1000)) { - value = value * 1000 - synopsis.options.minPublisherDuration = value - state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) - } - - value = getSetting(settings.PAYMENTS_MINIMUM_VISITS) - if (!value) { - value = 1 - appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISITS, value) - } - - if (value > 0) { - synopsis.options.minPublisherVisits = value - state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) - } +// TODO rename function +const stickyP = (state, publisherKey) => { + const pattern = urlUtil.getHostPattern(publisherKey) + let result = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPayments') - if (process.env.NODE_ENV === 'test') { - synopsis.options.minPublisherDuration = 0 - synopsis.options.minPublisherVisits = 0 - state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', 0) - state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', 0) - } else { - if (process.env.LEDGER_PUBLISHER_MIN_DURATION) { - value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_DURATION) - synopsis.options.minPublisherDuration = value - state = ledgerState.setSynopsisOption(state, 'minPublisherDuration', value) - } - if (process.env.LEDGER_PUBLISHER_MIN_VISITS) { - value = ledgerClient.prototype.numbion(process.env.LEDGER_PUBLISHER_MIN_VISITS) - synopsis.options.minPublisherVisits = value - state = ledgerState.setSynopsisOption(state, 'minPublisherVisits', value) - } - } - - underscore.keys(synopsis.publishers).forEach((publisher) => { - excludeP(publisher) - state = verifiedP(state, publisher) - }) - - state = updatePublisherInfo(state) - - return state -} - -const enable = (state, paymentsEnabled) => { - if (paymentsEnabled && !getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) - } - - publisherInfo._internal.enabled = paymentsEnabled - if (synopsis) { - return updatePublisherInfo(state) - } - - if (!ledgerPublisher) { - ledgerPublisher = require('ledger-publisher') - } - synopsis = new (ledgerPublisher.Synopsis)() - const stateSynopsis = ledgerState.getSynopsis(state) - - if (publisherInfo._internal.verboseP) { - console.log('\nstarting up ledger publisher integration') - } - - if (stateSynopsis.isEmpty()) { - return initSynopsis(state) - } - - try { - synopsis = new (ledgerPublisher.Synopsis)(stateSynopsis) - } catch (ex) { - console.error('synopsisPath parse error: ' + ex.toString()) - } - - state = initSynopsis(state) - - // synopsis cleanup - underscore.keys(synopsis.publishers).forEach((publisher) => { - if (synopsis.publishers[publisher].faviconURL === null) { - delete synopsis.publishers[publisher].faviconURL - } - }) - - // change undefined include publishers to include publishers - state = ledgerState.enableUndefinedPublishers(state, stateSynopsis.get('publishers')) - - return state -} - -const pathName = (name) => { - const parts = path.parse(name) - return path.join(electron.app.getPath('userData'), parts.name + parts.ext) -} - -const sufficientBalanceToReconcile = (state) => { - const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) - const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0) - const btc = ledgerState.getInfoProp(state, 'btc') - return btc && (balance + unconfirmed > 0.9 * Number(btc)) -} - -const shouldShowNotificationReviewPublishers = () => { - const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP) - return !nextTime || (underscore.now() > nextTime) -} - -const shouldShowNotificationAddFunds = () => { - const nextTime = getSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP) - return !nextTime || (underscore.now() > nextTime) -} - -const showNotificationReviewPublishers = (nextTime) => { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) - - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: locale.translation('reconciliationNotification'), - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('dismiss')}, - {text: locale.translation('reviewSites'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} - -const showNotificationAddFunds = () => { - const nextTime = underscore.now() + (3 * miliseconds.day) - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) - - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: locale.translation('addFundsNotification'), - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('updateLater')}, - {text: locale.translation('addFunds'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} - -/** - * Show message that it's time to add funds if reconciliation is less than - * a day in the future and balance is too low. - * 24 hours prior to reconciliation, show message asking user to review - * their votes. - */ -const showEnabledNotifications = (state) => { - const reconcileStamp = ledgerState.getInfoProp(state, 'reconcileStamp') - - if (!reconcileStamp) { - return - } - - if (reconcileStamp - new Date().getTime() < miliseconds.day) { - if (sufficientBalanceToReconcile(state)) { - if (shouldShowNotificationReviewPublishers()) { - const reconcileFrequency = ledgerState.getInfoProp(state, 'reconcileFrequency') - showNotificationReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day)) - } - } else if (shouldShowNotificationAddFunds()) { - showNotificationAddFunds() - } - } else if (reconcileStamp - underscore.now() < 2 * miliseconds.day) { - if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { - showNotificationReviewPublishers(underscore.now() + miliseconds.day) - } - } -} - -const showDisabledNotifications = (state) => { - if (!getSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED)) { - const firstRunTimestamp = state.get('firstRunTimestamp') - if (new Date().getTime() - firstRunTimestamp < appConfig.payments.delayNotificationTryPayments) { - return - } - - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: locale.translation('notificationTryPayments'), - buttons: [ - {text: locale.translation('noThanks')}, - {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) - } -} - -const showNotifications = (state) => { - if (getSetting(settings.PAYMENTS_ENABLED)) { - if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { - showEnabledNotifications(state) - } - } else { - showDisabledNotifications(state) - } -} - -const cacheRuleSet = (state, ruleset) => { - if ((!ruleset) || (underscore.isEqual(publisherInfo._internal.ruleset.raw, ruleset))) return - - try { - let stewed = [] - ruleset.forEach((rule) => { - let entry = { condition: acorn.parse(rule.condition) } - - if (rule.dom) { - if (rule.dom.publisher) { - entry.publisher = { selector: rule.dom.publisher.nodeSelector, - consequent: acorn.parse(rule.dom.publisher.consequent) - } - } - if (rule.dom.faviconURL) { - entry.faviconURL = { selector: rule.dom.faviconURL.nodeSelector, - consequent: acorn.parse(rule.dom.faviconURL.consequent) - } - } - } - if (!entry.publisher) entry.consequent = rule.consequent ? acorn.parse(rule.consequent) : rule.consequent - - stewed.push(entry) - }) - - publisherInfo._internal.ruleset.raw = ruleset - publisherInfo._internal.ruleset.cooked = stewed - if (!synopsis) { - return - } - - let syncP = false - ledgerState.getPublishers(state).forEach((publisher, index) => { - const location = (publisher.get('protocol') || 'http:') + '//' + index - let ctx = urlParse(location, true) - - ctx.TLD = tldjs.getPublicSuffix(ctx.host) - if (!ctx.TLD) return - - ctx = underscore.mapObject(ctx, function (value, key) { if (!underscore.isFunction(value)) return value }) - ctx.URL = location - ctx.SLD = tldjs.getDomain(ctx.host) - ctx.RLD = tldjs.getSubdomain(ctx.host) - ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' - - stewed.forEach((rule) => { - if ((rule.consequent !== null) || (rule.dom)) return - if (!ruleSolver.resolve(rule.condition, ctx)) return - - if (publisherInfo._internal.verboseP) console.log('\npurging ' + index) - delete synopsis.publishers[publisher] - state = ledgerState.deletePublishers(state, index) - syncP = true - }) - }) - - if (!syncP) { - return - } - - return updatePublisherInfo(state) - } catch (ex) { - console.error('ruleset error: ', ex) - return state - } -} - -const clientprep = () => { - if (!ledgerClient) ledgerClient = require('ledger-client') - ledgerInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_CLIENT_DEBUG) - publisherInfo._internal.debugP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_DEBUG) - publisherInfo._internal.verboseP = ledgerClient.prototype.boolion(process.env.LEDGER_PUBLISHER_VERBOSE) -} - -const roundtrip = (params, options, callback) => { - let parts = typeof params.server === 'string' ? urlParse(params.server) - : typeof params.server !== 'undefined' ? params.server - : typeof options.server === 'string' ? urlParse(options.server) : options.server - const rawP = options.rawP - - if (!params.method) params.method = 'GET' - parts = underscore.extend(underscore.pick(parts, [ 'protocol', 'hostname', 'port' ]), - underscore.omit(params, [ 'headers', 'payload', 'timeout' ])) - -// TBD: let the user configure this via preferences [MTR] - if ((parts.hostname === 'ledger.brave.com') && (params.useProxy)) parts.hostname = 'ledger-proxy.privateinternetaccess.com' - - const i = parts.path.indexOf('?') - if (i !== -1) { - parts.pathname = parts.path.substring(0, i) - parts.search = parts.path.substring(i) - } else { - parts.pathname = parts.path - } - - options = { - url: urlFormat(parts), - method: params.method, - payload: params.payload, - responseType: 'text', - headers: underscore.defaults(params.headers || {}, { 'content-type': 'application/json; charset=utf-8' }), - verboseP: options.verboseP - } - request.request(options, (err, response, body) => { - let payload - - if ((response) && (options.verboseP)) { - console.log('[ response for ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path + ' ]') - console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + - ' ' + (response.statusMessage || '')) - underscore.keys(response.headers).forEach((header) => { console.log('>>> ' + header + ': ' + response.headers[header]) }) - console.log('>>>') - console.log('>>> ' + (body || '').split('\n').join('\n>>> ')) - } - - if (err) return callback(err) - - if (Math.floor(response.statusCode / 100) !== 2) { - return callback(new Error('HTTP response ' + response.statusCode) + ' for ' + params.method + ' ' + params.path) - } - - try { - payload = rawP ? body : (response.statusCode !== 204) ? JSON.parse(body) : null - } catch (err) { - return callback(err) - } - - try { - callback(null, response, payload) - } catch (err0) { - if (options.verboseP) console.log('\ncallback: ' + err0.toString() + '\n' + err0.stack) - } - }) - - if (!options.verboseP) return - - console.log('<<< ' + params.method + ' ' + parts.protocol + '//' + parts.hostname + params.path) - underscore.keys(options.headers).forEach((header) => { console.log('<<< ' + header + ': ' + options.headers[header]) }) - console.log('<<<') - if (options.payload) console.log('<<< ' + JSON.stringify(params.payload, null, 2).split('\n').join('\n<<< ')) -} - -const updateLedgerInfo = (state) => { - const info = ledgerInfo._internal.paymentInfo - const now = underscore.now() - - // TODO check if we can have internal info in the state already - if (info) { - underscore.extend(ledgerInfo, - underscore.pick(info, [ 'address', 'passphrase', 'balance', 'unconfirmed', 'satoshis', 'btc', 'amount', - 'currency' ])) - if ((!info.buyURLExpires) || (info.buyURLExpires > now)) { - ledgerInfo.buyURL = info.buyURL - ledgerInfo.buyMaximumUSD = 6 - } - if (typeof process.env.ADDFUNDS_URL !== 'undefined') { - ledgerInfo.buyURLFrame = true - ledgerInfo.buyURL = process.env.ADDFUNDS_URL + '?' + - queryString.stringify({ currency: ledgerInfo.currency, - amount: getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT), - address: ledgerInfo.address }) - ledgerInfo.buyMaximumUSD = false - } - - underscore.extend(ledgerInfo, ledgerInfo._internal.cache || {}) - } - - // TODO we don't need this for BAT - /* - if ((client) && (now > ledgerInfo._internal.geoipExpiry)) { - ledgerInfo._internal.geoipExpiry = now + (5 * miliseconds.minute) - - if (!ledgerGeoIP) ledgerGeoIP = require('ledger-geoip') - return ledgerGeoIP.getGeoIP(client.options, (err, provider, result) => { - if (err) console.warn('ledger geoip warning: ' + JSON.stringify(err, null, 2)) - if (result) ledgerInfo.countryCode = result - - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - - if (now <= ledgerInfo._internal.exchangeExpiry) return updateLedgerInfo() - - ledgerInfo._internal.exchangeExpiry = now + miliseconds.day - roundtrip({ path: '/v1/exchange/providers' }, client.options, (err, response, body) => { - if (err) console.error('ledger exchange error: ' + JSON.stringify(err, null, 2)) - - ledgerInfo._internal.exchanges = body || {} - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - updateLedgerInfo() - }) - }) - } - */ - - if (ledgerInfo._internal.debugP) { - console.log('\nupdateLedgerInfo: ' + JSON.stringify(underscore.omit(ledgerInfo, [ '_internal' ]), null, 2)) - } - - return ledgerState.mergeInfoProp(state, underscore.omit(ledgerInfo, [ '_internal' ])) -} - -// Called from observeTransactions() when we see a new payment (transaction). -const showNotificationPaymentDone = (transactionContributionFiat) => { - const notificationPaymentDoneMessage = locale.translation('notificationPaymentDone') - .replace(/{{\s*amount\s*}}/, transactionContributionFiat.amount) - .replace(/{{\s*currency\s*}}/, transactionContributionFiat.currency) - // Hide the 'waiting for deposit' message box if it exists - appActions.hideNotification(locale.translation('addFundsNotification')) - appActions.showNotification({ - greeting: locale.translation('updateHello'), - message: notificationPaymentDoneMessage, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('Ok'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) -} - -const observeTransactions = (state, transactions) => { - const current = ledgerState.getInfoProp(state, 'transactions') - if (underscore.isEqual(current, transactions)) { - return - } - // Notify the user of new transactions. - if (getSetting(settings.PAYMENTS_NOTIFICATIONS) && current !== null) { - const newTransactions = underscore.difference(transactions, current) - if (newTransactions.length > 0) { - const newestTransaction = newTransactions[newTransactions.length - 1] - showNotificationPaymentDone(newestTransaction.contribution.fiat) - } - } -} - -const getStateInfo = (state, parsedData) => { - const info = parsedData.paymentInfo - const then = underscore.now() - miliseconds.year - - if (!parsedData.properties.wallet) { - return - } - - const newInfo = { - paymentId: parsedData.properties.wallet.paymentId, - passphrase: parsedData.properties.wallet.keychains.passphrase, - created: !!parsedData.properties.wallet, - creating: !parsedData.properties.wallet, - reconcileFrequency: parsedData.properties.days, - reconcileStamp: parsedData.reconcileStamp - } - - state = ledgerState.mergeInfoProp(state, newInfo) - - if (info) { - ledgerInfo._internal.paymentInfo = info // TODO check if we can just save this into the state - const paymentURL = 'bitcoin:' + info.address + '?amount=' + info.btc + '&label=' + encodeURI('Brave Software') - const oldUrl = ledgerState.getInfoProp(state, 'paymentURL') - if (oldUrl !== paymentURL) { - state = ledgerState.setInfoProp(state, 'paymentURL', paymentURL) - try { - let chunks = [] - qr.image(paymentURL, { type: 'png' }) - .on('data', (chunk) => { chunks.push(chunk) }) - .on('end', () => { - const paymentIMG = 'data:image/png;base64,' + Buffer.concat(chunks).toString('base64') - state = ledgerState.setInfoProp(state, 'paymentIMG', paymentIMG) - }) - } catch (ex) { - console.error('qr.imageSync error: ' + ex.toString()) - } - } - } - - let transactions = [] - if (!parsedData.transactions) { - return updateLedgerInfo(state) - } - - for (let i = parsedData.transactions.length - 1; i >= 0; i--) { - let transaction = parsedData.transactions[i] - if (transaction.stamp < then) break - - if ((!transaction.ballots) || (transaction.ballots.length < transaction.count)) continue - - let ballots = underscore.clone(transaction.ballots || {}) - parsedData.ballots.forEach((ballot) => { - if (ballot.viewingId !== transaction.viewingId) return - - if (!ballots[ballot.publisher]) ballots[ballot.publisher] = 0 - ballots[ballot.publisher]++ - }) - - transactions.push(underscore.extend(underscore.pick(transaction, - [ 'viewingId', 'contribution', 'submissionStamp', 'count' ]), - { ballots: ballots })) - } - - observeTransactions(state, transactions) - state = ledgerState.setInfoProp(state, 'transactions', transactions) - return updateLedgerInfo(state) -} - -// TODO refactor when action is added -/* -var getPaymentInfo = () => { - var amount, currency - - if (!client) return - - try { - ledgerInfo.bravery = client.getBraveryProperties() - if (ledgerInfo.bravery.fee) { - amount = ledgerInfo.bravery.fee.amount - currency = ledgerInfo.bravery.fee.currency - } - - client.getWalletProperties(amount, currency, function (err, body) { - var info = ledgerInfo._internal.paymentInfo || {} - - if (logError(err, 'getWalletProperties')) { - return - } - - info = underscore.extend(info, underscore.pick(body, [ 'buyURL', 'buyURLExpires', 'balance', 'unconfirmed', 'satoshis' ])) - info.address = client.getWalletAddress() - if ((amount) && (currency)) { - info = underscore.extend(info, { amount: amount, currency: currency }) - if ((body.rates) && (body.rates[currency])) { - info.btc = (amount / body.rates[currency]).toFixed(8) - } - } - ledgerInfo._internal.paymentInfo = info - updateLedgerInfo() - cacheReturnValue() - }) - } catch (ex) { - console.error('properties error: ' + ex.toString()) - } -} -*/ - -const setPaymentInfo = (amount) => { - var bravery - - if (!client) return - - try { - bravery = client.getBraveryProperties() - } catch (ex) { - // wallet being created... - return setTimeout(function () { setPaymentInfo(amount) }, 2 * miliseconds.second) - } - - amount = parseInt(amount, 10) - if (isNaN(amount) || (amount <= 0)) return - - underscore.extend(bravery.fee, { amount: amount }) - client.setBraveryProperties(bravery, (err, result) => { - if (ledgerInfo.created) { - // getPaymentInfo() TODO create action for this - } - - if (err) return console.error('ledger setBraveryProperties: ' + err.toString()) - - if (result) { - muonWriter(pathName(statePath), result) - // TODO save this new data to appState - } - }) -} - -let balanceTimeoutId = false -const getBalance = (state) => { - if (!client) return - - balanceTimeoutId = setTimeout(getBalance, 1 * miliseconds.minute) - if (!ledgerState.getInfoProp(state, 'address')) { - return - } - - if (!ledgerBalance) ledgerBalance = require('ledger-balance') - ledgerBalance.getBalance(ledgerInfo.address, underscore.extend({ balancesP: true }, client.options), - (err, provider, result) => { - // TODO create action to handle callback - if (err) { - return console.warn('ledger balance warning: ' + JSON.stringify(err, null, 2)) - } - /* - var unconfirmed - var info = ledgerInfo._internal.paymentInfo - - if (typeof result.unconfirmed === 'undefined') return - - if (result.unconfirmed > 0) { - unconfirmed = (result.unconfirmed / 1e8).toFixed(4) - if ((info || ledgerInfo).unconfirmed === unconfirmed) return - - ledgerInfo.unconfirmed = unconfirmed - if (info) info.unconfirmed = ledgerInfo.unconfirmed - if (clientOptions.verboseP) console.log('\ngetBalance refreshes ledger info: ' + ledgerInfo.unconfirmed) - return updateLedgerInfo() - } - - if (ledgerInfo.unconfirmed === '0.0000') return - - if (clientOptions.verboseP) console.log('\ngetBalance refreshes payment info') - getPaymentInfo() - */ - }) -} - -// TODO -const callback = (err, result, delayTime) => { - /* - var results - var entries = client && client.report() - - if (clientOptions.verboseP) { - console.log('\nledger client callback: clientP=' + (!!client) + ' errP=' + (!!err) + ' resultP=' + (!!result) + - ' delayTime=' + delayTime) - } - - if (err) { - console.log('ledger client error(1): ' + JSON.stringify(err, null, 2) + (err.stack ? ('\n' + err.stack) : '')) - if (!client) return - - if (typeof delayTime === 'undefined') delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) - } - - if (!result) return run(delayTime) - - if ((client) && (result.properties.wallet)) { - if (!ledgerInfo.created) setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) - - getStateInfo(result) - getPaymentInfo() - } - cacheRuleSet(result.ruleset) - if (result.rulesetV2) { - results = result.rulesetV2 - delete result.rulesetV2 - - entries = [] - results.forEach((entry) => { - var key = entry.facet + ':' + entry.publisher - - if (entry.exclude !== false) { - entries.push({ type: 'put', key: key, value: JSON.stringify(underscore.omit(entry, [ 'facet', 'publisher' ])) }) - } else { - entries.push({ type: 'del', key: key }) - } - }) - - v2RulesetDB.batch(entries, (err) => { - if (err) return console.error(v2RulesetPath + ' error: ' + JSON.stringify(err, null, 2)) - - if (entries.length === 0) return - - underscore.keys(synopsis.publishers).forEach((publisher) => { -// be safe... - if (synopsis.publishers[publisher]) delete synopsis.publishers[publisher].options.exclude - - excludeP(publisher) - }) - }) - } - if (result.publishersV2) { - results = result.publishersV2 - delete result.publishersV2 - - entries = [] - results.forEach((entry) => { - entries.push({ type: 'put', - key: entry.publisher, - value: JSON.stringify(underscore.omit(entry, [ 'publisher' ])) - }) - if ((synopsis.publishers[entry.publisher]) && - (synopsis.publishers[entry.publisher].options.verified !== entry.verified)) { - synopsis.publishers[entry.publisher].options.verified = entry.verified - updatePublisherInfo() - } - }) - v2PublishersDB.batch(entries, (err) => { - if (err) return console.error(v2PublishersPath + ' error: ' + JSON.stringify(err, null, 2)) - }) - } - - muonWriter(pathName(statePath), result) - run(delayTime) - */ -} - -const initialize = (state, paymentsEnabled) => { - if (!v2RulesetDB) v2RulesetDB = levelUp(pathName(v2RulesetPath)) - if (!v2PublishersDB) v2PublishersDB = levelUp(pathName(v2PublishersPath)) - state = enable(state, paymentsEnabled) - - // Check if relevant browser notifications should be shown every 15 minutes - if (notificationTimeout) { - clearInterval(notificationTimeout) - } - notificationTimeout = setInterval((state) => { - showNotifications(state) - }, 15 * miliseconds.minute, state) - - if (!paymentsEnabled) { - client = null - return ledgerState.resetInfo(state) - } - - if (client) { - return - } - - if (!ledgerPublisher) ledgerPublisher = require('ledger-publisher') - let ruleset = [] - ledgerPublisher.ruleset.forEach(rule => { if (rule.consequent) ruleset.push(rule) }) - state = cacheRuleSet(state, ruleset) - - try { - const fs = require('fs') - fs.accessSync(pathName(statePath), fs.FF_OK) - const data = fs.readFileSync(pathName(statePath)) - let parsedData - - try { - parsedData = JSON.parse(data) - if (clientOptions.verboseP) { - console.log('\nstarting up ledger client integration') - } - } catch (ex) { - console.error('statePath parse error: ' + ex.toString()) - return state - } - - state = getStateInfo(state, parsedData) - - try { - let timeUntilReconcile - clientprep() - client = ledgerClient(parsedData.personaId, - underscore.extend(parsedData.options, { roundtrip: roundtrip }, clientOptions), - parsedData) - - // Scenario: User enables Payments, disables it, waits 30+ days, then - // enables it again -> reconcileStamp is in the past. - // In this case reset reconcileStamp to the future. - try { timeUntilReconcile = client.timeUntilReconcile() } catch (ex) {} - let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') - if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { - client.setTimeUntilReconcile(null, (err, stateResult) => { - if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) - - if (!stateResult) { - return - } - state = getStateInfo(stateResult) - - muonWriter(pathName(statePath), stateResult) - }) - } - } catch (ex) { - return console.error('ledger client creation error: ', ex) - } - - // speed-up browser start-up by delaying the first synchronization action - // TODO create new action that is triggered after 3s - /* - setTimeout(() => { - if (!client) return - - if (client.sync(callback) === true) run(random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute })) - state = cacheRuleSet(state, parsedData.ruleset) - }, 3 * miliseconds.second) - */ - - // Make sure bravery props are up-to-date with user settings - const address = ledgerState.getInfoProp(state, 'address') - if (!address) { - state = ledgerState.setInfoProp(state, 'address', client.getWalletAddress()) - } - - setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) - getBalance(state) - - return state - } catch (err) { - if (err.code !== 'ENOENT') { - console.error('statePath read error: ' + err.toString()) - } - state = ledgerState.resetInfo(state) - return state - } -} - -const init = (state) => { - try { - state = initialize(state, getSetting(settings.PAYMENTS_ENABLED)) - } catch (ex) { - console.error('ledger.js initialization failed: ', ex) - } - - return state -} - -// TODO rename -const contributeP = (state, publisherKey) => { - const publisher = ledgerState.getPublisher(state, publisherKey) - return ( - (stickyP(state, publisherKey) || publisher.getIn(['options', 'exclude']) !== true) && - eligibleP(state, publisherKey) && - !blockedP(state, publisherKey) - ) -} - -const run = (delayTime) => { - // TODO implement - /* - if (clientOptions.verboseP) { - var entries - - console.log('\nledger client run: clientP=' + (!!client) + ' delayTime=' + delayTime) - - var line = (fields) => { - var result = '' - - fields.forEach((field) => { - var spaces - var max = (result.length > 0) ? 9 : 19 - - if (typeof field !== 'string') field = field.toString() - if (field.length < max) { - spaces = ' '.repeat(max - field.length) - field = spaces + field - } else { - field = field.substr(0, max) - } - result += ' ' + field - }) - - console.log(result.substr(1)) - } - - line([ 'publisher', - 'blockedP', 'stickyP', 'verified', - 'excluded', 'eligibleP', 'visibleP', - 'contribP', - 'duration', 'visits' - ]) - entries = synopsis.topN() || [] - entries.forEach((entry) => { - var publisher = entry.publisher - - line([ publisher, - blockedP(publisher), stickyP(publisher), synopsis.publishers[publisher].options.verified === true, - synopsis.publishers[publisher].options.exclude === true, eligibleP(publisher), visibleP(publisher), - contributeP(publisher), - Math.round(synopsis.publishers[publisher].duration / 1000), synopsis.publishers[publisher].visits ]) - }) - } - - if ((typeof delayTime === 'undefined') || (!client)) return - - var active, state, weights, winners - var ballots = client.ballots() - var data = (synopsis) && (ballots > 0) && synopsisNormalizer() - - if (data) { - weights = [] - data.forEach((datum) => { weights.push({ publisher: datum.site, weight: datum.weight / 100.0 }) }) - winners = synopsis.winners(ballots, weights) - } - if (!winners) winners = [] - - try { - winners.forEach((winner) => { - var result - - if (!contributeP(winner)) return - - result = client.vote(winner) - if (result) state = result - }) - if (state) muonWriter(pathName(statePath), state) - } catch (ex) { - console.log('ledger client error(2): ' + ex.toString() + (ex.stack ? ('\n' + ex.stack) : '')) - } - - if (delayTime === 0) { - try { - delayTime = client.timeUntilReconcile() - } catch (ex) { - delayTime = false - } - if (delayTime === false) delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) - } - if (delayTime > 0) { - if (runTimeoutId) return - - active = client - if (delayTime > (1 * miliseconds.hour)) delayTime = random.randomInt({ min: 3 * miliseconds.minute, max: miliseconds.hour }) - - runTimeoutId = setTimeout(() => { - runTimeoutId = false - if (active !== client) return - - if (!client) return console.log('\n\n*** MTR says this can\'t happen(1)... please tell him that he\'s wrong!\n\n') - - if (client.sync(callback) === true) return run(0) - }, delayTime) - return - } - - if (client.isReadyToReconcile()) return client.reconcile(uuid.v4().toLowerCase(), callback) - - console.log('what? wait, how can this happen?') - */ -} - -const networkConnected = (state) => { - // TODO pass state into debounced function - underscore.debounce((state) => { - if (!client) return - - if (runTimeoutId) { - clearTimeout(runTimeoutId) - runTimeoutId = false - } - if (client.sync(callback) === true) { - // TODO refactor - const delayTime = random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute }) - run(state, delayTime) - } - - if (balanceTimeoutId) clearTimeout(balanceTimeoutId) - balanceTimeoutId = setTimeout(getBalance, 5 * miliseconds.second) - }, 1 * miliseconds.minute, true) -} - -// TODO check if quitP is needed, now is defined in ledgerUtil.quit -const muonWriter = (path, payload) => { - muon.file.writeImportant(path, JSON.stringify(payload, null, 2), (success) => { - if (!success) return console.error('write error: ' + path) - - if ((quitP) && (!getSetting(settings.PAYMENTS_ENABLED)) && (getSetting(settings.SHUTDOWN_CLEAR_HISTORY))) { - if (ledgerInfo._internal.debugP) { - console.log('\ndeleting ' + path) - } - - const fs = require('fs') - return fs.unlink(path, (err) => { if (err) console.error('unlink error: ' + err.toString()) }) - } - - if (ledgerInfo._internal.debugP) console.log('\nwrote ' + path) - }) + return (result === undefined || result) } module.exports = { - synopsis, shouldTrackView, btcToCurrencyString, formattedTimeFromNow, formattedDateFromTimestamp, walletStatus, - backupKeys, - recoverKeys, - quit, - addVisit, - pageDataChanged, - init, - initialize, - setPaymentInfo, - updatePublisherInfo, - networkConnected, - verifiedP + blockedP, + contributeP, + visibleP, + eligibleP, + stickyP } diff --git a/app/common/lib/publisherUtil.js b/app/common/lib/publisherUtil.js index 0988cdd922f..c0ae2cbefd9 100644 --- a/app/common/lib/publisherUtil.js +++ b/app/common/lib/publisherUtil.js @@ -2,65 +2,22 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -const Immutable = require('immutable') - // Constants const settings = require('../../../js/constants/settings') -const siteSettingsState = require('../state/siteSettingsState') // Utils +const ledgerUtil = require('./ledgerUtil') const {getSetting} = require('../../../js/settings') const {isHttpOrHttps} = require('../../../js/lib/urlutil') const {isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') -const visiblePublisher = (state, publisherId) => { - if (publisherId == null) { - return true - } - - // ledgerPaymentsShown is undefined by default until - // user decide to permanently hide the publisher, - // so for icon to be shown it can be everything but false - const hostSettings = siteSettingsState.getSettingsByHost(state, publisherId) - const ledgerPaymentsShown = hostSettings && hostSettings.get('ledgerPaymentsShown') - - return typeof ledgerPaymentsShown === 'boolean' - ? ledgerPaymentsShown - : true -} - const publisherState = { - enabledForPaymentsPublisher: (state, locationId) => { - const locationInfo = state.get('locationInfo', Immutable.Map()) - const publisherId = locationInfo.getIn([locationId, 'publisher']) - - const synopsis = state.getIn(['publisherInfo', 'synopsis'], Immutable.Map()) - const hostSettings = siteSettingsState.getSettingsByHost(state, publisherId) - - // All publishers will be enabled by default if AUTO_SUGGEST is ON, - // excluding publishers defined on ledger's exclusion list - const excluded = locationInfo.getIn([locationId, 'exclude']) - const autoSuggestSites = getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST) - - // If session is clear then siteSettings is undefined and icon - // will never be shown, but synopsis may not be empty. - // In such cases let's check if synopsis matches current publisherId - const isValidPublisherSynopsis = !!synopsis.map(entry => entry.get('site')) - .includes(publisherId) - - // hostSettings is undefined until user hit addFunds button. - // For such cases check autoSuggestSites for eligibility. - return hostSettings - ? hostSettings.get('ledgerPayments') !== false - : isValidPublisherSynopsis || (autoSuggestSites && !excluded) - }, - shouldShowAddPublisherButton: (state, location, publisherId) => { return location && !isSourceAboutUrl(location) && getSetting(settings.PAYMENTS_ENABLED) && isHttpOrHttps(location) && - visiblePublisher(state, publisherId) + !ledgerUtil.blockedP(state, publisherId) } } diff --git a/app/common/state/ledgerState.js b/app/common/state/ledgerState.js index 2b51a532c27..3cbf4a5d31f 100644 --- a/app/common/state/ledgerState.js +++ b/app/common/state/ledgerState.js @@ -3,63 +3,106 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const Immutable = require('immutable') +const assert = require('assert') + +// State +const pageDataState = require('./pageDataState') // Utils const siteSettings = require('../../../js/state/siteSettings') -const {makeImmutable} = require('../../common/state/immutableUtil') +const urlUtil = require('../../../js/lib/urlutil') +const {makeImmutable, isMap} = require('../../common/state/immutableUtil') + +const validateState = function (state) { + state = makeImmutable(state) + assert.ok(isMap(state), 'state must be an Immutable.Map') + assert.ok(isMap(state.get('ledger')), 'state must contain an Immutable.Map of ledger') + return state +} const ledgerState = { - setRecoveryStatus: (state, status) => { - const date = new Date().getTime() - state = state.setIn(['about', 'preferences', 'recoverySucceeded'], status) - return state.setIn(['about', 'preferences', 'updatedStamp'], date) + /** + * LOCATIONS + */ + getLocation: (state, url) => { + state = validateState(state) + if (url == null) { + return Immutable.Map() + } + + return state.getIn(['ledger', 'locations', url]) || Immutable.Map() }, - setLedgerError: (state, error, caller) => { - if (error == null && caller == null) { - return state.setIn(['ledger', 'info', 'error'], null) + setLocationProp: (state, url, prop, value) => { + state = validateState(state) + if (url == null || prop == null) { + return state } - return state - .setIn(['ledger', 'info', 'error', 'caller'], caller) - .setIn(['ledger', 'info', 'error', 'error'], error) + return state.setIn(['ledger', 'locations', url, prop], value) }, - getLocation: (state, url) => { - if (url == null) { + getLocationProp: (state, url, prop) => { + state = validateState(state) + if (url == null || prop == null) { return null } - return state.getIn(['ledger', 'locations', url]) + return state.getIn(['ledger', 'locations', url, prop]) }, - changePinnedValues: (state, publishers) => { - if (publishers == null) { - return state + getLocationPublisher: (state, url) => { + state = validateState(state) + if (url == null) { + return Immutable.Map() } - publishers = makeImmutable(publishers) - publishers.forEach((item, index) => { - const pattern = `https?://${index}` - const percentage = item.get('pinPercentage') - let newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPinPercentage', percentage) - state = state.set('siteSettings', newSiteSettings) - }) - - return state + return state.getIn(['ledger', 'locations', url]) }, + /** + * SYNOPSIS + */ getSynopsis: (state) => { + state = validateState(state) return state.getIn(['ledger', 'synopsis']) || Immutable.Map() }, saveSynopsis: (state, publishers, options) => { + state = validateState(state) + if (options != null) { + state = state.setIn(['ledger', 'synopsis', 'options'], makeImmutable(options)) + } + + if (publishers != null) { + state = state.setIn(['ledger', 'synopsis', 'publishers'], makeImmutable(publishers)) + } + return state - .setIn(['ledger', 'synopsis', 'publishers'], publishers) - .setIn(['ledger', 'synopsis', 'options'], options) }, + resetSynopsis: (state, options = false) => { + state = validateState(state) + + if (options) { + state = state + .setIn(['ledger', 'synopsis', 'options'], Immutable.Map()) + .setIn(['ledger', 'about', 'synopsisOptions'], Immutable.Map()) + } + + state = pageDataState.resetPageData(state) + + return state + .setIn(['ledger', 'synopsis', 'publishers'], Immutable.Map()) + .setIn(['ledger', 'locations'], Immutable.Map()) + .setIn(['ledger', 'about', 'synopsis'], Immutable.List()) + }, + + /** + * SYNOPSIS / PUBLISHERS + */ getPublisher: (state, key) => { + state = validateState(state) if (key == null) { return Immutable.Map() } @@ -68,53 +111,120 @@ const ledgerState = { }, getPublishers: (state) => { + state = validateState(state) return state.getIn(['ledger', 'synopsis', 'publishers']) || Immutable.Map() }, + hasPublisher: (state, key) => { + state = validateState(state) + if (key == null) { + return false + } + + return state.hasIn(['ledger', 'synopsis', 'publishers', key]) + }, + + setPublisher: (state, key, value) => { + state = validateState(state) + if (key == null || value == null) { + return state + } + + value = makeImmutable(value) + return state.setIn(['ledger', 'synopsis', 'publishers', key], value) + }, + deletePublishers: (state, key) => { + state = validateState(state) + + if (key == null) { + return state + } + return state.deleteIn(['ledger', 'synopsis', 'publishers', key]) }, + setPublishersProp: (state, key, prop, value) => { + state = validateState(state) + + if (key == null || prop == null) { + return state + } + + return state.setIn(['ledger', 'synopsis', 'publishers', key, prop], value) + }, + + /** + * SYNOPSIS / PUBLISHER / OPTIONS + */ + setPublisherOption: (state, key, prop, value) => { + state = validateState(state) + + if (key == null || prop == null) { + return state + } + + return state.setIn(['ledger', 'synopsis', 'publishers', key, 'options', prop], value) + }, + + getPublisherOption: (state, key, prop) => { + state = validateState(state) + + if (key == null || prop == null) { + return state + } + + return state.getIn(['ledger', 'synopsis', 'publishers', key, 'options', prop]) + }, + + /** + * SYNOPSIS / OPTIONS + */ getSynopsisOption: (state, prop) => { + state = validateState(state) if (prop == null) { - return state.getIn(['ledger', 'synopsis', 'options']) + return null } - return state.getIn(['ledger', 'synopsis', 'options', prop]) + return state.getIn(['ledger', 'synopsis', 'options', prop], null) + }, + + getSynopsisOptions: (state) => { + state = validateState(state) + return state.getIn(['ledger', 'synopsis', 'options']) || Immutable.Map() }, setSynopsisOption: (state, prop, value) => { + state = validateState(state) if (prop == null) { return state } - return state.setIn(['ledger', 'synopsis', 'options', prop], value) - }, - - enableUndefinedPublishers: (state, publishers) => { - const sitesObject = state.get('siteSettings') - Object.keys(publishers).map((item) => { - const pattern = `https?://${item}` - const result = sitesObject.getIn([pattern, 'ledgerPayments']) - - if (result === undefined) { - const newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPayments', true) - state = state.set('siteSettings', newSiteSettings) - } - }) + state = state.setIn(['ledger', 'synopsis', 'options', prop], value) + state = ledgerState.setAboutSynopsisOptions(state) return state }, + /** + * INFO + */ getInfoProp: (state, prop) => { + state = validateState(state) if (prop == null) { - return state.getIn(['ledger', 'info']) + return null } - return state.getIn(['ledger', 'info', prop]) + return state.getIn(['ledger', 'info', prop], null) + }, + + getInfoProps: (state) => { + state = validateState(state) + return state.getIn(['ledger', 'info']) || Immutable.Map() }, setInfoProp: (state, prop, value) => { + state = validateState(state) if (prop == null) { return state } @@ -123,20 +233,105 @@ const ledgerState = { }, mergeInfoProp: (state, data) => { + state = validateState(state) if (data == null) { return state } - const oldData = ledgerState.getInfoProp() + data = makeImmutable(data) + + // clean-up + if (data.has('publishersV2')) { + data = data.set('publishersV2Stamp', data.getIn(['publishersV2', 'publishersV2Stamp'])) + data = data.delete('publishersV2') + } + if (data.has('rulesetV2')) { + data = data.set('rulesV2Stamp', data.getIn(['rulesetV2', 'rulesV2Stamp'])) + data = data.delete('rulesetV2') + } + + const oldData = ledgerState.getInfoProps(state) return state.setIn(['ledger', 'info'], oldData.merge(data)) }, resetInfo: (state) => { - return state.setIn(['ledger', 'info'], {}) + state = validateState(state) + return state.setIn(['ledger', 'info'], Immutable.Map()) + }, + + /** + * OTHERS + */ + setRecoveryStatus: (state, status) => { + state = validateState(state) + const date = new Date().getTime() + state = state.setIn(['about', 'preferences', 'recoverySucceeded'], status) + return state.setIn(['about', 'preferences', 'updatedStamp'], date) }, - resetSynopsis: (state) => { - return state.deleteIn(['ledger', 'synopsis']) + setLedgerError: (state, error, caller) => { + state = validateState(state) + if (error == null && caller == null) { + return state.setIn(['ledger', 'info', 'error'], null) + } + + return state + .setIn(['ledger', 'info', 'error', 'caller'], caller) + .setIn(['ledger', 'info', 'error', 'error'], error) + }, + + changePinnedValues: (state, publishers) => { + state = validateState(state) + if (publishers == null) { + return state + } + + publishers = makeImmutable(publishers) + publishers.forEach((item) => { + const publisherKey = item.get('site') + const pattern = urlUtil.getHostPattern(publisherKey) + const percentage = item.get('pinPercentage') + let newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPinPercentage', percentage) + state = state.set('siteSettings', newSiteSettings) + }) + + return state + }, + + enableUndefinedPublishers: (state, publishers) => { + state = validateState(state) + const sitesObject = state.get('siteSettings') + + if (publishers == null) { + return state + } + + for (let item of publishers) { + const key = item[0] + const pattern = urlUtil.getHostPattern(key) + const result = sitesObject.getIn([pattern, 'ledgerPayments']) + + if (result === undefined) { + const newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPayments', true) + state = state.set('siteSettings', newSiteSettings) + } + } + + return state + }, + + // TODO (optimization) don't have two almost identical object in state (synopsi->publishers and about->synopsis) + saveAboutSynopsis: (state, publishers) => { + state = validateState(state) + return state + .setIn(['ledger', 'about', 'synopsis'], publishers) + .setIn(['ledger', 'about', 'synopsisOptions'], ledgerState.getSynopsisOptions(state)) + }, + + setAboutSynopsisOptions: (state) => { + state = validateState(state) + return state + .setIn(['ledger', 'about', 'synopsisOptions'], ledgerState.getSynopsisOptions(state)) } } diff --git a/app/common/state/pageDataState.js b/app/common/state/pageDataState.js index 2b399e7c4d3..457744fe081 100644 --- a/app/common/state/pageDataState.js +++ b/app/common/state/pageDataState.js @@ -33,16 +33,17 @@ const pageDataState = { url, tabId }) + state = state.setIn(['pageData', 'last', 'url'], url) return state.setIn(['pageData', 'view'], pageViewEvent) }, addInfo: (state, data) => { - data = makeImmutable(data) - if (data == null) { return state } + data = makeImmutable(data) + const key = pageDataUtil.getInfoKey(data.get('url')) data = data.set('key', key) @@ -50,6 +51,10 @@ const pageDataState = { return state.setIn(['pageData', 'info', key], data) }, + resetInfo: (state) => { + return state.setIn(['pageData', 'last', 'info'], '') + }, + addLoad: (state, data) => { if (data == null) { return state @@ -64,11 +69,15 @@ const pageDataState = { return state.getIn(['pageData', 'view']) || Immutable.Map() }, + getLastUrl: (state) => { + return state.getIn(['pageData', 'last', 'url']) + }, + getLastInfo: (state) => { const key = state.getIn(['pageData', 'last', 'info']) if (key == null) { - Immutable.Map() + return Immutable.Map() } return state.getIn(['pageData', 'info', key], Immutable.Map()) @@ -92,6 +101,16 @@ const pageDataState = { } return state.setIn(['pageData', 'info', key, 'publisher'], publisher) + }, + + resetPageData: (state) => { + return state + .setIn(['pageData', 'load'], Immutable.List()) + .setIn(['pageData', 'info'], Immutable.Map()) + .setIn(['pageData', 'view'], Immutable.Map()) + .setIn(['pageData', 'last', 'info'], null) + .setIn(['pageData', 'last', 'url'], null) + .setIn(['pageData', 'last', 'tabId'], null) } } diff --git a/app/common/state/siteSettingsState.js b/app/common/state/siteSettingsState.js index 5fd8041487b..411a8720176 100644 --- a/app/common/state/siteSettingsState.js +++ b/app/common/state/siteSettingsState.js @@ -41,8 +41,8 @@ const api = { }, setSettingsProp: (state, pattern, prop, value) => { - if (prop == null) { - return null + if (prop == null || pattern == null) { + return state } return state.setIn(['siteSettings', pattern, prop], value) diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 1179f921784..ea59135aaa0 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -254,6 +254,7 @@ urlbar.placeholder=Enter a URL or search term urlCopied=URL copied to clipboard useBrave=Use Brave verifiedPublisher.title=This is a verified publisher. Click to enable this publisher for payments +notVisiblePublisher.title=Publisher is not yet added to the ledger, because it doesn't meet criteria yet. versionInformation=Version Information videoCapturePermission=Video Capture viewCertificate=View Certificate @@ -263,4 +264,4 @@ windowCaptionButtonClose=Close windowCaptionButtonMaximize=Maximize windowCaptionButtonMinimize=Minimize windowCaptionButtonRestore=Restore Down -yes=Yes \ No newline at end of file +yes=Yes diff --git a/app/ledger.js b/app/ledger.js deleted file mode 100644 index d8e3a03882e..00000000000 --- a/app/ledger.js +++ /dev/null @@ -1,520 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -'use strict' - -/* brave ledger integration for the brave browser - - module entry points: - init() - called by app/index.js to start module - quit() - .. .. .. .. prior to browser quitting - boot() - .. .. .. .. to create wallet - reset() - .. .. .. .. to remove state - - IPC entry point: - LEDGER_PUBLISHER - called synchronously by app/extensions/brave/content/scripts/pageInformation.js - CHANGE_SETTING - called asynchronously to record a settings change - - eventStore entry point: - addChangeListener - called when tabs render or gain focus - */ - -/* internal terminology: - - blockedP: the user has selected 'Never include this site' (site setting 'ledgerPaymentsShown') - stickyP: the user has toggled ON the button to the right of the address bar (site setting 'ledgerPayments') - excluded: the publisher appears on the list of sites to exclude from automatic inclusion (if auto-include is enabled) - - eligibleP: the current scorekeeper says the publisher has received enough durable visits - visibleP: (stickyP OR (!excluded AND eligibleP)) AND !blockedP -contributeP: (stickyP OR !excluded) AND eligibleP AND !blockedP - */ - -const fs = require('fs') -const os = require('os') -const path = require('path') -const urlParse = require('./common/urlParse') -const urlFormat = require('url').format -const Immutable = require('immutable') - -const electron = require('electron') -const app = electron.app -const ipc = electron.ipcMain -const session = electron.session - -const acorn = require('acorn') -const levelup = require('level') -const moment = require('moment') -const qr = require('qr-image') -const querystring = require('querystring') -const random = require('random-lib') -const tldjs = require('tldjs') -const underscore = require('underscore') -const uuid = require('uuid') - -const appActions = require('../js/actions/appActions') -const appConfig = require('../js/constants/appConfig') -const messages = require('../js/constants/messages') -const settings = require('../js/constants/settings') -const request = require('../js/lib/request') -const getSetting = require('../js/settings').getSetting -const locale = require('./locale') -const appStore = require('../js/stores/appStore') -const rulesolver = require('./extensions/brave/content/scripts/pageInformation') -const ledgerUtil = require('./common/lib/ledgerUtil') -const tabs = require('./browser/tabs') -const pageDataState = require('./common/state/pageDataState') - -// "only-when-needed" loading... -let ledgerBalance = null -let ledgerClient = null -let ledgerGeoIP = null -let ledgerPublisher = null - -// testing data - - -// TBD: remove these post beta [MTR] -// TODO remove, it's not used anymore -const logPath = 'ledger-log.json' -const publisherPath = 'ledger-publisher.json' -const scoresPath = 'ledger-scores.json' - -// TBD: move these to secureState post beta [MTR] -const synopsisPath = 'ledger-synopsis.json' - -/* - * ledger globals - */ - -var bootP = false -var client -const clientOptions = { - debugP: process.env.LEDGER_DEBUG, - loggingP: process.env.LEDGER_LOGGING, - rulesTestP: process.env.LEDGER_RULES_TESTING, - verboseP: process.env.LEDGER_VERBOSE, - server: process.env.LEDGER_SERVER_URL, - createWorker: app.createWorker -} -var quitP - -/* - * publisher globals - */ - -var synopsis -var locations = {} -var publishers = {} - -/* - * utility globals - */ - -/* - * notification state globals - */ - -let addFundsMessage -let reconciliationMessage -let notificationPaymentDoneMessage -let notificationTryPaymentsMessage -let notificationTimeout = null - - -/* - * module entry points - */ - - -var boot = () => { - if ((bootP) || (client)) return - - bootP = true - fs.access(pathName(statePath), fs.FF_OK, (err) => { - if (!err) return - - if (err.code !== 'ENOENT') console.error('statePath read error: ' + err.toString()) - - ledgerInfo.creating = true - appActions.updateLedgerInfo({ creating: true }) - try { - clientprep() - client = ledgerClient(null, underscore.extend({ roundtrip: roundtrip }, clientOptions), null) - } catch (ex) { - appActions.updateLedgerInfo({}) - - bootP = false - return console.error('ledger client boot error: ', ex) - } - if (client.sync(callback) === true) run(random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute })) - getBalance() - - bootP = false - }) -} - -/* - * Print or Save Recovery Keys - */ - - -/* - * Recover Ledger Keys - */ - -/* - * IPC entry point - */ - -if (ipc) { - ipc.on(messages.LEDGER_CREATE_WALLET, () => { - boot() - }) - - let ledgerPaymentsPresent = {} - // TODO(bridiver) - convert this to an action - process.on(messages.LEDGER_PAYMENTS_PRESENT, (tabId, presentP) => { - if (presentP) { - ledgerPaymentsPresent[tabId] = presentP - } else { - delete ledgerPaymentsPresent[tabId] - } - - if (Object.keys(ledgerPaymentsPresent).length > 0 && getSetting(settings.PAYMENTS_ENABLED)) { - if (!balanceTimeoutId) getBalance() - } else if (balanceTimeoutId) { - clearTimeout(balanceTimeoutId) - balanceTimeoutId = false - } - }) - - ipc.on(messages.LEDGER_PUBLISHER, (event, location) => { - var ctx - - if ((!synopsis) || (event.sender.session === session.fromPartition('default')) || (!tldjs.isValid(location))) { - event.returnValue = {} - return - } - - ctx = urlParse(location, true) - ctx.TLD = tldjs.getPublicSuffix(ctx.host) - if (!ctx.TLD) { - if (publisherInfo._internal.verboseP) console.log('\nno TLD for:' + ctx.host) - event.returnValue = {} - return - } - - ctx = underscore.mapObject(ctx, function (value, key) { if (!underscore.isFunction(value)) return value }) - ctx.URL = location - ctx.SLD = tldjs.getDomain(ctx.host) - ctx.RLD = tldjs.getSubdomain(ctx.host) - ctx.QLD = ctx.RLD ? underscore.last(ctx.RLD.split('.')) : '' - - if (!event.sender.isDestroyed()) { - event.sender.send(messages.LEDGER_PUBLISHER_RESPONSE + '-' + location, { context: ctx, rules: publisherInfo._internal.ruleset.cooked }) - } - }) - - ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => { - const win = electron.BrowserWindow.getActiveWindow() - if (message === locale.translation('addFundsNotification')) { - appActions.hideNotification(message) - // See showNotificationAddFunds() for buttons. - // buttonIndex === 1 is "Later"; the timestamp until which to delay is set - // in showNotificationAddFunds() when triggering this notification. - if (buttonIndex === 0) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) - } else if (buttonIndex === 2 && win) { - // Add funds: Open payments panel - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: win.id - }) - } - } else if (message === locale.translation('reconciliationNotification')) { - appActions.hideNotification(message) - // buttonIndex === 1 is Dismiss - if (buttonIndex === 0) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) - } else if (buttonIndex === 2 && win) { - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: win.id - }) - } - } else if (message === notificationPaymentDoneMessage) { - appActions.hideNotification(message) - if (buttonIndex === 0) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) - } - } else if (message === locale.translation('notificationTryPayments')) { - appActions.hideNotification(message) - if (buttonIndex === 1 && win) { - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: win.id - }) - } - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) - } - }) - - ipc.on(messages.ADD_FUNDS_CLOSED, () => { - if (balanceTimeoutId) clearTimeout(balanceTimeoutId) - balanceTimeoutId = setTimeout(getBalance, 5 * milisecons.second) - }) -} - -/* - * eventStore entry point - */ - -var fileTypes = { - bmp: new Buffer([ 0x42, 0x4d ]), - gif: new Buffer([ 0x47, 0x49, 0x46, 0x38, [0x37, 0x39], 0x61 ]), - ico: new Buffer([ 0x00, 0x00, 0x01, 0x00 ]), - jpeg: new Buffer([ 0xff, 0xd8, 0xff ]), - png: new Buffer([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a ]) -} - -var signatureMax = 0 -underscore.keys(fileTypes).forEach((fileType) => { - if (signatureMax < fileTypes[fileType].length) signatureMax = fileTypes[fileType].length -}) -signatureMax = Math.ceil(signatureMax * 1.5) - -/* - * module initialization - */ - -/* - * update location information - */ - -var updateLocationInfo = (location) => { - appActions.updateLocationInfo(locations) -} - -var updateLocation = (location, publisher) => { - var updateP - - if (typeof locations[location].stickyP === 'undefined') locations[location].stickyP = stickyP(publisher) - if (typeof locations[location].verified !== 'undefined') return - - if (synopsis && synopsis.publishers[publisher] && (typeof synopsis.publishers[publisher].options.verified !== 'undefined')) { - locations[location].verified = synopsis.publishers[publisher].options.verified || false - updateP = true - } else { - verifiedP(publisher, (err, result) => { - if ((err) && (!err.notFound)) return - - locations[location].verified = (result && result.verified) || false - updateLocationInfo(location) - }) - } - - if (synopsis && synopsis.publishers[publisher] && (typeof synopsis.publishers[publisher].options.exclude !== 'undefined')) { - locations[location].exclude = synopsis.publishers[publisher].options.exclude || false - updateP = true - } else { - excludeP(publisher, (err, result) => { - if ((err) && (!err.notFound)) return - - locations[location].exclude = (result && result.exclude) || false - updateLocationInfo(location) - }) - } - - if (updateP) updateLocationInfo(location) -} - -const getFavIcon = (publisher, page, location) => { - if ((page.protocol) && (!publisher.protocol)) { - publisher.protocol = page.protocol - } - - if ((typeof publisher.faviconURL === 'undefined') && ((page.faviconURL) || (publisher.protocol))) { - let faviconURL = page.faviconURL || publisher.protocol + '//' + urlParse(location).host + '/favicon.ico' - if (publisherInfo._internal.debugP) { - console.log('\nrequest: ' + faviconURL) - } - - publisher.faviconURL = null - fetchFavIcon(publisher, faviconURL) - } -} - -const fetchFavIcon = (publisher, url, redirects) => { - if (typeof redirects === 'undefined') redirects = 0 - - request.request({ url: url, responseType: 'blob' }, (err, response, blob) => { - let matchP, prefix, tail - - if ((response) && (publisherInfo._internal.verboseP)) { - console.log('[ response for ' + url + ' ]') - console.log('>>> HTTP/' + response.httpVersionMajor + '.' + response.httpVersionMinor + ' ' + response.statusCode + - ' ' + (response.statusMessage || '')) - underscore.keys(response.headers).forEach((header) => { console.log('>>> ' + header + ': ' + response.headers[header]) }) - console.log('>>>') - console.log('>>> ' + (blob || '').substr(0, 80)) - } - - if (publisherInfo._internal.debugP) { - console.log('\nresponse: ' + url + - ' errP=' + (!!err) + ' blob=' + (blob || '').substr(0, 80) + '\nresponse=' + - JSON.stringify(response, null, 2)) - } - - if (err) { - console.error('response error: ' + err.toString() + '\n' + err.stack) - return null - } - - if ((response.statusCode === 301) && (response.headers.location)) { - if (redirects < 3) fetchFavIcon(publisher, response.headers.location, redirects++) - return null - } - - if ((response.statusCode !== 200) || (response.headers['content-length'] === '0')) { - return null - } - - tail = blob.indexOf(';base64,') - if (blob.indexOf('data:image/') !== 0) { - // NB: for some reason, some sites return an image, but with the wrong content-type... - if (tail <= 0) { - return null - } - - prefix = new Buffer(blob.substr(tail + 8, signatureMax), 'base64') - underscore.keys(fileTypes).forEach((fileType) => { - if (matchP) return - if ((prefix.length >= fileTypes[fileType].length) || - (fileTypes[fileType].compare(prefix, 0, fileTypes[fileType].length) !== 0)) return - - blob = 'data:image/' + fileType + blob.substr(tail) - matchP = true - }) - if (!matchP) { - return - } - } else if ((tail > 0) && (tail + 8 >= blob.length)) return - - if (publisherInfo._internal.debugP) { - console.log('\n' + publisher.site + ' synopsis=' + - JSON.stringify(underscore.extend(underscore.omit(publisher, [ 'faviconURL', 'window' ]), - { faviconURL: publisher.faviconURL && '... ' }), null, 2)) - } - - publisher.faviconURL = blob - updatePublisherInfo() - }) -} - - -/* - * publisher utilities - */ - -/* - * update ledger information - */ - -var ledgerInfo = { - creating: false, - created: false, - - reconcileFrequency: undefined, - reconcileStamp: undefined, - - transactions: - [ -/* - { - viewingId: undefined, - surveyorId: undefined, - contribution: { - fiat: { - amount: undefined, - currency: undefined - }, - rates: { - [currency]: undefined // bitcoin value in - }, - satoshis: undefined, - fee: undefined - }, - submissionStamp: undefined, - submissionId: undefined, - count: undefined, - satoshis: undefined, - votes: undefined, - ballots: { - [publisher]: undefined - } - , ... - */ - ], - - // set from ledger client's state.paymentInfo OR client's getWalletProperties - // Bitcoin wallet address - address: undefined, - - // Bitcoin wallet balance (truncated BTC and satoshis) - balance: undefined, - unconfirmed: undefined, - satoshis: undefined, - - // the desired contribution (the btc value approximates the amount/currency designation) - btc: undefined, - amount: undefined, - currency: undefined, - - paymentURL: undefined, - buyURL: undefined, - bravery: undefined, - - // wallet credentials - paymentId: undefined, - passphrase: undefined, - - // advanced ledger settings - minPublisherDuration: undefined, - minPublisherVisits: undefined, - showOnlyVerified: undefined, - - hasBitcoinHandler: false, - - // geoIP/exchange information - countryCode: undefined, - exchangeInfo: undefined, - - _internal: { - exchangeExpiry: 0, - exchanges: {}, - geoipExpiry: 0 - }, - error: null -} - -/* - * ledger client callbacks - */ - -/* - * low-level utilities - */ - - - -module.exports = { - init: init, - recoverKeys: recoverKeys, - backupKeys: backupKeys, - quit: quit, - boot: boot, - reset: reset, - doAction -} diff --git a/app/pdf.js b/app/pdf.js index 43a70382519..f74b89d6ba4 100644 --- a/app/pdf.js +++ b/app/pdf.js @@ -8,14 +8,23 @@ const electron = require('electron') const config = require('../js/constants/config') const BrowserWindow = electron.BrowserWindow -const renderUrlToPdf = (appState, action, testingMode) => { +const renderUrlToPdf = (appState, action) => { let url = action.url let savePath = action.savePath let openAfterwards = action.openAfterwards let currentBw = BrowserWindow.getFocusedWindow() - let bw = new BrowserWindow({show: !!testingMode, backgroundColor: '#ffffff'}) + let bw = new BrowserWindow({ + show: false, + width: 0, + height: 0, + focusable: false, + backgroundColor: '#ffffff', + webPreferences: { + partition: 'default' + } + }) let wv = bw.webContents @@ -45,12 +54,12 @@ const renderUrlToPdf = (appState, action, testingMode) => { if (openAfterwards && savePath) { currentBw.webContents.loadURL('file://' + finalSavePath) } + } - if (bw && !testingMode) { - try { - bw.close() - } catch (exc) {} - } + if (bw) { + try { + bw.close() + } catch (exc) {} } }) }) diff --git a/app/renderer/components/navigation/navigationBar.js b/app/renderer/components/navigation/navigationBar.js index 34a2f98fdf5..b45b5d02e13 100644 --- a/app/renderer/components/navigation/navigationBar.js +++ b/app/renderer/components/navigation/navigationBar.js @@ -21,6 +21,7 @@ const settings = require('../../../../js/constants/settings') const tabState = require('../../../common/state/tabState') const publisherState = require('../../../common/lib/publisherUtil') const frameStateUtil = require('../../../../js/state/frameStateUtil') +const ledgerState = require('../../../common/state/ledgerState') // Utils const cx = require('../../../../js/lib/classSet') @@ -44,7 +45,7 @@ class NavigationBar extends React.Component { const loading = activeFrame.get('loading') const location = activeFrame.get('location', '') const locationId = getBaseUrl(location) - const publisherId = state.getIn(['locationInfo', locationId, 'publisher']) + const publisherId = ledgerState.getLocationProp(state, locationId, 'publisher') const navbar = activeFrame.get('navbar', Immutable.Map()) const locationCache = bookmarkLocationCache.getCacheKey(state, location) diff --git a/app/renderer/components/navigation/publisherToggle.js b/app/renderer/components/navigation/publisherToggle.js index b603cdba49b..fde43f8ae4d 100644 --- a/app/renderer/components/navigation/publisherToggle.js +++ b/app/renderer/components/navigation/publisherToggle.js @@ -14,12 +14,13 @@ const BrowserButton = require('../common/browserButton') const appActions = require('../../../../js/actions/appActions') // State -const publisherState = require('../../../common/lib/publisherUtil') +const ledgerState = require('../../../common/state/ledgerState') // Utils const {getHostPattern} = require('../../../../js/lib/urlutil') const {getBaseUrl} = require('../../../../js/lib/appUrlUtil') const frameStateUtil = require('../../../../js/state/frameStateUtil') +const ledgerUtil = require('../../../common/lib/ledgerUtil') // Style const globalStyles = require('../styles/global') @@ -36,7 +37,10 @@ class PublisherToggle extends React.Component { get l10nString () { let l10nData = 'disabledPublisher' - if (this.props.isVerifiedPublisher && !this.props.isEnabledForPaymentsPublisher) { + + if (!this.props.isVisibleInLedger) { + l10nData = 'notVisiblePublisher' + } else if (this.props.isVerifiedPublisher && !this.props.isEnabledForPaymentsPublisher) { l10nData = 'verifiedPublisher' } else if (this.props.isEnabledForPaymentsPublisher) { l10nData = 'enabledPublisher' @@ -45,7 +49,9 @@ class PublisherToggle extends React.Component { } onAuthorizePublisher () { - appActions.changeSiteSetting(this.props.hostPattern, 'ledgerPayments', !this.props.isEnabledForPaymentsPublisher) + if (this.props.isVisibleInLedger) { + appActions.changeSiteSetting(this.props.hostPattern, 'ledgerPayments', !this.props.isEnabledForPaymentsPublisher) + } } mergeProps (state, ownProps) { @@ -53,16 +59,17 @@ class PublisherToggle extends React.Component { const activeFrame = frameStateUtil.getActiveFrame(currentWindow) || Immutable.Map() const location = activeFrame.get('location', '') const locationId = getBaseUrl(location) - const locationInfo = state.get('locationInfo', Immutable.Map()) + const publisherId = ledgerState.getLocationProp(state, locationId, 'publisher') const props = {} // used in renderer - props.isEnabledForPaymentsPublisher = publisherState.enabledForPaymentsPublisher(state, locationId) - props.isVerifiedPublisher = locationInfo.getIn([locationId, 'verified']) + props.isVisibleInLedger = ledgerUtil.visibleP(state, publisherId) + props.isEnabledForPaymentsPublisher = ledgerUtil.stickyP(state, publisherId) + props.isVerifiedPublisher = ledgerState.getPublisherOption(state, publisherId, 'verified') // used in functions - props.publisherId = locationInfo.getIn([locationId, 'publisher']) - props.hostPattern = getHostPattern(props.publisherId) + props.publisherId = publisherId + props.hostPattern = getHostPattern(publisherId) return props } @@ -75,12 +82,15 @@ class PublisherToggle extends React.Component { className={css(styles.addPublisherButtonContainer)}> @@ -126,5 +136,9 @@ const styles = StyleSheet.create({ fundUnverified: { backgroundImage: `url(${fundUnverifiedPublisherImage})`, backgroundSize: '18px' + }, + + notVisible: { + opacity: 0.3 } }) diff --git a/app/renderer/components/navigation/urlBar.js b/app/renderer/components/navigation/urlBar.js index beecc54dc04..464cfd12dec 100644 --- a/app/renderer/components/navigation/urlBar.js +++ b/app/renderer/components/navigation/urlBar.js @@ -27,6 +27,7 @@ const frameStateUtil = require('../../../../js/state/frameStateUtil') const siteSettings = require('../../../../js/state/siteSettings') const tabState = require('../../../common/state/tabState') const siteSettingsState = require('../../../common/state/siteSettingsState') +const ledgerState = require('../../../common/state/ledgerState') // Utils const cx = require('../../../../js/lib/classSet') @@ -422,7 +423,7 @@ class UrlBar extends React.Component { const braverySettings = siteSettings.getSiteSettingsForURL(allSiteSettings, location) // TODO(bridiver) - these definitely needs a helpers - const publisherId = state.getIn(['locationInfo', baseUrl, 'publisher']) + const publisherId = ledgerState.getLocationPublisher(state, baseUrl) const activateSearchEngine = urlbar.getIn(['searchDetail', 'activateSearchEngine']) const urlbarSearchDetail = urlbar.get('searchDetail') diff --git a/app/renderer/components/preferences/payment/enabledContent.js b/app/renderer/components/preferences/payment/enabledContent.js index a9fecffc22b..0b0c276b5aa 100644 --- a/app/renderer/components/preferences/payment/enabledContent.js +++ b/app/renderer/components/preferences/payment/enabledContent.js @@ -23,10 +23,12 @@ const globalStyles = require('../../styles/global') const {paymentStylesVariables} = require('../../styles/payment') const cx = require('../../../../../js/lib/classSet') +// Actions +const appActions = require('../../../../../js/actions/appActions') + // other const getSetting = require('../../../../../js/settings').getSetting const settings = require('../../../../../js/constants/settings') -const aboutActions = require('../../../../../js/about/aboutActions') // TODO: report when funds are too low // TODO: support non-USD currency @@ -67,7 +69,7 @@ class EnabledContent extends ImmutableComponent { createWallet () { const ledgerData = this.props.ledgerData if (!ledgerData.get('created')) { - aboutActions.createWallet() + appActions.onLedgerWalletCreate() } return () => {} diff --git a/app/renderer/components/preferences/payment/ledgerRecovery.js b/app/renderer/components/preferences/payment/ledgerRecovery.js index 5e4bcd40026..78679519188 100644 --- a/app/renderer/components/preferences/payment/ledgerRecovery.js +++ b/app/renderer/components/preferences/payment/ledgerRecovery.js @@ -20,6 +20,7 @@ const commonStyles = require('../../styles/commonStyles') // other const aboutActions = require('../../../../../js/about/aboutActions') +const appActions = require('../../../../../js/actions/appActions') class LedgerRecoveryContent extends ImmutableComponent { constructor () { @@ -37,8 +38,8 @@ class LedgerRecoveryContent extends ImmutableComponent { } clearRecoveryStatus () { - aboutActions.clearRecoveryStatus() this.props.hideAdvancedOverlays() + appActions.resetRecoverStatus() } render () { diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index 528c001b248..db1a74df1f0 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -24,6 +24,7 @@ const pinIcon = require('../../../../extensions/brave/img/ledger/icon_pin.svg') const settings = require('../../../../../js/constants/settings') const getSetting = require('../../../../../js/settings').getSetting const aboutActions = require('../../../../../js/about/aboutActions') +const urlUtil = require('../../../../../js/lib/urlutil') const {SettingCheckbox, SiteSettingCheckbox} = require('../../common/settings') class LedgerTable extends ImmutableComponent { @@ -51,7 +52,7 @@ class LedgerTable extends ImmutableComponent { } getHostPattern (synopsis) { - return `https?://${synopsis.get('site')}` + return urlUtil.getHostPattern(synopsis.get('site')) } getVerifiedIcon (synopsis) { diff --git a/app/sessionStore.js b/app/sessionStore.js index 506daab1967..67dd3df224f 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -967,6 +967,18 @@ module.exports.defaultAppState = () => { }, load: [], view: {} + }, + ledger: { + about: { + synopsis: [], + synopsisOptions: {} + }, + info: {}, + locations: {}, + synopsis: { + options: {}, + publishers: {} + } } } } diff --git a/docs/state.md b/docs/state.md index 761abfaaa5f..b1625ac3884 100644 --- a/docs/state.md +++ b/docs/state.md @@ -228,8 +228,6 @@ AppStore }], // contributions reconciling/reconciled unconfirmed: string // unconfirmed balance in BTC.toFixed(4) }, - isBooting: boolean, // flag which telll us if wallet is still creating or not - isQuiting: boolan, // flag which tell us if we are closing ledger (because of browser close) locations: { [url]: { publisher: string, // url of the publisher in question diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index f231492ae89..384d3277c86 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -155,16 +155,6 @@ const aboutActions = { }) }, - /** - * Clear wallet recovery status - */ - clearRecoveryStatus: function () { - aboutActions.dispatchAction({ - actionType: appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED, - recoverySucceeded: undefined - }) - }, - /** * Click through a certificate error. * @@ -254,10 +244,6 @@ const aboutActions = { ipc.send(messages.EXPORT_BOOKMARKS) }, - createWallet: function () { - ipc.send(messages.LEDGER_CREATE_WALLET) - }, - setLedgerEnabled: function (enabled) { ipc.send(messages.LEDGER_ENABLE, enabled) }, diff --git a/js/about/preferences.js b/js/about/preferences.js index 0ec7e81779d..843d9b71b7d 100644 --- a/js/about/preferences.js +++ b/js/about/preferences.js @@ -834,13 +834,13 @@ class AboutPreferences extends React.Component { this.setState(stateDiff) // Tell ledger when Add Funds overlay is closed if (isVisible === false && overlayName === 'addFunds') { - ipc.send(messages.ADD_FUNDS_CLOSED) + appActions.onAddFoundsClosed() } } createWallet () { if (this.state.ledgerData && !this.state.ledgerData.get('created')) { - aboutActions.createWallet() + appActions.onLedgerWalletCreate() } } diff --git a/js/actions/appActions.js b/js/actions/appActions.js index eb9ae07f17e..6b40fcdf082 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -338,27 +338,6 @@ const appActions = { actionType: appConstants.APP_CLEAR_COMPLETED_DOWNLOADS }) }, - - /** - * Dispatches a message indicating ledger recovery succeeded - */ - ledgerRecoverySucceeded: function () { - dispatch({ - actionType: appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED, - recoverySucceeded: true - }) - }, - - /** - * Dispatches a message indicating ledger recovery failed - */ - ledgerRecoveryFailed: function () { - dispatch({ - actionType: appConstants.APP_LEDGER_RECOVERY_STATUS_CHANGED, - recoverySucceeded: false - }) - }, - /** * Sets the etag value for a downloaded data file. * This is used for keeping track of when to re-download adblock and tracking @@ -514,28 +493,6 @@ const appActions = { }) }, - /** - * Updates ledger information for the payments pane - * @param {object} ledgerInfo - the current ledger state - */ - updateLedgerInfo: function (ledgerInfo) { - dispatch({ - actionType: appConstants.APP_UPDATE_LEDGER_INFO, - ledgerInfo - }) - }, - - /** - * Updates location information for the URL bar - * @param {object} locationInfo - the current location synopsis - */ - updateLocationInfo: function (locationInfo) { - dispatch({ - actionType: appConstants.APP_UPDATE_LOCATION_INFO, - locationInfo - }) - }, - /** * Shows a message in the notification bar * @param {{message: string, buttons: Array., frameOrigin: string, options: Object}} detail @@ -1611,6 +1568,130 @@ const appActions = { }) }, + onFavIconReceived: function (publisherKey, blob) { + dispatch({ + actionType: appConstants.APP_ON_FAVICON_RECEIVED, + publisherKey, + blob + }) + }, + + onPublisherOptionUpdate: function (publisherKey, prop, value, saveIntoSettings = false) { + dispatch({ + actionType: appConstants.APP_ON_PUBLISHER_OPTION_UPDATE, + publisherKey, + prop, + value, + saveIntoSettings + }) + }, + + onLedgerLocationUpdate: function (location, prop, value) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_LOCATION_UPDATE, + location, + prop, + value + }) + }, + + onLedgerWalletCreate: function () { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_WALLET_CREATE + }) + }, + + onBootStateFile: function () { + dispatch({ + actionType: appConstants.APP_ON_BOOT_STATE_FILE + }) + }, + + onLedgerBalanceReceived: function (unconfirmed) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_BALANCE_RECEIVED, + unconfirmed + }) + }, + + onWalletProperties: function (body) { + dispatch({ + actionType: appConstants.APP_ON_WALLET_PROPERTIES, + body + }) + }, + + ledgerPaymentsPresent: function (tabId, present) { + dispatch({ + actionType: appConstants.APP_LEDGER_PAYMENTS_PRESENT, + tabId, + present + }) + }, + + onAddFoundsClosed: function () { + dispatch({ + actionType: appConstants.APP_ON_ADD_FUNDS_CLOSED + }) + }, + + onWalletRecovery: function (error, result) { + dispatch({ + actionType: appConstants.APP_ON_WALLET_RECOVERY, + error, + result + }) + }, + + onBraveryProperties: function (error, result) { + dispatch({ + actionType: appConstants.APP_ON_BRAVERY_PROPERTIES, + error, + result + }) + }, + + onLedgerFirstSync: function (parsedData) { + dispatch({ + actionType: appConstants.APP_ON_FIRST_LEDGER_SYNC, + parsedData + }) + }, + + onLedgerCallback: function (result, delayTime) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_CALLBACK, + result, + delayTime + }) + }, + + onTimeUntilReconcile: function (stateResult) { + dispatch({ + actionType: appConstants.APP_ON_TIME_UNTIL_RECONCILE, + stateResult + }) + }, + + onLedgerRun: function (delay) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_RUN, + delay + }) + }, + + onNetworkConnected: function () { + dispatch({ + actionType: appConstants.APP_ON_NETWORK_CONNECTED + }) + }, + + resetRecoverStatus: function () { + dispatch({ + actionType: appConstants.APP_ON_RESET_RECOVERY_STATUS + }) + }, + onPinnedTabReorder: function (siteKey, destinationKey, prepend) { dispatch({ actionType: appConstants.APP_ON_PINNED_TAB_REORDER, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 5a1e00366f7..0f37e816275 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -32,8 +32,6 @@ const appConstants = { APP_CLEAR_SITE_SETTINGS: _, APP_ON_CLEAR_BROWSING_DATA: _, APP_IMPORT_BROWSER_DATA: _, - APP_UPDATE_LEDGER_INFO: _, - APP_UPDATE_LOCATION_INFO: _, APP_SHOW_NOTIFICATION: _, /** @param {Object} detail */ APP_HIDE_NOTIFICATION: _, /** @param {string} message */ APP_BACKUP_KEYS: _, @@ -153,7 +151,25 @@ const appConstants = { APP_INSPECT_ELEMENT: _, APP_ON_BOOKMARK_WIDTH_CHANGED: _, APP_ON_BOOKMARK_FOLDER_WIDTH_CHANGED: _, - APP_WINDOW_RESIZED: _ + APP_WINDOW_RESIZED: _, + APP_ON_FAVICON_RECEIVED: _, + APP_ON_PUBLISHER_OPTION_UPDATE: _, + APP_ON_LEDGER_OPTION_UPDATE: _, + APP_ON_LEDGER_WALLET_CREATE: _, + APP_ON_BOOT_STATE_FILE: _, + APP_LEDGER_PAYMENTS_PRESENT: _, + APP_ON_WALLET_RECOVERY: _, + APP_ON_BRAVERY_PROPERTIES: _, + APP_ON_LEDGER_BALANCE_RECEIVED: _, + APP_ON_LEDGER_LOCATION_UPDATE: _, + APP_ON_WALLET_PROPERTIES: _, + APP_ON_ADD_FUNDS_CLOSED: _, + APP_ON_FIRST_LEDGER_SYNC: _, + APP_ON_LEDGER_CALLBACK: _, + APP_ON_TIME_UNTIL_RECONCILE: _, + APP_ON_LEDGER_RUN: _, + APP_ON_NETWORK_CONNECTED: _, + APP_ON_RESET_RECOVERY_STATUS: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/messages.js b/js/constants/messages.js index 21afa547b8a..c72ace77b15 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -132,12 +132,9 @@ const messages = { // Debugging DEBUG_REACT_PROFILE: _, // Ledger - LEDGER_PAYMENTS_PRESENT: _, LEDGER_PUBLISHER: _, LEDGER_PUBLISHER_RESPONSE: _, LEDGER_UPDATED: _, - LEDGER_CREATE_WALLET: _, - ADD_FUNDS_CLOSED: _, RENDER_URL_TO_PDF: _, // Sync SYNC_UPDATED: _, diff --git a/test/about/ledgerTableTest.js b/test/about/ledgerTableTest.js index aa064879123..ca5aa9df869 100644 --- a/test/about/ledgerTableTest.js +++ b/test/about/ledgerTableTest.js @@ -99,11 +99,13 @@ describe('Ledger table', function () { return true }) .tabByIndex(0) + .waitForVisible(`${firstTableFirstRow} [data-test-id="siteName"]`) .waitUntil(function () { return this.getText(`${firstTableFirstRow} [data-test-id="siteName"]`).then((value) => { return value === topPublisher.get('site') }) }, 5000) + .waitForVisible(`${firstTableFirstRow} [data-test-id="pinnedInput"]`) .waitUntil(function () { return this.getValue(`${firstTableFirstRow} [data-test-id="pinnedInput"]`).then((value) => { return Number(value) === topPublisher.get('pinPercentage') @@ -149,6 +151,7 @@ describe('Ledger table', function () { return true }) .tabByIndex(0) + .waitForVisible(`${firstTableFirstRow} [data-test-id="siteName"]`) .waitUntil(function () { return this.getText(`${firstTableFirstRow} [data-test-id="siteName"]`).then((value) => { return value === topPublisher.get('site') @@ -157,8 +160,7 @@ describe('Ledger table', function () { .waitForVisible(`${firstTableFirstRow} [data-switch-status="true"]`) }) - // TODO re-enable when #9641 is fixed - it.skip('check pinned sites amount, when you have 0 eligible unpinned sites', function * () { + it('check pinned sites amount, when you have 0 eligible unpinned sites', function * () { yield this.app.client .tabByIndex(0) .click(`${secondTableFirstRow} [data-test-pinned="false"]`) diff --git a/test/lib/brave.js b/test/lib/brave.js index e3eca354bba..31f02bc9a3f 100644 --- a/test/lib/brave.js +++ b/test/lib/brave.js @@ -1049,10 +1049,11 @@ var exports = { // get synopsis from the store this.app.client.addCommand('waitUntilSynopsis', function (cb) { + logVerbose(`waitUntilSynopsis()`) return this.waitUntil(function () { return this.getAppState().then((val) => { val = Immutable.fromJS(val) - let synopsis = val.getIn(['value', 'publisherInfo', 'synopsis']) + let synopsis = val.getIn(['value', 'ledger', 'synopsis']) if (synopsis !== undefined) { return cb(synopsis) } diff --git a/test/unit/app/browser/reducers/pageDataReducerTest.js b/test/unit/app/browser/reducers/pageDataReducerTest.js index 48bfd8e7c61..29f47783af1 100644 --- a/test/unit/app/browser/reducers/pageDataReducerTest.js +++ b/test/unit/app/browser/reducers/pageDataReducerTest.js @@ -96,6 +96,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = state .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], 'https://brave.com') .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: 'https://brave.com', @@ -133,6 +134,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = state .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: null, @@ -181,6 +183,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = state .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: null, @@ -207,6 +210,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = state .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: null, @@ -286,6 +290,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = state .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], 'https://brave.com') .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: 'https://brave.com', @@ -317,6 +322,7 @@ describe('pageDataReducer unit tests', function () { const expectedState = newState .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], 'https://brave.com') .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: 0, url: 'https://brave.com', diff --git a/test/unit/app/common/lib/publisherUtilTest.js b/test/unit/app/common/lib/publisherUtilTest.js index 892050361f8..c3f3178d113 100644 --- a/test/unit/app/common/lib/publisherUtilTest.js +++ b/test/unit/app/common/lib/publisherUtilTest.js @@ -68,66 +68,4 @@ describe('publisherUtil test', function () { assert.equal(result, true) }) }) - - describe('enabledForPaymentsPublisher', function () { - const state = Immutable.fromJS({ - locationInfo: { - 'https://brave.com': { - exclude: false, - publisher: 'brave.com', - stickyP: false, - timestamp: 1496942403068, - verified: false - } - }, - publisherInfo: { - synopsis: { - 0: { - daysSpent: 0, - duration: 623405, - faviconURL: '', - hoursSpent: 0, - minutesSpent: 10, - percentage: 100, - publisherURL: 'http://brave.com', - score: 9.365888800773842, - secondsSpent: 23, - site: 'brave.com', - verified: false, - views: 1, - weight: 100 - } - } - }, - siteSettings: { - 'https?://brave.com': { - ledgerPayments: false - } - } - }) - - it('host settings is null, but publisher synopsis is valid', function () { - let newState = state.set('siteSettings', Immutable.fromJS({})) - const result = publisherUtil.enabledForPaymentsPublisher(newState, 'https://brave.com') - assert.equal(result, true) - }) - - it('host settings is null, publisher synopsis is null, but auto include is on and exclude on off', function () { - let newState = state.set('siteSettings', Immutable.fromJS({})) - newState = newState.set('publisherInfo', Immutable.fromJS({})) - const result = publisherUtil.enabledForPaymentsPublisher(newState, 'https://brave.com') - assert.equal(result, true) - }) - - it('host settings is set and ledgerPayments is false', function () { - const result = publisherUtil.enabledForPaymentsPublisher(state, 'https://brave.com') - assert.equal(result, false) - }) - - it('host settings is set and ledgerPayments is true', function () { - let newState = state.setIn(['siteSettings', 'https?://brave.com', 'ledgerPayments'], true) - const result = publisherUtil.enabledForPaymentsPublisher(newState, 'https://brave.com') - assert.equal(result, true) - }) - }) }) diff --git a/test/unit/app/common/state/pageDataStateTest.js b/test/unit/app/common/state/pageDataStateTest.js index 3ef0a42d0e5..f2eef85aaf1 100644 --- a/test/unit/app/common/state/pageDataStateTest.js +++ b/test/unit/app/common/state/pageDataStateTest.js @@ -88,6 +88,7 @@ describe('pageDataState unit tests', function () { const result = pageDataState.addView(state) const expectedResult = state .setIn(['pageData', 'last', 'tabId'], null) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: now.getTime(), url: null, @@ -103,11 +104,11 @@ describe('pageDataState unit tests', function () { url: 'https://brave.com', tabId: 1 })) - const result = pageDataState.addView(state, 'https://brave.com', 1) + const result = pageDataState.addView(newState, 'https://brave.com', 1) const expectedResult = newState .setIn(['pageData', 'last', 'tabId'], 1) - assert.deepEqual(result, expectedResult) + assert.deepEqual(result.toJS(), expectedResult.toJS()) }) it('url is private', function () { @@ -116,6 +117,7 @@ describe('pageDataState unit tests', function () { const result = pageDataState.addView(state, 'https://brave.com', 1) const expectedResult = state .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: now.getTime(), url: null, @@ -129,6 +131,7 @@ describe('pageDataState unit tests', function () { const result = pageDataState.addView(state, 'about:history', 1) const expectedResult = state .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], null) .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: now.getTime(), url: null, @@ -142,6 +145,7 @@ describe('pageDataState unit tests', function () { const result = pageDataState.addView(state, 'https://brave.com', 1) const expectedResult = state .setIn(['pageData', 'last', 'tabId'], 1) + .setIn(['pageData', 'last', 'url'], 'https://brave.com') .setIn(['pageData', 'view'], Immutable.fromJS({ timestamp: now.getTime(), url: 'https://brave.com', diff --git a/test/unit/app/renderer/components/navigation/navigationBarTest.js b/test/unit/app/renderer/components/navigation/navigationBarTest.js index 54a9d525f9a..64175a6c4cc 100644 --- a/test/unit/app/renderer/components/navigation/navigationBarTest.js +++ b/test/unit/app/renderer/components/navigation/navigationBarTest.js @@ -18,18 +18,9 @@ class urlBarFake extends React.Component { } const fakeAppState = Immutable.fromJS({ - locationInfo: { - 'https://brave.com': { - exclude: false, - publisher: 'brave.com', - stickyP: false, - timestamp: 1496942403068, - verified: true - } - }, - publisherInfo: { + ledger: { synopsis: { - 0: { + 'brave.com': { daysSpent: 0, duration: 623405, faviconURL: '', @@ -44,6 +35,15 @@ const fakeAppState = Immutable.fromJS({ views: 1, weight: 100 } + }, + locations: { + 'https://brave.com': { + exclude: false, + publisher: 'brave.com', + stickyP: false, + timestamp: 1496942403068, + verified: true + } } }, siteSettings: { diff --git a/test/unit/app/renderer/components/navigation/publisherToggleTest.js b/test/unit/app/renderer/components/navigation/publisherToggleTest.js index 44dd287e8dc..84f528526ad 100644 --- a/test/unit/app/renderer/components/navigation/publisherToggleTest.js +++ b/test/unit/app/renderer/components/navigation/publisherToggleTest.js @@ -14,31 +14,31 @@ describe('PublisherToggle component', function () { let PublisherToggle, windowStore, appStore const fakeAppState = Immutable.fromJS({ - locationInfo: { - 'https://brave.com': { - exclude: false, - publisher: 'brave.com', - stickyP: false, - timestamp: 1496942403068, - verified: true - } - }, - publisherInfo: { + ledger: { synopsis: { - 0: { - daysSpent: 0, - duration: 623405, - faviconURL: '', - hoursSpent: 0, - minutesSpent: 10, - percentage: 100, - publisherURL: 'https://brave.com', - score: 9.365888800773842, - secondsSpent: 23, - site: 'brave.com', - verified: false, - views: 1, - weight: 100 + publishers: { + 'brave.com': { + duration: 623405, + faviconURL: '', + percentage: 100, + publisherURL: 'https://brave.com', + score: 9.365888800773842, + site: 'brave.com', + options: { + verified: true + }, + visits: 1, + weight: 100 + } + } + }, + locations: { + 'https://brave.com': { + exclude: false, + publisher: 'brave.com', + stickyP: false, + timestamp: 1496942403068, + verified: true } } }, @@ -89,14 +89,14 @@ describe('PublisherToggle component', function () { describe('default behaviour (when autoSuggest is ON)', function () { it('Show as disabled if publisher is on exclusion list', function () { windowStore.state = defaultWindowStore - appStore.state = fakeAppState.setIn(['locationInfo', 'https://brave.com', 'exclude'], true) + appStore.state = fakeAppState.setIn(['ledger', 'locations', 'https://brave.com', 'exclude'], true) const wrapper = mount() assert.equal(wrapper.find('[data-test-id="publisherButton"]').length, 1) assert.equal(wrapper.find('span').props()['data-test-authorized'], false) }) - it('Show as verified if publisher is shown as verified on locationInfo list', function () { + it('Show as verified if publisher is shown as verified on ledger locations list', function () { windowStore.state = defaultWindowStore appStore.state = fakeAppState const wrapper = mount() From 63804e833676dfc313984868deed586c95c46409 Mon Sep 17 00:00:00 2001 From: Brian Clifton Date: Sat, 30 Sep 2017 12:40:30 -0700 Subject: [PATCH 4/6] Adding unit tests for ledgerReducer Auditors: @NejcZdovc --- .../app/browser/reducers/ledgerReducerTest.js | 524 ++++++++++++++++++ test/unit/app/sessionStoreTest.js | 27 +- test/unit/lib/fakeFileSystem.js | 28 + 3 files changed, 553 insertions(+), 26 deletions(-) create mode 100644 test/unit/app/browser/reducers/ledgerReducerTest.js create mode 100644 test/unit/lib/fakeFileSystem.js diff --git a/test/unit/app/browser/reducers/ledgerReducerTest.js b/test/unit/app/browser/reducers/ledgerReducerTest.js new file mode 100644 index 00000000000..57658534b63 --- /dev/null +++ b/test/unit/app/browser/reducers/ledgerReducerTest.js @@ -0,0 +1,524 @@ +/* global describe, it, before, after */ +const Immutable = require('immutable') +const assert = require('assert') +const mockery = require('mockery') +const sinon = require('sinon') +const appConstants = require('../../../../../js/constants/appConstants') +const settings = require('../../../../../js/constants/settings') +require('../../../braveUnit') + +describe('ledgerReducer unit tests', function () { + let ledgerReducer + let fakeLedgerApi + let fakeLedgerState + let appState + let paymentsEnabled + let returnedState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + const fakeLevel = () => {} + const fakeElectron = require('../../../lib/fakeElectron') + const fakeAdBlock = require('../../../lib/fakeAdBlock') + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('level', fakeLevel) + mockery.registerMock('ad-block', fakeAdBlock) + + const dummyModifyState = (state) => { + return state.set('unittest', true) + } + fakeLedgerApi = { + init: dummyModifyState, + migration: dummyModifyState, + backupKeys: dummyModifyState, + recoverKeys: dummyModifyState, + quit: dummyModifyState, + pageDataChanged: dummyModifyState, + addVisit: dummyModifyState, + + boot: () => {}, + onBootStateFile: dummyModifyState, + balanceReceived: dummyModifyState, + onWalletProperties: dummyModifyState, + paymentPresent: dummyModifyState, + addFoundClosed: dummyModifyState, + onWalletRecovery: dummyModifyState, + onBraveryProperties: dummyModifyState, + onLedgerFirstSync: dummyModifyState, + onCallback: dummyModifyState, + onTimeUntilReconcile: dummyModifyState, + run: () => {}, + onNetworkConnected: dummyModifyState + } + fakeLedgerState = { + resetSynopsis: dummyModifyState, + setRecoveryStatus: dummyModifyState + } + mockery.registerMock('../../browser/api/ledger', fakeLedgerApi) + mockery.registerMock('../../common/state/ledgerState', fakeLedgerState) + mockery.registerMock('../../../js/settings', { + getSetting: (settingKey, settingsCollection, value) => { + if (settingKey === settings.PAYMENTS_ENABLED) { + return paymentsEnabled + } + return false + } + }) + ledgerReducer = require('../../../../../app/browser/reducers/ledgerReducer') + + appState = Immutable.fromJS({ + ledger: {} + }) + }) + + after(function () { + mockery.disable() + }) + + describe('APP_SET_STATE', function () { + let migrationSpy + let initSpy + before(function () { + migrationSpy = sinon.spy(fakeLedgerApi, 'migration') + initSpy = sinon.spy(fakeLedgerApi, 'init') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_SET_STATE + })) + }) + after(function () { + migrationSpy.restore() + initSpy.restore() + }) + it('calls ledgerApi.migration', function () { + assert(migrationSpy.withArgs(appState).calledOnce) + }) + it('calls ledgerApi.init', function () { + assert(initSpy.calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_BACKUP_KEYS', function () { + let backupKeysSpy + before(function () { + backupKeysSpy = sinon.spy(fakeLedgerApi, 'backupKeys') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_BACKUP_KEYS, + backupAction: 'ActionGoesHere' + })) + }) + after(function () { + backupKeysSpy.restore() + }) + it('calls ledgerApi.backupKeys', function () { + assert(backupKeysSpy.withArgs(appState, 'ActionGoesHere').calledOnce) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, appState) + }) + }) + + describe('APP_RECOVER_WALLET', function () { + let recoverKeysSpy + before(function () { + recoverKeysSpy = sinon.spy(fakeLedgerApi, 'recoverKeys') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_RECOVER_WALLET, + useRecoveryKeyFile: 'useKeyFile', + firstRecoveryKey: 'firstKey', + secondRecoveryKey: 'secondKey' + })) + }) + after(function () { + recoverKeysSpy.restore() + }) + it('calls ledgerApi.recoverKeys', function () { + assert(recoverKeysSpy.withArgs(appState, 'useKeyFile', 'firstKey', 'secondKey').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_SHUTTING_DOWN', function () { + let quitSpy + before(function () { + quitSpy = sinon.spy(fakeLedgerApi, 'quit') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_SHUTTING_DOWN + })) + }) + after(function () { + quitSpy.restore() + }) + it('calls ledgerApi.quit', function () { + assert(quitSpy.withArgs(appState).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_CLEAR_BROWSING_DATA', function () { + let resetSynopsisSpy + let clearAppState + before(function () { + resetSynopsisSpy = sinon.spy(fakeLedgerState, 'resetSynopsis') + }) + after(function () { + resetSynopsisSpy.restore() + }) + describe('when clearData.browserHistory is true and payments is disabled', function () { + before(function () { + resetSynopsisSpy.reset() + paymentsEnabled = false + clearAppState = appState.setIn(['settings', settings.PAYMENTS_ENABLED], paymentsEnabled) + clearAppState = clearAppState.set('clearBrowsingDataDefaults', Immutable.fromJS({ + browserHistory: true + })) + returnedState = ledgerReducer(clearAppState, Immutable.fromJS({ + actionType: appConstants.APP_ON_CLEAR_BROWSING_DATA + })) + }) + it('calls ledgerState.resetSynopsis', function () { + assert(resetSynopsisSpy.withArgs(clearAppState).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, clearAppState) + }) + }) + describe('else', function () { + before(function () { + resetSynopsisSpy.reset() + paymentsEnabled = true + clearAppState = appState.setIn(['settings', settings.PAYMENTS_ENABLED], paymentsEnabled) + clearAppState = clearAppState.set('clearBrowsingDataDefaults', Immutable.fromJS({ + browserHistory: true + })) + returnedState = ledgerReducer(clearAppState, Immutable.fromJS({ + actionType: appConstants.APP_ON_CLEAR_BROWSING_DATA + })) + }) + it('does not call ledgerState.resetSynopsis', function () { + assert(resetSynopsisSpy.notCalled) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, clearAppState) + }) + }) + }) + + describe('APP_IDLE_STATE_CHANGED', function () { + let pageDataChangedSpy + let addVisitSpy + before(function () { + pageDataChangedSpy = sinon.spy(fakeLedgerApi, 'pageDataChanged') + addVisitSpy = sinon.spy(fakeLedgerApi, 'addVisit') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_IDLE_STATE_CHANGED + })) + }) + after(function () { + pageDataChangedSpy.restore() + addVisitSpy.restore() + }) + it('calls ledgerApi.pageDataChanged', function () { + assert(pageDataChangedSpy.withArgs(appState).calledOnce) + }) + it('calls ledgerApi.addVisit', function () { + assert(addVisitSpy.calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('', function () { + + }) + + describe('APP_ON_LEDGER_WALLET_CREATE', function () { + let bootSpy + before(function () { + bootSpy = sinon.spy(fakeLedgerApi, 'boot') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_LEDGER_WALLET_CREATE + })) + }) + after(function () { + bootSpy.restore() + }) + it('calls ledgerApi.boot', function () { + assert(bootSpy.calledOnce) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_BOOT_STATE_FILE', function () { + let onBootStateFileSpy + before(function () { + onBootStateFileSpy = sinon.spy(fakeLedgerApi, 'onBootStateFile') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_BOOT_STATE_FILE + })) + }) + after(function () { + onBootStateFileSpy.restore() + }) + it('calls ledgerApi.onBootStateFile', function () { + assert(onBootStateFileSpy.withArgs(appState).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_LEDGER_BALANCE_RECEIVED', function () { + let balanceReceivedSpy + before(function () { + balanceReceivedSpy = sinon.spy(fakeLedgerApi, 'balanceReceived') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_LEDGER_BALANCE_RECEIVED, + unconfirmed: true + })) + }) + after(function () { + balanceReceivedSpy.restore() + }) + it('calls ledgerApi.balanceReceived', function () { + assert(balanceReceivedSpy.withArgs(appState, true).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_WALLET_PROPERTIES', function () { + let onWalletPropertiesSpy + before(function () { + onWalletPropertiesSpy = sinon.spy(fakeLedgerApi, 'onWalletProperties') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_WALLET_PROPERTIES, + body: 'text-goes-here' + })) + }) + after(function () { + onWalletPropertiesSpy.restore() + }) + it('calls ledgerApi.onWalletProperties', function () { + assert(onWalletPropertiesSpy.withArgs(appState, 'text-goes-here').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_LEDGER_PAYMENTS_PRESENT', function () { + let paymentPresentSpy + before(function () { + paymentPresentSpy = sinon.spy(fakeLedgerApi, 'paymentPresent') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_LEDGER_PAYMENTS_PRESENT, + tabId: 123, + present: true + })) + }) + after(function () { + paymentPresentSpy.restore() + }) + it('calls ledgerApi.paymentPresent', function () { + assert(paymentPresentSpy.withArgs(appState, 123, true).calledOnce) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_ADD_FUNDS_CLOSED', function () { + let addFoundClosedSpy + before(function () { + addFoundClosedSpy = sinon.spy(fakeLedgerApi, 'addFoundClosed') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_ADD_FUNDS_CLOSED + })) + }) + after(function () { + addFoundClosedSpy.restore() + }) + it('calls ledgerApi.addFoundClosed', function () { + assert(addFoundClosedSpy.withArgs(appState).calledOnce) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_WALLET_RECOVERY', function () { + let onWalletRecoverySpy + before(function () { + onWalletRecoverySpy = sinon.spy(fakeLedgerApi, 'onWalletRecovery') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_WALLET_RECOVERY, + error: 'error-goes-here', + result: 'result-goes-here' + })) + }) + after(function () { + onWalletRecoverySpy.restore() + }) + it('calls ledgerApi.onWalletRecovery', function () { + assert(onWalletRecoverySpy.withArgs(appState, 'error-goes-here', 'result-goes-here').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_BRAVERY_PROPERTIES', function () { + let onBraveryPropertiesSpy + before(function () { + onBraveryPropertiesSpy = sinon.spy(fakeLedgerApi, 'onBraveryProperties') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_BRAVERY_PROPERTIES, + error: 'error-goes-here', + result: 'result-goes-here' + })) + }) + after(function () { + onBraveryPropertiesSpy.restore() + }) + it('calls ledgerApi.onBraveryProperties', function () { + assert(onBraveryPropertiesSpy.withArgs(appState, 'error-goes-here', 'result-goes-here').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_FIRST_LEDGER_SYNC', function () { + let onLedgerFirstSyncSpy + before(function () { + onLedgerFirstSyncSpy = sinon.spy(fakeLedgerApi, 'onLedgerFirstSync') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_FIRST_LEDGER_SYNC, + parsedData: 'parsed-data-goes-here' + })) + }) + after(function () { + onLedgerFirstSyncSpy.restore() + }) + it('calls ledgerApi.onLedgerFirstSync', function () { + assert(onLedgerFirstSyncSpy.withArgs(appState, 'parsed-data-goes-here').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_LEDGER_CALLBACK', function () { + let onCallbackSpy + before(function () { + onCallbackSpy = sinon.spy(fakeLedgerApi, 'onCallback') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_LEDGER_CALLBACK, + result: 'result-goes-here', + delayTime: 39 + })) + }) + after(function () { + onCallbackSpy.restore() + }) + it('calls ledgerApi.onCallback', function () { + assert(onCallbackSpy.withArgs(appState, 'result-goes-here', 39).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_TIME_UNTIL_RECONCILE', function () { + let onTimeUntilReconcileSpy + before(function () { + onTimeUntilReconcileSpy = sinon.spy(fakeLedgerApi, 'onTimeUntilReconcile') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_TIME_UNTIL_RECONCILE, + stateResult: 'state-result-goes-here' + })) + }) + after(function () { + onTimeUntilReconcileSpy.restore() + }) + it('calls ledgerApi.onTimeUntilReconcile', function () { + assert(onTimeUntilReconcileSpy.withArgs(appState, 'state-result-goes-here').calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_LEDGER_RUN', function () { + let runSpy + before(function () { + runSpy = sinon.spy(fakeLedgerApi, 'run') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_LEDGER_RUN, + delay: 7 + })) + }) + after(function () { + runSpy.restore() + }) + it('calls ledgerApi.run', function () { + assert(runSpy.withArgs(appState, 7).calledOnce) + }) + it('returns an ununmodified state', function () { + assert.deepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_NETWORK_CONNECTED', function () { + let onNetworkConnectedSpy + before(function () { + onNetworkConnectedSpy = sinon.spy(fakeLedgerApi, 'onNetworkConnected') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_NETWORK_CONNECTED + })) + }) + after(function () { + onNetworkConnectedSpy.restore() + }) + it('calls ledgerApi.onNetworkConnected', function () { + assert(onNetworkConnectedSpy.withArgs(appState).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) + + describe('APP_ON_RESET_RECOVERY_STATUS', function () { + let setRecoveryStatusSpy + before(function () { + setRecoveryStatusSpy = sinon.spy(fakeLedgerState, 'setRecoveryStatus') + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_RESET_RECOVERY_STATUS + })) + }) + after(function () { + setRecoveryStatusSpy.restore() + }) + it('calls ledgerApi.setRecoveryStatus', function () { + assert(setRecoveryStatusSpy.withArgs(appState, null).calledOnce) + }) + it('returns a modified state', function () { + assert.notDeepEqual(returnedState, appState) + }) + }) +}) diff --git a/test/unit/app/sessionStoreTest.js b/test/unit/app/sessionStoreTest.js index 2c7e3a8df91..47523cb4dec 100644 --- a/test/unit/app/sessionStoreTest.js +++ b/test/unit/app/sessionStoreTest.js @@ -43,32 +43,7 @@ describe('sessionStore unit tests', function () { const fakeWindowState = { getPersistentState: (data) => { return makeImmutable(data) } } - const fakeFileSystem = { - readFileSync: (path) => { - return JSON.stringify({ - cleanedOnShutdown: false - }) - }, - writeFile: (path, options, callback) => { - console.log('calling mocked fs.writeFile') - callback() - }, - rename: (oldPath, newPath, callback) => { - console.log('calling mocked fs.rename') - callback() - }, - copySync: (oldPath, newPath) => { - console.log('calling mocked fs.copySync') - }, - existsSync: (path) => { - console.log('calling mocked fs.existsSync') - return true - }, - remove: (path, callback) => { - console.log('calling mocked fs.remove') - if (callback) callback() - } - } + const fakeFileSystem = require('../lib/fakeFileSystem') const fakeLocale = { init: (language) => { return new Promise((resolve, reject) => { diff --git a/test/unit/lib/fakeFileSystem.js b/test/unit/lib/fakeFileSystem.js new file mode 100644 index 00000000000..cf2a25d2454 --- /dev/null +++ b/test/unit/lib/fakeFileSystem.js @@ -0,0 +1,28 @@ +const fakeFileSystem = { + readFileSync: (path) => { + return JSON.stringify({ + cleanedOnShutdown: false + }) + }, + writeFile: (path, options, callback) => { + console.log('calling mocked fs.writeFile') + callback() + }, + rename: (oldPath, newPath, callback) => { + console.log('calling mocked fs.rename') + callback() + }, + copySync: (oldPath, newPath) => { + console.log('calling mocked fs.copySync') + }, + existsSync: (path) => { + console.log('calling mocked fs.existsSync') + return true + }, + remove: (path, callback) => { + console.log('calling mocked fs.remove') + if (callback) callback() + } +} + +module.exports = fakeFileSystem From 22bb84e3c923c173ca0d01c7ff5688a4d19c8d2a Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Mon, 2 Oct 2017 05:40:21 +0200 Subject: [PATCH 5/6] Addresses review comments Part of #11037 PR --- app/browser/api/ledger.js | 189 ++++++++---------- app/browser/reducers/ledgerReducer.js | 16 +- app/common/lib/ledgerUtil.js | 14 +- app/common/state/ledgerState.js | 24 +-- .../preferences/payment/ledgerTable.js | 3 +- js/actions/appActions.js | 12 +- js/constants/appConstants.js | 3 +- test/about/ledgerPanelTest.js | 12 +- 8 files changed, 121 insertions(+), 152 deletions(-) diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index a25ad3b9038..a5d54831a5c 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -15,7 +15,7 @@ const qr = require('qr-image') const underscore = require('underscore') const tldjs = require('tldjs') const urlFormat = require('url').format -const queryString = require('queryString') +const queryString = require('querystring') const levelUp = require('level') const random = require('random-lib') const uuid = require('uuid') @@ -51,8 +51,8 @@ let bootP let quitP let notificationPaymentDoneMessage const _internal = { - verboseP: true, - debugP: true, + verboseP: process.env.LEDGER_VERBOSE || true, + debugP: process.env.LEDGER_DEBUG || true, ruleset: { raw: [], cooked: [] @@ -102,6 +102,8 @@ const fileTypes = { jpeg: new Buffer([0xff, 0xd8, 0xff]), png: new Buffer([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) } +const minimumVisitTimeDefault = 8 * 1000 +const nextAddFoundsTime = 3 * miliseconds.day let signatureMax = 0 underscore.keys(fileTypes).forEach((fileType) => { @@ -327,6 +329,7 @@ const getPublisherData = (result, scorekeeper) => { let data = { verified: result.options.verified || false, + exclude: result.options.exclude || false, site: result.publisherKey, views: result.visits, duration: duration, @@ -694,13 +697,13 @@ const addVisit = (state, location, timestamp, tabId) => { const lastUrl = pageDataState.getLastUrl(state) const aboutUrl = getSourceAboutUrl(lastUrl) || lastUrl - if (aboutUrl.match(/^about/)) { + if (aboutUrl && aboutUrl.match(/^about/)) { state = pageDataState.resetInfo(state) } location = getSourceAboutUrl(location) || location - currentUrl = location.match(/^about/) ? locationDefault : location + currentUrl = (location && location.match(/^about/)) ? locationDefault : location currentTimestamp = timestamp return state } @@ -887,11 +890,10 @@ const pageDataChanged = (state) => { if (initP) { excludeP(publisherKey, (unused, exclude) => { if (!getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST)) { - exclude = false - } else { - exclude = !exclude + exclude = true } - appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude, true) + appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + savePublisherOption(publisherKey, 'exclude', exclude) }) } @@ -1033,7 +1035,7 @@ const initSynopsis = (state) => { state = ledgerState.saveSynopsis(state, null, synopsis.options) let value = getSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME) if (!value) { - value = 8 * 1000 + value = minimumVisitTimeDefault appActions.changeSetting(settings.PAYMENTS_MINIMUM_VISIT_TIME, value) } @@ -1079,6 +1081,7 @@ const initSynopsis = (state) => { const publisherKey = item[0] excludeP(publisherKey, (unused, exclude) => { appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + savePublisherOption(publisherKey, 'exclude', exclude) }) state = verifiedP(state, publisherKey, (error, result) => { @@ -1132,9 +1135,6 @@ const enable = (state, paymentsEnabled) => { } }) - // change undefined include publishers to include publishers - state = ledgerState.enableUndefinedPublishers(state, stateSynopsis.get('publishers')) - return state } @@ -1179,7 +1179,7 @@ const showNotificationReviewPublishers = (nextTime) => { } const showNotificationAddFunds = () => { - const nextTime = new Date().getTime() + (3 * miliseconds.day) + const nextTime = new Date().getTime() + nextAddFoundsTime appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) appActions.showNotification({ @@ -1435,32 +1435,6 @@ const updateLedgerInfo = (state) => { state = ledgerState.setInfoProp(state, 'buyMaximumUSD', false) } - // TODO remove when BAT is implemented, we don't need this for BAT - /* - if ((client) && (now > ledgerInfo._internal.geoipExpiry)) { - ledgerInfo._internal.geoipExpiry = now + (5 * miliseconds.minute) - - if (!ledgerGeoIP) ledgerGeoIP = require('ledger-geoip') - return ledgerGeoIP.getGeoIP(client.options, (err, provider, result) => { - if (err) console.warn('ledger geoip warning: ' + JSON.stringify(err, null, 2)) - if (result) ledgerInfo.countryCode = result - - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - - if (now <= ledgerInfo._internal.exchangeExpiry) return updateLedgerInfo() - - ledgerInfo._internal.exchangeExpiry = now + miliseconds.day - roundtrip({ path: '/v1/exchange/providers' }, client.options, (err, response, body) => { - if (err) console.error('ledger exchange error: ' + JSON.stringify(err, null, 2)) - - ledgerInfo._internal.exchanges = body || {} - ledgerInfo.exchangeInfo = ledgerInfo._internal.exchanges[ledgerInfo.countryCode] - updateLedgerInfo() - }) - }) - } - */ - return state } @@ -1793,6 +1767,7 @@ const onCallback = (state, result, delayTime) => { const publisherKey = item[0] excludeP(publisherKey, (unused, exclude) => { appActions.onPublisherOptionUpdate(publisherKey, 'exclude', exclude) + savePublisherOption(publisherKey, 'exclude', exclude) }) } }) @@ -1860,80 +1835,91 @@ const initialize = (state, paymentsEnabled) => { try { const fs = require('fs') - // TODO change this back to async - fs.accessSync(pathName(statePath), fs.FF_OK) - const data = fs.readFileSync(pathName(statePath)) - let parsedData - - try { - parsedData = JSON.parse(data) - if (clientOptions.verboseP) { - console.log('\nstarting up ledger client integration') + fs.access(pathName(statePath), fs.FF_OK, (err) => { + if (err) { + return } - } catch (ex) { - console.error('statePath parse error: ' + ex.toString()) - return state - } - state = getStateInfo(state, parsedData) - - try { - let timeUntilReconcile - clientprep() - client = ledgerClient(parsedData.personaId, - underscore.extend(parsedData.options, {roundtrip: roundtrip}, clientOptions), - parsedData) - - // Scenario: User enables Payments, disables it, waits 30+ days, then - // enables it again -> reconcileStamp is in the past. - // In this case reset reconcileStamp to the future. - try { - timeUntilReconcile = client.timeUntilReconcile() - } catch (ex) {} - - let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') - if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { - client.setTimeUntilReconcile(null, (err, stateResult) => { - if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) + fs.readFile(pathName(statePath), (err, data) => { + if (err) { + return console.error('read error: ' + err.toString()) + } - if (!stateResult) { - return + try { + appActions.onInitRead(JSON.parse(data)) + if (clientOptions.verboseP) { + console.log('\nstarting up ledger client integration') } + } catch (ex) { + console.error('statePath parse error: ' + ex.toString()) + } + }) + }) - appActions.onTimeUntilReconcile(stateResult) - }) - } - } catch (ex) { - console.error('ledger client creation error: ', ex) - return state + return state + } catch (err) { + if (err.code !== 'ENOENT') { + console.error('statePath read error: ' + err.toString()) } + state = ledgerState.resetInfo(state) + return state + } +} - // speed-up browser start-up by delaying the first synchronization action - setTimeout(() => { - if (!client) { - return - } +const onInitRead = (state, parsedData) => { + parsedData = parsedData.toJS() + state = getStateInfo(state, parsedData) - appActions.onLedgerFirstSync(parsedData) - }, 3 * miliseconds.second) + try { + let timeUntilReconcile + clientprep() + client = ledgerClient(parsedData.personaId, + underscore.extend(parsedData.options, {roundtrip: roundtrip}, clientOptions), + parsedData) - // Make sure bravery props are up-to-date with user settings - const address = ledgerState.getInfoProp(state, 'address') - if (address) { - state = ledgerState.setInfoProp(state, 'address', client.getWalletAddress()) - } + // Scenario: User enables Payments, disables it, waits 30+ days, then + // enables it again -> reconcileStamp is in the past. + // In this case reset reconcileStamp to the future. + try { + timeUntilReconcile = client.timeUntilReconcile() + } catch (ex) {} - setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) - getBalance(state) + let ledgerWindow = (ledgerState.getSynopsisOption(state, 'numFrames') - 1) * ledgerState.getSynopsisOption(state, 'frameSize') + if (typeof timeUntilReconcile === 'number' && timeUntilReconcile < -ledgerWindow) { + client.setTimeUntilReconcile(null, (err, stateResult) => { + if (err) return console.error('ledger setTimeUntilReconcile error: ' + err.toString()) - return state - } catch (err) { - if (err.code !== 'ENOENT') { - console.error('statePath read error: ' + err.toString()) + if (!stateResult) { + return + } + + appActions.onTimeUntilReconcile(stateResult) + }) } - state = ledgerState.resetInfo(state) + } catch (ex) { + console.error('ledger client creation error: ', ex) return state } + + // speed-up browser start-up by delaying the first synchronization action + setTimeout(() => { + if (!client) { + return + } + + appActions.onLedgerFirstSync(parsedData) + }, 3 * miliseconds.second) + + // Make sure bravery props are up-to-date with user settings + const address = ledgerState.getInfoProp(state, 'address') + if (address) { + state = ledgerState.setInfoProp(state, 'address', client.getWalletAddress()) + } + + setPaymentInfo(getSetting(settings.PAYMENTS_CONTRIBUTION_AMOUNT)) + getBalance(state) + + return state } const onTimeUntilReconcile = (state, stateResult) => { @@ -2183,5 +2169,6 @@ module.exports = { onTimeUntilReconcile, run, onNetworkConnected, - migration + migration, + onInitRead } diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index f9b313b97e7..a9639b6881f 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -11,11 +11,9 @@ const settings = require('../../../js/constants/settings') // State const ledgerState = require('../../common/state/ledgerState') -const siteSettingsState = require('../../common/state/siteSettingsState') // Utils const ledgerApi = require('../../browser/api/ledger') -const urlUtil = require('../../../js/lib/urlutil') const {makeImmutable} = require('../../common/state/immutableUtil') const getSetting = require('../../../js/settings').getSetting @@ -209,10 +207,8 @@ const ledgerReducer = (state, action, immutableAction) => { case appConstants.APP_ON_EXCLUSION_STATUS: { const key = action.get('publisherKey') - const pattern = urlUtil.getHostPattern(key) const value = action.get('excluded') ledgerApi.savePublisherOption(key, 'exclude', value) - state = siteSettingsState.setSettingsProp(state, pattern, 'ledgerPayments', value) state = ledgerState.setPublishersProp(state, key, ['options', 'exclude'], value) state = ledgerApi.updatePublisherInfo(state) break @@ -229,13 +225,6 @@ const ledgerReducer = (state, action, immutableAction) => { const key = action.get('publisherKey') const prop = action.get('prop') state = ledgerState.setPublisherOption(state, key, prop, value) - - if (action.get('saveIntoSettings')) { - const pattern = urlUtil.getHostPattern(key) - if (prop === 'exclude') { - state = siteSettingsState.setSettingsProp(state, pattern, 'ledgerPayments', value) - } - } break } case appConstants.APP_ON_LEDGER_WALLET_CREATE: @@ -308,6 +297,11 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerState.setRecoveryStatus(state, null) break } + case appConstants.APP_ON_LEDGER_INIT_READ: + { + state = ledgerApi.onInitRead(state, action.get('parsedData')) + break + } } return state } diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 9293983c16e..7857321b487 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -161,17 +161,11 @@ const visibleP = (state, publisherKey) => { // Publisher Options const deletedByUser = blockedP(state, publisherKey) - const includeExclude = stickyP(state, publisherKey) const eligibleByStats = eligibleP(state, publisherKey) // num of visits and time spent - const isInExclusionList = publisherOptions.get('exclude') const verifiedPublisher = publisherOptions.get('verified') return ( eligibleByStats && - ( - isInExclusionList !== true || - includeExclude - ) && ( (onlyVerified && verifiedPublisher) || !onlyVerified @@ -199,6 +193,14 @@ const stickyP = (state, publisherKey) => { const pattern = urlUtil.getHostPattern(publisherKey) let result = siteSettingsState.getSettingsProp(state, pattern, 'ledgerPayments') + if (result == null) { + const excluded = ledgerState.getPublisherOption(state, publisherKey, 'exclude') + + if (excluded != null) { + result = !excluded + } + } + return (result === undefined || result) } diff --git a/app/common/state/ledgerState.js b/app/common/state/ledgerState.js index 3cbf4a5d31f..04bfff9dbdd 100644 --- a/app/common/state/ledgerState.js +++ b/app/common/state/ledgerState.js @@ -171,7 +171,7 @@ const ledgerState = { state = validateState(state) if (key == null || prop == null) { - return state + return null } return state.getIn(['ledger', 'synopsis', 'publishers', key, 'options', prop]) @@ -298,28 +298,6 @@ const ledgerState = { return state }, - enableUndefinedPublishers: (state, publishers) => { - state = validateState(state) - const sitesObject = state.get('siteSettings') - - if (publishers == null) { - return state - } - - for (let item of publishers) { - const key = item[0] - const pattern = urlUtil.getHostPattern(key) - const result = sitesObject.getIn([pattern, 'ledgerPayments']) - - if (result === undefined) { - const newSiteSettings = siteSettings.mergeSiteSetting(state.get('siteSettings'), pattern, 'ledgerPayments', true) - state = state.set('siteSettings', newSiteSettings) - } - } - - return state - }, - // TODO (optimization) don't have two almost identical object in state (synopsi->publishers and about->synopsis) saveAboutSynopsis: (state, publishers) => { state = validateState(state) diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index db1a74df1f0..9f8b95f4806 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -70,7 +70,8 @@ class LedgerTable extends ImmutableComponent { return result } } - return getSetting(settings.PAYMENTS_SITES_AUTO_SUGGEST, this.props.settings) + + return !synopsis.get('exclude') } shouldShow (synopsis) { diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 6b40fcdf082..7f13cf68058 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1576,13 +1576,12 @@ const appActions = { }) }, - onPublisherOptionUpdate: function (publisherKey, prop, value, saveIntoSettings = false) { + onPublisherOptionUpdate: function (publisherKey, prop, value) { dispatch({ actionType: appConstants.APP_ON_PUBLISHER_OPTION_UPDATE, publisherKey, prop, - value, - saveIntoSettings + value }) }, @@ -1692,6 +1691,13 @@ const appActions = { }) }, + onInitRead: function (parsedData) { + dispatch({ + actionType: appConstants.APP_ON_LEDGER_INIT_READ, + parsedData + }) + }, + onPinnedTabReorder: function (siteKey, destinationKey, prepend) { dispatch({ actionType: appConstants.APP_ON_PINNED_TAB_REORDER, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 0f37e816275..93e26fb4255 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -169,7 +169,8 @@ const appConstants = { APP_ON_TIME_UNTIL_RECONCILE: _, APP_ON_LEDGER_RUN: _, APP_ON_NETWORK_CONNECTED: _, - APP_ON_RESET_RECOVERY_STATUS: _ + APP_ON_RESET_RECOVERY_STATUS: _, + APP_ON_LEDGER_INIT_READ: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/test/about/ledgerPanelTest.js b/test/about/ledgerPanelTest.js index 7c10708cfd4..eb3cf8f9453 100644 --- a/test/about/ledgerPanelTest.js +++ b/test/about/ledgerPanelTest.js @@ -309,12 +309,12 @@ describe('Regular payment panel tests', function () { .windowByUrl(Brave.browserWindowUrl) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://example.com'].ledgerPayments === true + return val.value.ledger.synopsis.publishers['example.com'].options.exclude === false }) }) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://eff.org'].ledgerPayments === true + return val.value.ledger.synopsis.publishers['eff.org'].options.exclude === false }) }) }) @@ -339,12 +339,12 @@ describe('Regular payment panel tests', function () { .windowByUrl(Brave.browserWindowUrl) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://example.com'].ledgerPayments === false + return val.value.ledger.synopsis.publishers['example.com'].options.exclude === true }) }) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://eff.org'].ledgerPayments === false + return val.value.ledger.synopsis.publishers['eff.org'].options.exclude === true }) }) }) @@ -370,12 +370,12 @@ describe('Regular payment panel tests', function () { .windowByUrl(Brave.browserWindowUrl) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://example.com'].ledgerPayments === true + return val.value.ledger.synopsis.publishers['example.com'].options.exclude === false }) }) .waitUntil(function () { return this.getAppState().then((val) => { - return val.value.siteSettings['https?://eff.org'].ledgerPayments === false + return val.value.ledger.synopsis.publishers['eff.org'].options.exclude === true }) }) }) From a4deb1b3290557f6a4a340836fa0e0171b683480 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Tue, 3 Oct 2017 08:27:01 +0200 Subject: [PATCH 6/6] Temporary fix for startup problem --- app/browser/api/ledger.js | 1 - app/browser/reducers/ledgerReducer.js | 19 ++++++++++++++----- js/stores/appStore.js | 19 +++++++++++++++---- .../app/browser/reducers/ledgerReducerTest.js | 8 ++++---- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index a5d54831a5c..651b1aed501 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -1867,7 +1867,6 @@ const initialize = (state, paymentsEnabled) => { } const onInitRead = (state, parsedData) => { - parsedData = parsedData.toJS() state = getStateInfo(state, parsedData) try { diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index a9639b6881f..934c3bda992 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -18,8 +18,17 @@ const {makeImmutable} = require('../../common/state/immutableUtil') const getSetting = require('../../../js/settings').getSetting const ledgerReducer = (state, action, immutableAction) => { - action = immutableAction || makeImmutable(action) - switch (action.get('actionType')) { + let actionType = action.actionType + if ( + action.actionType !== appConstants.APP_ON_FIRST_LEDGER_SYNC && + action.actionType !== appConstants.APP_ON_BRAVERY_PROPERTIES && + action.actionType !== appConstants.APP_ON_LEDGER_INIT_READ + ) { + action = immutableAction || makeImmutable(action) + actionType = action.get('actionType') + } + + switch (actionType) { case appConstants.APP_SET_STATE: { state = ledgerApi.migration(state) @@ -264,12 +273,12 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_BRAVERY_PROPERTIES: { - state = ledgerApi.onBraveryProperties(state, action.get('error'), action.get('result')) + state = ledgerApi.onBraveryProperties(state, action.error, action.result) break } case appConstants.APP_ON_FIRST_LEDGER_SYNC: { - state = ledgerApi.onLedgerFirstSync(state, action.get('parsedData')) + state = ledgerApi.onLedgerFirstSync(state, action.parsedData) break } case appConstants.APP_ON_LEDGER_CALLBACK: @@ -299,7 +308,7 @@ const ledgerReducer = (state, action, immutableAction) => { } case appConstants.APP_ON_LEDGER_INIT_READ: { - state = ledgerApi.onInitRead(state, action.get('parsedData')) + state = ledgerApi.onInitRead(state, action.parsedData) break } } diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 7039fc699cf..ed2ca3a86e8 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -173,6 +173,7 @@ const applyReducers = (state, action, immutableAction) => reducers.reduce( }, appState) const handleAppAction = (action) => { + const ledgerReducer = require('../../app/browser/reducers/ledgerReducer') const timeStart = process.hrtime() if (action.actionType === appConstants.APP_SET_STATE) { reducers = [ @@ -203,7 +204,7 @@ const handleAppAction = (action) => { require('../../app/browser/reducers/bookmarkToolbarReducer'), require('../../app/browser/reducers/siteSettingsReducer'), require('../../app/browser/reducers/pageDataReducer'), - require('../../app/browser/reducers/ledgerReducer'), + ledgerReducer, require('../../app/browser/menu') ] initialized = true @@ -215,9 +216,19 @@ const handleAppAction = (action) => { return } - // maintain backwards compatibility for now by adding an additional param for immutableAction - const immutableAction = makeImmutable(action) - appState = applyReducers(appState, action, immutableAction) + let immutableAction = Immutable.Map() + // exclude big chucks that have regular JS in it + if ( + action.actionType === appConstants.APP_ON_FIRST_LEDGER_SYNC || + action.actionType === appConstants.APP_ON_BRAVERY_PROPERTIES || + action.actionType === appConstants.APP_ON_LEDGER_INIT_READ + ) { + appState = ledgerReducer(appState, action, immutableAction) + } else { + // maintain backwards compatibility for now by adding an additional param for immutableAction + immutableAction = makeImmutable(action) + appState = applyReducers(appState, action, immutableAction) + } switch (action.actionType) { case appConstants.APP_SET_STATE: diff --git a/test/unit/app/browser/reducers/ledgerReducerTest.js b/test/unit/app/browser/reducers/ledgerReducerTest.js index 57658534b63..faae36e58d4 100644 --- a/test/unit/app/browser/reducers/ledgerReducerTest.js +++ b/test/unit/app/browser/reducers/ledgerReducerTest.js @@ -386,11 +386,11 @@ describe('ledgerReducer unit tests', function () { let onBraveryPropertiesSpy before(function () { onBraveryPropertiesSpy = sinon.spy(fakeLedgerApi, 'onBraveryProperties') - returnedState = ledgerReducer(appState, Immutable.fromJS({ + returnedState = ledgerReducer(appState, { actionType: appConstants.APP_ON_BRAVERY_PROPERTIES, error: 'error-goes-here', result: 'result-goes-here' - })) + }) }) after(function () { onBraveryPropertiesSpy.restore() @@ -407,10 +407,10 @@ describe('ledgerReducer unit tests', function () { let onLedgerFirstSyncSpy before(function () { onLedgerFirstSyncSpy = sinon.spy(fakeLedgerApi, 'onLedgerFirstSync') - returnedState = ledgerReducer(appState, Immutable.fromJS({ + returnedState = ledgerReducer(appState, { actionType: appConstants.APP_ON_FIRST_LEDGER_SYNC, parsedData: 'parsed-data-goes-here' - })) + }) }) after(function () { onLedgerFirstSyncSpy.restore()