diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 651b1aed501..1192b545e79 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -49,7 +49,6 @@ let currentTimestamp = new Date().getTime() let visitsByPublisher = {} let bootP let quitP -let notificationPaymentDoneMessage const _internal = { verboseP: process.env.LEDGER_VERBOSE || true, debugP: process.env.LEDGER_DEBUG || true, @@ -68,7 +67,6 @@ let ledgerBalance // Timers let balanceTimeoutId = false -let notificationTimeout let runTimeoutId // Database @@ -104,13 +102,263 @@ const fileTypes = { } const minimumVisitTimeDefault = 8 * 1000 const nextAddFoundsTime = 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 btc = ledgerState.getInfoProp(state, 'btc') + return btc && (balance + unconfirmed > 0.9 * Number(btc)) +} +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) + + // Show relevant browser notifications on launch + notifications.onLaunch(state) + }, + onLaunch: (state) => { + if (!getSetting(settings.PAYMENTS_NOTIFICATIONS)) { + return + } + // 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 btcToBatTimestamp) + // - notification has not already been shown yet + // (see https://github.com/brave/browser-laptop/issues/11021) + if (hasFunds(state)) { + const isNewInstall = state.get('firstRunTimestamp') === state.getIn(['migrations', 'btcToBatTimestamp']) + const hasBeenNotified = state.getIn(['migrations', 'btcToBatTimestamp']) !== state.getIn(['migrations', 'btcToBatNotifiedTimestamp']) + if (!isNewInstall && !hasBeenNotified) { + notifications.showBraveWalletUpdated() + } + } + }, + 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({ + 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({ + 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() + nextAddFoundsTime + appActions.changeSetting(settings.PAYMENTS_NOTIFICATION_ADD_FUNDS_TIMESTAMP, nextTime) + + appActions.showNotification({ + 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({ + 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({ + 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) => { @@ -144,47 +392,11 @@ if (ipc) { }) 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) - } + notifications.onResponse( + message, + buttonIndex, + electron.BrowserWindow.getActiveWindow() + ) }) } @@ -1143,121 +1355,6 @@ const pathName = (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() + nextAddFoundsTime - 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 @@ -1438,27 +1535,6 @@ const updateLedgerInfo = (state) => { 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) { @@ -1468,7 +1544,7 @@ const observeTransactions = (state, transactions) => { if (getSetting(settings.PAYMENTS_NOTIFICATIONS)) { if (transactions.length > 0) { const newestTransaction = transactions[transactions.length - 1] - showNotificationPaymentDone(newestTransaction.contribution.fiat) + notifications.showPaymentDone(newestTransaction.contribution.fiat) } } } @@ -1809,13 +1885,7 @@ const initialize = (state, paymentsEnabled) => { 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) + notifications.init(state) if (!paymentsEnabled) { client = null @@ -2169,5 +2239,6 @@ module.exports = { run, onNetworkConnected, migration, - onInitRead + onInitRead, + notifications } diff --git a/app/browser/reducers/ledgerReducer.js b/app/browser/reducers/ledgerReducer.js index 934c3bda992..5191a0c1544 100644 --- a/app/browser/reducers/ledgerReducer.js +++ b/app/browser/reducers/ledgerReducer.js @@ -311,6 +311,11 @@ const ledgerReducer = (state, action, immutableAction) => { state = ledgerApi.onInitRead(state, action.parsedData) break } + case appConstants.APP_ON_BTC_TO_BAT_NOTIFIED: + { + state = state.setIn(['migrations', 'btcToBatNotifiedTimestamp'], new Date().getTime()) + break + } } return state } diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index ea59135aaa0..d885936509c 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -159,6 +159,7 @@ notificationTryPaymentsYes=Sure, I'll try notificationUpdatePassword=Would you like Brave to update your password on {{origin}}? notificationUpdatePasswordWithUserName=Would you like Brave to update the password for {{username}} on {{origin}}? notNow=Not Now +notVisiblePublisher.title=Publisher is not yet added to the ledger, because it doesn't meet criteria yet. ok=OK openTypeInNewPrivateTab=Open {{type}} in a new private tab openTypeInNewTab=Open {{type}} in a new tab @@ -254,11 +255,14 @@ 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 viewPageSource=View Page Source +walletConvertedBackup=Back up your new wallet +walletConvertedDismiss=Later +walletConvertedLearnMore=Learn More +walletConvertedToBat=Your Brave Payments BTC wallet has been converted to a BAT wallet. widevinePanelTitle=Brave needs to install Google Widevine to proceed windowCaptionButtonClose=Close windowCaptionButtonMaximize=Maximize diff --git a/app/locale.js b/app/locale.js index 1443a9f2b45..18c658f6c7e 100644 --- a/app/locale.js +++ b/app/locale.js @@ -216,6 +216,7 @@ var rendererIdentifiers = function () { 'displayQRCode', 'updateLater', 'updateHello', + // notifications 'notificationPasswordWithUserName', 'notificationUpdatePasswordWithUserName', 'notificationUpdatePassword', @@ -231,6 +232,11 @@ var rendererIdentifiers = function () { 'no', 'noThanks', 'neverForThisSite', + 'walletConvertedBackup', + 'walletConvertedDismiss', + 'walletConvertedLearnMore', + 'walletConvertedToBat', + // other 'passwordsManager', 'extensionsManager', 'downloadItemPause', diff --git a/app/sessionStore.js b/app/sessionStore.js index 67dd3df224f..1343fa13007 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -979,6 +979,10 @@ module.exports.defaultAppState = () => { options: {}, publishers: {} } + }, + migrations: { + btcToBatTimestamp: new Date().getTime(), + btcToBatNotifiedTimestamp: new Date().getTime() } } } diff --git a/docs/state.md b/docs/state.md index b1625ac3884..d17b1a030ed 100644 --- a/docs/state.md +++ b/docs/state.md @@ -281,6 +281,10 @@ AppStore } } }, + migrations: { + btcToBatTimestamp: integer, // when btcToBat code was first ran (where session is upgraded) + btcToBatNotifiedTimestamp: integer, // when user was shown wallet upgraded notification + }, menu: { template: object // used on Windows and by our tests: template object with Menubar control }, diff --git a/js/about/preferences.js b/js/about/preferences.js index 843d9b71b7d..1ea0cb3cc54 100644 --- a/js/about/preferences.js +++ b/js/about/preferences.js @@ -707,6 +707,13 @@ class AboutPreferences extends React.Component { secondRecoveryKey: '' } + // Similar to tabFromCurrentHash, this allows to set + // state via a query string inside the hash. + const params = this.hashParams + if (params && typeof this.state[params] === 'boolean') { + this.state[params] = true + } + ipc.on(messages.SETTINGS_UPDATED, (e, settings) => { this.setState({ settings: Immutable.fromJS(settings || {}) }) }) @@ -759,13 +766,42 @@ class AboutPreferences extends React.Component { } updateTabFromAnchor () { - this.setState({ + const newState = { preferenceTab: this.tabFromCurrentHash - }) + } + // first attempt at solving https://github.com/brave/browser-laptop/issues/8966 + // only handles one param and sets it to true + const params = this.hashParams + if (params && typeof this.state[params] === 'boolean') { + newState[params] = true + } + this.setState(newState) } + /** + * Parses a query string like: + * about:preferences#payments?ledgerBackupOverlayVisible + * and returns the part: + * `payments` + */ get hash () { - return window.location.hash ? window.location.hash.slice(1) : '' + const hash = window.location.hash ? window.location.hash.slice(1) : '' + return hash.split('?', 2)[0] + } + + /** + * Parses a query string like: + * about:preferences#payments?ledgerBackupOverlayVisible + * and returns the part: + * `ledgerBackupOverlayVisible` + */ + get hashParams () { + const hash = window.location.hash ? window.location.hash.slice(1) : '' + const splitHash = hash.split('?', 2) + if (splitHash.length === 2) { + return splitHash[1] + } + return undefined } get tabFromCurrentHash () { diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 7f13cf68058..dd55f774036 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -1742,6 +1742,12 @@ const appActions = { windowId } }) + }, + + onBitcoinToBatNotified: function () { + dispatch({ + actionType: appConstants.APP_ON_BTC_TO_BAT_NOTIFIED + }) } } diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 93e26fb4255..c4e15ef19d6 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -170,7 +170,8 @@ const appConstants = { APP_ON_LEDGER_RUN: _, APP_ON_NETWORK_CONNECTED: _, APP_ON_RESET_RECOVERY_STATUS: _, - APP_ON_LEDGER_INIT_READ: _ + APP_ON_LEDGER_INIT_READ: _, + APP_ON_BTC_TO_BAT_NOTIFIED: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/test/unit/about/preferencesTest.js b/test/unit/about/preferencesTest.js index 76b5245a7ad..456fe3044cf 100644 --- a/test/unit/about/preferencesTest.js +++ b/test/unit/about/preferencesTest.js @@ -96,6 +96,18 @@ describe('Preferences component', function () { assert.equal(this.result.find('[data-test-id="generalSettings"]').length, 0) assert.equal(this.result.find('[data-test-id="searchSettings"]').length, 1) }) + + it('Allows state change from query string in hash', function () { + this.result = mount(Preferences) + assert.equal(this.result.find('[data-test-id="generalSettings"]').length, 1) + assert.equal(this.result.find('[data-test-id="paymentsContainer"]').length, 0) + assert.equal(this.result.node.state.ledgerBackupOverlayVisible, false) + window.location.hash = 'payments?ledgerBackupOverlayVisible' + this.result = mount(Preferences) + assert.equal(this.result.find('[data-test-id="generalSettings"]').length, 0) + assert.equal(this.result.find('[data-test-id="paymentsContainer"]').length, 1) + assert.equal(this.result.node.state.ledgerBackupOverlayVisible, true) + }) }) describe('General', function () { diff --git a/test/unit/app/browser/api/ledgerTest.js b/test/unit/app/browser/api/ledgerTest.js new file mode 100644 index 00000000000..b2f53b4245f --- /dev/null +++ b/test/unit/app/browser/api/ledgerTest.js @@ -0,0 +1,202 @@ +/* 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') + +const defaultAppState = Immutable.fromJS({ + ledger: { + } +}) + +describe('ledger api unit tests', function () { + let ledgerApi + let paymentsEnabled + let paymentsNotifications + + before(function () { + this.clock = sinon.useFakeTimers() + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + const fakeLevel = () => {} + const fakeElectron = require('../../../lib/fakeElectron') + const fakeAdBlock = require('../../../lib/fakeAdBlock') + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('level', fakeLevel) + mockery.registerMock('ad-block', fakeAdBlock) + mockery.registerMock('../../../js/settings', { + getSetting: (settingKey, settingsCollection, value) => { + if (settingKey === settings.PAYMENTS_ENABLED) { + return paymentsEnabled + } + if (settingKey === settings.PAYMENTS_NOTIFICATIONS) { + return paymentsNotifications + } + return false + } + }) + ledgerApi = require('../../../../../app/browser/api/ledger') + }) + after(function () { + mockery.disable() + this.clock.restore() + }) + + describe('initialize', function () { + let notificationsInitSpy + beforeEach(function () { + notificationsInitSpy = sinon.spy(ledgerApi.notifications, 'init') + }) + afterEach(function () { + notificationsInitSpy.restore() + }) + it('calls notifications.init', function () { + ledgerApi.initialize(defaultAppState, true) + assert(notificationsInitSpy.calledOnce) + }) + }) + + describe('notifications', function () { + describe('init', function () { + let onLaunchSpy + let onIntervalSpy + beforeEach(function () { + onLaunchSpy = sinon.spy(ledgerApi.notifications, 'onLaunch') + onIntervalSpy = sinon.spy(ledgerApi.notifications, 'onInterval') + }) + afterEach(function () { + onLaunchSpy.restore() + onIntervalSpy.restore() + }) + it('does not immediately call notifications.onInterval', function () { + ledgerApi.notifications.init(defaultAppState) + assert(onIntervalSpy.notCalled) + }) + it('calls notifications.onInterval after interval', function () { + this.clock.tick(0) + ledgerApi.notifications.init(defaultAppState) + this.clock.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) + }) + it('calls notifications.onLaunch', function () { + ledgerApi.notifications.init(defaultAppState) + assert(onLaunchSpy.withArgs(defaultAppState).calledOnce) + }) + }) + + describe('onLaunch', function () { + let showBraveWalletUpdatedSpy + beforeEach(function () { + showBraveWalletUpdatedSpy = sinon.spy(ledgerApi.notifications, 'showBraveWalletUpdated') + }) + afterEach(function () { + showBraveWalletUpdatedSpy.restore() + }) + + describe('with the BAT Mercury wallet update message', function () { + let ledgerStateWithBalance + + before(function () { + ledgerStateWithBalance = defaultAppState.merge(Immutable.fromJS({ + ledger: { + info: { + balance: 200 + } + }, + firstRunTimestamp: 12345, + migrations: { + btcToBatTimestamp: 12345, + btcToBatNotifiedTimestamp: 12345 + } + })) + }) + + describe('when payment notifications are disabled', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = false + }) + it('does not notify the user', function () { + ledgerApi.notifications.onLaunch(ledgerStateWithBalance) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('when payments are disabled', function () { + before(function () { + paymentsEnabled = false + paymentsNotifications = true + }) + it('does not notify the user', function () { + ledgerApi.notifications.onLaunch(ledgerStateWithBalance) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('user does not have funds', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + const ledgerStateWithoutBalance = ledgerStateWithBalance.setIn(['ledger', 'info', 'balance'], 0) + ledgerApi.notifications.onLaunch(ledgerStateWithoutBalance) + 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 already seen the notification', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('does not notify the user', function () { + const ledgerStateSeenNotification = ledgerStateWithBalance + .setIn(['migrations', 'btcToBatTimestamp'], 32145) + .setIn(['migrations', 'btcToBatNotifiedTimestamp'], 54321) + ledgerApi.notifications.onLaunch(ledgerStateSeenNotification) + assert(showBraveWalletUpdatedSpy.notCalled) + }) + }) + + describe('when payment notifications are enabled, payments are enabled, user has funds, user had wallet before BAT Mercury, and user not been shown message yet', function () { + before(function () { + paymentsEnabled = true + paymentsNotifications = true + }) + it('notifies the user', function () { + const targetSession = ledgerStateWithBalance + .setIn(['migrations', 'btcToBatTimestamp'], 32145) + .setIn(['migrations', 'btcToBatNotifiedTimestamp'], 32145) + ledgerApi.notifications.onLaunch(targetSession) + assert(showBraveWalletUpdatedSpy.calledOnce) + }) + }) + }) + }) + }) +}) diff --git a/test/unit/app/browser/reducers/ledgerReducerTest.js b/test/unit/app/browser/reducers/ledgerReducerTest.js index faae36e58d4..0df428d5012 100644 --- a/test/unit/app/browser/reducers/ledgerReducerTest.js +++ b/test/unit/app/browser/reducers/ledgerReducerTest.js @@ -521,4 +521,16 @@ describe('ledgerReducer unit tests', function () { assert.notDeepEqual(returnedState, appState) }) }) + + describe('APP_ON_BTC_TO_BAT_NOTIFIED', function () { + before(function () { + returnedState = ledgerReducer(appState, Immutable.fromJS({ + actionType: appConstants.APP_ON_BTC_TO_BAT_NOTIFIED + })) + }) + it('sets the notification timestamp', function () { + assert.notDeepEqual(returnedState, appState) + assert(returnedState.getIn(['migrations', 'btcToBatNotifiedTimestamp'])) + }) + }) })