From 3064556599f21db702fd0c0f3554e33349f14b06 Mon Sep 17 00:00:00 2001 From: NejcZdovc Date: Sat, 23 Sep 2017 12:21:19 +0200 Subject: [PATCH] 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()