Skip to content

Commit

Permalink
Ledger notifications refactor and add new BTC=>BAT notification
Browse files Browse the repository at this point in the history
Fixes brave#11021

Presents this one-time "Converted BTC to BAT" alert:
 - 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

Includes a "learn more" link pointing to:
https://brave.com/faq-payments/#brave-payments

Has a possible security risk- the deep link allows URL hash to modify state (limited to boolean state variables on about:preferences)

Auditors: @NejcZdovc, @diracdeltas

Test Plan:
`npm run unittest -- --grep="ledger api unit tests"`
`npm run unittest -- --grep="ledgerReducer unit tests"`
`npm run unittest -- --grep="Preferences component"`
  • Loading branch information
bsclifton authored and syuan100 committed Nov 9, 2017
1 parent 50ab8ea commit 7bac062
Show file tree
Hide file tree
Showing 12 changed files with 557 additions and 194 deletions.
449 changes: 260 additions & 189 deletions app/browser/api/ledger.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions app/browser/reducers/ledgerReducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 5 additions & 1 deletion app/extensions/brave/locales/en-US/app.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions app/locale.js
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ var rendererIdentifiers = function () {
'displayQRCode',
'updateLater',
'updateHello',
// notifications
'notificationPasswordWithUserName',
'notificationUpdatePasswordWithUserName',
'notificationUpdatePassword',
Expand All @@ -231,6 +232,11 @@ var rendererIdentifiers = function () {
'no',
'noThanks',
'neverForThisSite',
'walletConvertedBackup',
'walletConvertedDismiss',
'walletConvertedLearnMore',
'walletConvertedToBat',
// other
'passwordsManager',
'extensionsManager',
'downloadItemPause',
Expand Down
4 changes: 4 additions & 0 deletions app/sessionStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ module.exports.defaultAppState = () => {
options: {},
publishers: {}
}
},
migrations: {
btcToBatTimestamp: new Date().getTime(),
btcToBatNotifiedTimestamp: new Date().getTime()
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions docs/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
},
Expand Down
42 changes: 39 additions & 3 deletions js/about/preferences.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || {}) })
})
Expand Down Expand Up @@ -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 () {
Expand Down
6 changes: 6 additions & 0 deletions js/actions/appActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -1742,6 +1742,12 @@ const appActions = {
windowId
}
})
},

onBitcoinToBatNotified: function () {
dispatch({
actionType: appConstants.APP_ON_BTC_TO_BAT_NOTIFIED
})
}
}

Expand Down
3 changes: 2 additions & 1 deletion js/constants/appConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
12 changes: 12 additions & 0 deletions test/unit/about/preferencesTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
202 changes: 202 additions & 0 deletions test/unit/app/browser/api/ledgerTest.js
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
})
})
})
12 changes: 12 additions & 0 deletions test/unit/app/browser/reducers/ledgerReducerTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']))
})
})
})

0 comments on commit 7bac062

Please sign in to comment.