diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 2a7c7f57806..b770e693948 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -8,7 +8,6 @@ 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') @@ -30,12 +29,10 @@ const migrationState = require('../../common/state/migrationState') // 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, isSourceAboutUrl} = require('../../../js/lib/appUrlUtil') const urlParse = require('../../common/urlParse') @@ -44,6 +41,7 @@ const request = require('../../../js/lib/request') const ledgerUtil = require('../../common/lib/ledgerUtil') const tabState = require('../../common/state/tabState') const pageDataUtil = require('../../common/lib/pageDataUtil') +const ledgerNotifications = require('./ledgerNotifications') // Caching let locationDefault = 'NOOP' @@ -103,320 +101,12 @@ const fileTypes = { png: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) } const minimumVisitTimeDefault = 8 * 1000 -const nextAddFundsTime = 3 * miliseconds.day let signatureMax = 0 underscore.keys(fileTypes).forEach((fileType) => { if (signatureMax < fileTypes[fileType].length) signatureMax = fileTypes[fileType].length }) signatureMax = Math.ceil(signatureMax * 1.5) -// Notifications -const sufficientBalanceToReconcile = (state) => { - const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) - const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0) - const bat = ledgerState.getInfoProp(state, 'bat') - return bat && (balance + unconfirmed > 0.9 * Number(bat)) -} -const hasFunds = (state) => { - const balance = getSetting(settings.PAYMENTS_ENABLED) - ? Number(ledgerState.getInfoProp(state, 'balance') || 0) - : 0 - return balance > 0 -} -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 notifications = { - text: { - hello: locale.translation('updateHello'), - paymentDone: undefined, - addFunds: locale.translation('addFundsNotification'), - tryPayments: locale.translation('notificationTryPayments'), - reconciliation: locale.translation('reconciliationNotification'), - walletConvertedToBat: locale.translation('walletConvertedToBat') - }, - pollingInterval: 15 * miliseconds.minutes, - timeout: undefined, - displayOptions: { - style: 'greetingStyle', - persist: false - }, - init: (state) => { - // Check if relevant browser notifications should be shown every 15 minutes - if (notifications.timeout) { - clearInterval(notifications.timeout) - } - notifications.timeout = setInterval((state) => { - notifications.onInterval(state) - }, notifications.pollingInterval, state) - }, - onLaunch: (state) => { - const enabled = getSetting(settings.PAYMENTS_ENABLED) - if (!enabled) { - return state - } - - state = checkBtcBatMigrated(state, enabled) - - if (hasFunds(state)) { - // Don't bother processing the rest, which are only notifications. - if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) { - return state - } - - // Show one-time BAT conversion message: - // - if payments are enabled - // - user has a positive balance - // - this is an existing profile (new profiles will have firstRunTimestamp matching batMercuryTimestamp) - // - wallet has been transitioned - // - notification has not already been shown yet - // (see https://github.com/brave/browser-laptop/issues/11021) - const isNewInstall = migrationState.isNewInstall(state) - const hasUpgradedWallet = migrationState.hasUpgradedWallet(state) - const hasBeenNotified = migrationState.hasBeenNotified(state) - if (!isNewInstall && hasUpgradedWallet && !hasBeenNotified) { - notifications.showBraveWalletUpdated() - } - } - - return state - }, - onInterval: (state) => { - if (getSetting(settings.PAYMENTS_ENABLED)) { - if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { - notifications.showEnabledNotifications(state) - } - } else { - notifications.showDisabledNotifications(state) - } - }, - onResponse: (message, buttonIndex, activeWindow) => { - switch (message) { - case notifications.text.addFunds: - // 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 && activeWindow) { - // Add funds: Open payments panel - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: activeWindow.id - }) - } - break - - case notifications.text.reconciliation: - // buttonIndex === 1 is Dismiss - if (buttonIndex === 0) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) - } else if (buttonIndex === 2 && activeWindow) { - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: activeWindow.id - }) - } - break - - case notifications.text.paymentDone: - if (buttonIndex === 0) { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) - } - break - - case notifications.text.tryPayments: - if (buttonIndex === 1 && activeWindow) { - appActions.createTabRequested({ - url: 'about:preferences#payments', - windowId: activeWindow.id - }) - } - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) - break - - case notifications.text.walletConvertedToBat: - if (buttonIndex === 0) { - // Open backup modal - appActions.createTabRequested({ - url: 'about:preferences#payments?ledgerBackupOverlayVisible', - windowId: activeWindow.id - }) - } - break - - default: - return - } - - appActions.hideNotification(message) - }, - /** - * 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. - */ - 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') - notifications.showReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day)) - } - } else if (shouldShowNotificationAddFunds()) { - notifications.showAddFunds() - } - } else if (reconcileStamp - new Date().getTime() < 2 * miliseconds.day) { - if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { - notifications.showReviewPublishers(new Date().getTime() + miliseconds.day) - } - } - }, - 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({ - from: 'ledger', - greeting: locale.translation('updateHello'), - message: notifications.text.tryPayments, - buttons: [ - {text: locale.translation('noThanks')}, - {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} - ], - options: { - style: 'greetingStyle', - persist: false - } - }) - } - }, - showReviewPublishers: (nextTime) => { - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) - - appActions.showNotification({ - from: 'ledger', - greeting: notifications.text.hello, - message: notifications.text.reconciliation, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('dismiss')}, - {text: locale.translation('reviewSites'), className: 'primaryButton'} - ], - options: notifications.displayOptions - }) - }, - showAddFunds: () => { - const nextTime = new Date().getTime() + nextAddFundsTime - appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) - - appActions.showNotification({ - from: 'ledger', - greeting: notifications.text.hello, - message: notifications.text.addFunds, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('updateLater')}, - {text: locale.translation('addFunds'), className: 'primaryButton'} - ], - options: notifications.displayOptions - }) - }, - // Called from observeTransactions() when we see a new payment (transaction). - showPaymentDone: (transactionContributionFiat) => { - notifications.text.paymentDone = 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(notifications.text.addFunds) - appActions.showNotification({ - from: 'ledger', - greeting: locale.translation('updateHello'), - message: notifications.text.paymentDone, - buttons: [ - {text: locale.translation('turnOffNotifications')}, - {text: locale.translation('Ok'), className: 'primaryButton'} - ], - options: notifications.displayOptions - }) - }, - showBraveWalletUpdated: () => { - appActions.onBitcoinToBatNotified() - - appActions.showNotification({ - from: 'ledger', - greeting: notifications.text.hello, - message: notifications.text.walletConvertedToBat, - // Learn More. - buttons: [ - {text: locale.translation('walletConvertedBackup')}, - {text: locale.translation('walletConvertedDismiss')} - ], - options: { - style: 'greetingStyle', - persist: false, - advancedLink: 'https://brave.com/faq-payments/#brave-payments', - advancedText: locale.translation('walletConvertedLearnMore') - } - }) - } -} - -// 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) => { - notifications.onResponse( - message, - buttonIndex, - electron.BrowserWindow.getActiveWindow() - ) - }) -} - let ledgerPaymentsPresent = {} const paymentPresent = (state, tabId, present) => { if (present) { @@ -1573,7 +1263,7 @@ const observeTransactions = (state, transactions) => { if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { if (transactions.length > 0) { const newestTransaction = transactions[transactions.length - 1] - notifications.showPaymentDone(newestTransaction.contribution.fiat) + ledgerNotifications.showPaymentDone(newestTransaction.contribution.fiat) } } } @@ -1946,7 +1636,7 @@ const initialize = (state, paymentsEnabled) => { if (!v2RulesetDB) v2RulesetDB = levelUp(pathName(v2RulesetPath)) state = enable(state, paymentsEnabled) - notifications.init(state) + ledgerNotifications.init(state) if (!paymentsEnabled) { client = null @@ -2090,7 +1780,7 @@ const onInitRead = (state, parsedData) => { getBalance(state) // Show relevant browser notifications on launch - state = notifications.onLaunch(state) + state = ledgerNotifications.onLaunch(state) return state } @@ -2450,7 +2140,7 @@ const transitionWalletToBat = () => { // NOTE: onLedgerCallback will save latest client to disk as ledger-state.json appActions.onLedgerCallback(result, random.randomInt({ min: miliseconds.minute, max: 10 * miliseconds.minute })) appActions.onBitcoinToBatTransitioned() - notifications.showBraveWalletUpdated() + ledgerNotifications.showBraveWalletUpdated() client.publisherTimestamp((err, result) => { if (err) { console.error('Error while retrieving publisher timestamp', err.toString()) @@ -2494,12 +2184,12 @@ const getMethods = () => { onNetworkConnected, migration, onInitRead, - notifications, deleteSynopsis, transitionWalletToBat, getNewClient, savePublisherData, - pruneSynopsis + pruneSynopsis, + checkBtcBatMigrated } let privateMethods = {} @@ -2508,7 +2198,6 @@ const getMethods = () => { privateMethods = { enable, addVisit, - checkBtcBatMigrated, clearVisitsByPublisher: function () { visitsByPublisher = {} }, diff --git a/app/browser/api/ledgerNotifications.js b/app/browser/api/ledgerNotifications.js new file mode 100644 index 00000000000..66f89ed6899 --- /dev/null +++ b/app/browser/api/ledgerNotifications.js @@ -0,0 +1,348 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const electron = require('electron') +const ipc = electron.ipcMain + +// Constants +const appConfig = require('../../../js/constants/appConfig') +const messages = require('../../../js/constants/messages') +const settings = require('../../../js/constants/settings') + +// State +const ledgerState = require('../../common/state/ledgerState') +const migrationState = require('../../common/state/migrationState') + +// Actions +const appActions = require('../../../js/actions/appActions') + +// Utils +const locale = require('../../locale') +const getSetting = require('../../../js/settings').getSetting + +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 text = { + hello: locale.translation('updateHello'), + paymentDone: undefined, + addFunds: locale.translation('addFundsNotification'), + tryPayments: locale.translation('notificationTryPayments'), + reconciliation: locale.translation('reconciliationNotification'), + walletConvertedToBat: locale.translation('walletConvertedToBat') +} + +const pollingInterval = 15 * miliseconds.minute // 15 * minutes +let intervalTimeout +const displayOptions = { + style: 'greetingStyle', + persist: false +} +const nextAddFundsTime = 3 * miliseconds.day + +const sufficientBalanceToReconcile = (state) => { + const balance = Number(ledgerState.getInfoProp(state, 'balance') || 0) + const unconfirmed = Number(ledgerState.getInfoProp(state, 'unconfirmed') || 0) + const bat = ledgerState.getInfoProp(state, 'bat') + return bat && (balance + unconfirmed > 0.9 * Number(bat)) +} +const hasFunds = (state) => { + const balance = getSetting(settings.PAYMENTS_ENABLED) + ? Number(ledgerState.getInfoProp(state, 'balance') || 0) + : 0 + return balance > 0 +} +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 init = (state) => { + // Check if relevant browser notifications should be shown every 15 minutes + if (intervalTimeout) { + clearInterval(intervalTimeout) + } + intervalTimeout = setInterval((state) => { + module.exports.onInterval(state) + }, pollingInterval, state) +} + +const onLaunch = (state) => { + const enabled = getSetting(settings.PAYMENTS_ENABLED) + if (!enabled) { + return state + } + + const ledger = require('./ledger') + state = ledger.checkBtcBatMigrated(state, enabled) + + if (hasFunds(state)) { + // Don't bother processing the rest, which are only + if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + return state + } + + // Show one-time BAT conversion message: + // - if payments are enabled + // - user has a positive balance + // - this is an existing profile (new profiles will have firstRunTimestamp matching batMercuryTimestamp) + // - wallet has been transitioned + // - notification has not already been shown yet + // (see https://github.com/brave/browser-laptop/issues/11021) + const isNewInstall = migrationState.isNewInstall(state) + const hasUpgradedWallet = migrationState.hasUpgradedWallet(state) + const hasBeenNotified = migrationState.hasBeenNotified(state) + if (!isNewInstall && hasUpgradedWallet && !hasBeenNotified) { + showBraveWalletUpdated() + } + } + + return state +} + +const onInterval = (state) => { + if (getSetting(settings.PAYMENTS_ENABLED)) { + if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + showEnabledNotifications(state) + } + } else { + showDisabledNotifications(state) + } +} + +const onResponse = (message, buttonIndex, activeWindow) => { + switch (message) { + case text.addFunds: + // 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 && activeWindow) { + // Add funds: Open payments panel + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: activeWindow.id + }) + } + break + + case text.reconciliation: +// buttonIndex === 1 is Dismiss + if (buttonIndex === 0) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } else if (buttonIndex === 2 && activeWindow) { + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: activeWindow.id + }) + } + break + + case text.paymentDone: + if (buttonIndex === 0) { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATIONS, false) + } + break + + case text.tryPayments: + if (buttonIndex === 1 && activeWindow) { + appActions.createTabRequested({ + url: 'about:preferences#payments', + windowId: activeWindow.id + }) + } + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_TRY_PAYMENTS_DISMISSED, true) + break + + case text.walletConvertedToBat: + if (buttonIndex === 0) { + // Open backup modal + appActions.createTabRequested({ + url: 'about:preferences#payments?ledgerBackupOverlayVisible', + windowId: activeWindow.id + }) + } + break + + default: + return + } + + appActions.hideNotification(message) +} + +/** + * 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') + showReviewPublishers(reconcileStamp + ((reconcileFrequency - 2) * miliseconds.day)) + } + } else if (shouldShowNotificationAddFunds()) { + showAddFunds() + } + } else if (reconcileStamp - new Date().getTime() < 2 * miliseconds.day) { + if (sufficientBalanceToReconcile(state) && (shouldShowNotificationReviewPublishers())) { + showReviewPublishers(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({ + from: 'ledger', + greeting: locale.translation('updateHello'), + message: text.tryPayments, + buttons: [ + {text: locale.translation('noThanks')}, + {text: locale.translation('notificationTryPaymentsYes'), className: 'primaryButton'} + ], + options: { + style: 'greetingStyle', + persist: false + } + }) + } +} + +const showReviewPublishers = (nextTime) => { + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_RECONCILE_SOON_TIMESTAMP, nextTime) + + appActions.showNotification({ + from: 'ledger', + greeting: text.hello, + message: text.reconciliation, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('dismiss')}, + {text: locale.translation('reviewSites'), className: 'primaryButton'} + ], + options: displayOptions + }) +} + +const showAddFunds = () => { + const nextTime = new Date().getTime() + nextAddFundsTime + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) + + appActions.showNotification({ + from: 'ledger', + greeting: text.hello, + message: text.addFunds, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('updateLater')}, + {text: locale.translation('addFunds'), className: 'primaryButton'} + ], + options: displayOptions + }) +} + +// Called from observeTransactions() when we see a new payment (transaction). +const showPaymentDone = (transactionContributionFiat) => { + text.paymentDone = 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(text.addFunds) + appActions.showNotification({ + from: 'ledger', + greeting: locale.translation('updateHello'), + message: text.paymentDone, + buttons: [ + {text: locale.translation('turnOffNotifications')}, + {text: locale.translation('Ok'), className: 'primaryButton'} + ], + options: displayOptions + }) +} + +const showBraveWalletUpdated = () => { + appActions.onBitcoinToBatNotified() + + appActions.showNotification({ + from: 'ledger', + greeting: text.hello, + message: text.walletConvertedToBat, + // Learn More. + buttons: [ + {text: locale.translation('walletConvertedBackup')}, + {text: locale.translation('walletConvertedDismiss')} + ], + options: { + style: 'greetingStyle', + persist: false, + advancedLink: 'https://brave.com/faq-payments/#brave-payments', + advancedText: locale.translation('walletConvertedLearnMore') + } + }) +} + +if (ipc) { + ipc.on(messages.NOTIFICATION_RESPONSE, (e, message, buttonIndex) => { + onResponse( + message, + buttonIndex, + electron.BrowserWindow.getActiveWindow() + ) + }) +} + +const getMethods = () => { + const publicMethods = { + showPaymentDone, + init, + onLaunch, + showBraveWalletUpdated, + onInterval + } + + let privateMethods = {} + + if (process.env.NODE_ENV === 'test') { + privateMethods = { + setTimeOut: (data) => { + intervalTimeout = data + }, + getTimeOut: () => { + return intervalTimeout + }, + getPollingInterval: () => { + return pollingInterval + } + } + } + + return Object.assign({}, publicMethods, privateMethods) +} + +module.exports = getMethods() diff --git a/test/unit/app/browser/api/ledgerNotificationsTest.js b/test/unit/app/browser/api/ledgerNotificationsTest.js new file mode 100644 index 00000000000..57b26247a6c --- /dev/null +++ b/test/unit/app/browser/api/ledgerNotificationsTest.js @@ -0,0 +1,306 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* global describe, it, after, before, beforeEach, afterEach */ +const Immutable = require('immutable') +const assert = require('assert') +const sinon = require('sinon') +const mockery = require('mockery') +const settings = require('../../../../../js/constants/settings') + +describe('ledgerNotifications unit test', function () { + let fakeClock + let ledgerApi + let ledgerNotificationsApi + + let paymentsEnabled + let paymentsNotifications + let paymentsMinVisitTime = 5000 + + const defaultAppState = Immutable.fromJS({ + ledger: {}, + migrations: {} + }) + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + const fakeElectron = require('../../../lib/fakeElectron') + const fakeAdBlock = require('../../../lib/fakeAdBlock') + const fakeLevel = require('../../../lib/fakeLevel') + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('ad-block', fakeAdBlock) + mockery.registerMock('level', fakeLevel) + mockery.registerMock('../../../js/settings', { + getSetting: (settingKey, settingsCollection, value) => { + switch (settingKey) { + case settings.PAYMENTS_ENABLED: + return paymentsEnabled + case settings.PAYMENTS_NOTIFICATIONS: + return paymentsNotifications + case settings.PAYMENTS_MINIMUM_VISIT_TIME: + return paymentsMinVisitTime + } + return false + } + }) + + fakeClock = sinon.useFakeTimers() + ledgerApi = require('../../../../../app/browser/api/ledger') + ledgerNotificationsApi = require('../../../../../app/browser/api/ledgerNotifications') + }) + + after(function () { + fakeClock.restore() + mockery.deregisterAll() + mockery.disable() + }) + + describe('init', function () { + let onIntervalSpy + beforeEach(function () { + onIntervalSpy = sinon.spy(ledgerNotificationsApi, 'onInterval') + }) + afterEach(function () { + onIntervalSpy.restore() + }) + it('does not immediately call notifications.onInterval', function () { + ledgerNotificationsApi.init(defaultAppState) + assert(onIntervalSpy.notCalled) + }) + it('calls notifications.onInterval after interval', function () { + fakeClock.tick(0) + ledgerNotificationsApi.init(defaultAppState) + fakeClock.tick(ledgerNotificationsApi.getPollingInterval()) + assert(onIntervalSpy.calledOnce) + }) + it('assigns a value to timeout', function () { + ledgerNotificationsApi.setTimeOut(0) + ledgerNotificationsApi.init(defaultAppState) + assert(ledgerNotificationsApi.getTimeOut(0)) + }) + }) + + describe('onLaunch', function () { + let showBraveWalletUpdatedSpy + let transitionWalletToBatSpy + beforeEach(function () { + showBraveWalletUpdatedSpy = sinon.spy(ledgerNotificationsApi, 'showBraveWalletUpdated') + transitionWalletToBatSpy = sinon.spy(ledgerApi, 'transitionWalletToBat') + }) + afterEach(function () { + showBraveWalletUpdatedSpy.restore() + transitionWalletToBatSpy.restore() + }) + + describe('with BAT Mercury', function () { + let ledgerStateWithBalance + + before(function () { + ledgerStateWithBalance = defaultAppState.merge(Immutable.fromJS({ + ledger: { + info: { + balance: 200 + } + }, + firstRunTimestamp: 12345, + migrations: { + batMercuryTimestamp: 12345, + btc2BatTimestamp: 12345, + btc2BatNotifiedTimestamp: 12345 + } + })) + }) + + describe('with wallet update message', function () { + describe('when payment notifications are disabled', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = false + }) + it('does not notify the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('when payments are disabled', function () { + before(function () { + paymentsEnabled = false + paymentsNotifications = true + }) + it('does not notify the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('user does not have funds', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['ledger', 'info', 'balance'], 0) + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('user did not have a session before BAT Mercury', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + ledgerNotificationsApi.onLaunch(ledgerStateWithBalance) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('user has not had the wallet transitioned from BTC to BAT', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 32145) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('user has already seen the notification', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 54321) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('when payment notifications are enabled, payments are enabled, user has funds, user had wallet before BAT Mercury, wallet has been transitioned, and user not been shown message yet', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('notifies the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.calledOnce) + }) + }) + }) + + describe('with the wallet transition from bitcoin to BAT', function () { + describe('when payment notifications are disabled', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = false + }) + it('calls ledger.transitionWalletToBat', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(transitionWalletToBatSpy.calledOnce) + }) + }) + + describe('when payments are disabled', function () { + before(function () { + paymentsEnabled = false + paymentsNotifications = true + }) + it('does not call ledger.transitionWalletToBat', function () { + ledgerNotificationsApi.onLaunch(ledgerStateWithBalance) + assert(transitionWalletToBatSpy.notCalled) + }) + }) + + describe('user does not have funds', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('calls ledger.transitionWalletToBat', function () { + const ledgerStateWithoutBalance = ledgerStateWithBalance + .setIn(['ledger', 'info', 'balance'], 0) + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(ledgerStateWithoutBalance) + assert(transitionWalletToBatSpy.calledOnce) + }) + }) + + describe('user did not have a session before BAT Mercury', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not call ledger.transitionWalletToBat', function () { + ledgerNotificationsApi.onLaunch(ledgerStateWithBalance) + assert(transitionWalletToBatSpy.notCalled) + }) + }) + + describe('user has already upgraded', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not call ledger.transitionWalletToBat', function () { + const ledgerStateSeenNotification = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 54321) + ledgerNotificationsApi.onLaunch(ledgerStateSeenNotification) + assert(transitionWalletToBatSpy.notCalled) + }) + }) + + describe('when payments are enabled and user had wallet before BAT Mercury', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('calls ledger.transitionWalletToBat', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'batMercuryTimestamp'], 32145) + .setIn(['migrations', 'btc2BatTimestamp'], 32145) + ledgerNotificationsApi.onLaunch(targetSession) + assert(transitionWalletToBatSpy.calledOnce) + }) + }) + }) + }) + }) +}) diff --git a/test/unit/app/browser/api/ledgerTest.js b/test/unit/app/browser/api/ledgerTest.js index 5c55f9602e4..1cba09bd6a6 100644 --- a/test/unit/app/browser/api/ledgerTest.js +++ b/test/unit/app/browser/api/ledgerTest.js @@ -12,21 +12,15 @@ const appActions = require('../../../../../js/actions/appActions') const migrationState = require('../../../../../app/common/state/migrationState') const batPublisher = require('bat-publisher') -const defaultAppState = Immutable.fromJS({ - ledger: {}, - migrations: {} -}) - describe('ledger api unit tests', function () { let ledgerApi + let ledgerNotificationsApi let isBusy = false let ledgerClient // settings - let paymentsEnabled - let paymentsNotifications - let paymentsMinVisitTime = 5000 let contributionAmount = 25 + let paymentsMinVisitTime = 5000 // spies let ledgerTransitionSpy @@ -36,34 +30,27 @@ describe('ledger api unit tests', function () { let onBitcoinToBatBeginTransitionSpy let onChangeSettingSpy + const defaultAppState = Immutable.fromJS({ + ledger: {}, + migrations: {} + }) + before(function () { mockery.enable({ warnOnReplace: false, warnOnUnregistered: false, useCleanCache: true }) - const fakeLevel = (pathName) => { - return { - batch: function (entries, cb) { - if (typeof cb === 'function') cb() - }, - get: function (key, cb) { - if (typeof cb === 'function') cb(null, '{"' + key + '": "value-goes-here"}') - } - } - } + const fakeElectron = require('../../../lib/fakeElectron') const fakeAdBlock = require('../../../lib/fakeAdBlock') + const fakeLevel = require('../../../lib/fakeLevel') mockery.registerMock('electron', fakeElectron) mockery.registerMock('level', fakeLevel) mockery.registerMock('ad-block', fakeAdBlock) mockery.registerMock('../../../js/settings', { getSetting: (settingKey, settingsCollection, value) => { switch (settingKey) { - case settings.PAYMENTS_ENABLED: - return paymentsEnabled - case settings.PAYMENTS_NOTIFICATIONS: - return paymentsNotifications case settings.PAYMENTS_CONTRIBUTION_AMOUNT: return contributionAmount case settings.PAYMENTS_MINIMUM_VISIT_TIME: @@ -150,6 +137,8 @@ describe('ledger api unit tests', function () { } mockery.registerMock('bat-publisher', lp) + ledgerNotificationsApi = require('../../../../../app/browser/api/ledgerNotifications') + // once everything is stubbed, load the ledger ledgerApi = require('../../../../../app/browser/api/ledger') }) @@ -165,7 +154,7 @@ describe('ledger api unit tests', function () { describe('initialize', function () { let notificationsInitStub beforeEach(function () { - notificationsInitStub = sinon.stub(ledgerApi.notifications, 'init') + notificationsInitStub = sinon.stub(ledgerNotificationsApi, 'init') }) afterEach(function () { notificationsInitStub.restore() @@ -193,7 +182,7 @@ describe('ledger api unit tests', function () { contributionAmount = 25 }) before(function () { - onLaunchSpy = sinon.spy(ledgerApi.notifications, 'onLaunch') + onLaunchSpy = sinon.spy(ledgerNotificationsApi, 'onLaunch') setPaymentInfoSpy = sinon.spy(ledgerApi, 'setPaymentInfo') }) after(function () { @@ -497,258 +486,6 @@ describe('ledger api unit tests', function () { }) }) - describe('notifications', function () { - let fakeClock - before(function () { - fakeClock = sinon.useFakeTimers() - }) - after(function () { - fakeClock.restore() - }) - describe('init', function () { - let onIntervalSpy - beforeEach(function () { - onIntervalSpy = sinon.spy(ledgerApi.notifications, 'onInterval') - }) - afterEach(function () { - onIntervalSpy.restore() - }) - it('does not immediately call notifications.onInterval', function () { - ledgerApi.notifications.init(defaultAppState) - assert(onIntervalSpy.notCalled) - }) - it('calls notifications.onInterval after interval', function () { - fakeClock.tick(0) - ledgerApi.notifications.init(defaultAppState) - fakeClock.tick(ledgerApi.notifications.pollingInterval) - assert(onIntervalSpy.calledOnce) - }) - it('assigns a value to notifications.timeout', function () { - ledgerApi.notifications.timeout = 0 - ledgerApi.notifications.init(defaultAppState) - assert(ledgerApi.notifications.timeout) - }) - }) - - describe('onLaunch', function () { - let showBraveWalletUpdatedSpy - let transitionWalletToBatSpy - beforeEach(function () { - showBraveWalletUpdatedSpy = sinon.spy(ledgerApi.notifications, 'showBraveWalletUpdated') - transitionWalletToBatSpy = sinon.spy(ledgerApi, 'transitionWalletToBat') - }) - afterEach(function () { - showBraveWalletUpdatedSpy.restore() - transitionWalletToBatSpy.restore() - }) - - describe('with BAT Mercury', function () { - let ledgerStateWithBalance - - before(function () { - ledgerStateWithBalance = defaultAppState.merge(Immutable.fromJS({ - ledger: { - info: { - balance: 200 - } - }, - firstRunTimestamp: 12345, - migrations: { - batMercuryTimestamp: 12345, - btc2BatTimestamp: 12345, - btc2BatNotifiedTimestamp: 12345 - } - })) - }) - - describe('with wallet update message', function () { - describe('when payment notifications are disabled', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = false - }) - it('does not notify the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('when payments are disabled', function () { - before(function () { - paymentsEnabled = false - paymentsNotifications = true - }) - it('does not notify the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('user does not have funds', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not notify the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['ledger', 'info', 'balance'], 0) - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('user did not have a session before BAT Mercury', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not notify the user', function () { - ledgerApi.notifications.onLaunch(ledgerStateWithBalance) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('user has not had the wallet transitioned from BTC to BAT', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not notify the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 32145) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('user has already seen the notification', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not notify the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 54321) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.notCalled) - }) - }) - - describe('when payment notifications are enabled, payments are enabled, user has funds, user had wallet before BAT Mercury, wallet has been transitioned, and user not been shown message yet', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('notifies the user', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - .setIn(['migrations', 'btc2BatNotifiedTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(showBraveWalletUpdatedSpy.calledOnce) - }) - }) - }) - - describe('with the wallet transition from bitcoin to BAT', function () { - describe('when payment notifications are disabled', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = false - }) - it('calls ledger.transitionWalletToBat', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(transitionWalletToBatSpy.calledOnce) - }) - }) - - describe('when payments are disabled', function () { - before(function () { - paymentsEnabled = false - paymentsNotifications = true - }) - it('does not call ledger.transitionWalletToBat', function () { - ledgerApi.notifications.onLaunch(ledgerStateWithBalance) - assert(transitionWalletToBatSpy.notCalled) - }) - }) - - describe('user does not have funds', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('calls ledger.transitionWalletToBat', function () { - const ledgerStateWithoutBalance = ledgerStateWithBalance - .setIn(['ledger', 'info', 'balance'], 0) - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 32145) - ledgerApi.notifications.onLaunch(ledgerStateWithoutBalance) - assert(transitionWalletToBatSpy.calledOnce) - }) - }) - - describe('user did not have a session before BAT Mercury', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not call ledger.transitionWalletToBat', function () { - ledgerApi.notifications.onLaunch(ledgerStateWithBalance) - assert(transitionWalletToBatSpy.notCalled) - }) - }) - - describe('user has already upgraded', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('does not call ledger.transitionWalletToBat', function () { - const ledgerStateSeenNotification = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 54321) - ledgerApi.notifications.onLaunch(ledgerStateSeenNotification) - assert(transitionWalletToBatSpy.notCalled) - }) - }) - - describe('when payments are enabled and user had wallet before BAT Mercury', function () { - before(function () { - paymentsEnabled = true - paymentsNotifications = true - }) - it('calls ledger.transitionWalletToBat', function () { - const targetSession = ledgerStateWithBalance - .setIn(['migrations', 'batMercuryTimestamp'], 32145) - .setIn(['migrations', 'btc2BatTimestamp'], 32145) - ledgerApi.notifications.onLaunch(targetSession) - assert(transitionWalletToBatSpy.calledOnce) - }) - }) - }) - }) - }) - }) - describe('synopsisNormalizer', function () { describe('prune synopsis', function () { let pruneSynopsisSpy @@ -855,8 +592,6 @@ describe('ledger api unit tests', function () { .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io', 'options', 'verifiedTimestamp'], 10) const expectedState = newState - .setIn(['ledger', 'about', 'synopsis'], []) - .setIn(['ledger', 'about', 'synopsisOptions'], {}) .setIn(['ledger', 'synopsis', 'publishers', 'clifton.io', 'options', 'verified'], true) const result = ledgerApi.checkVerifiedStatus(newState, 'clifton.io') assert.deepEqual(result.toJS(), expectedState.toJS()) diff --git a/test/unit/lib/fakeLevel.js b/test/unit/lib/fakeLevel.js new file mode 100644 index 00000000000..36d005a9f34 --- /dev/null +++ b/test/unit/lib/fakeLevel.js @@ -0,0 +1,12 @@ +const fakeLevel = (pathName) => { + return { + batch: function (entries, cb) { + if (typeof cb === 'function') cb() + }, + get: function (key, cb) { + if (typeof cb === 'function') cb(null, '{"' + key + '": "value-goes-here"}') + } + } +} + +module.exports = fakeLevel