Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom notifications #133

Merged
merged 38 commits into from
May 9, 2019
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7965890
- Allow user to spawn custom notifications
liamaharon Apr 26, 2019
2e72580
- Improve userInitiatedNotify func name
liamaharon Apr 26, 2019
2d02ce5
Check assist is ready for userInitiatedNotify
liamaharon Apr 26, 2019
3cf8f87
Remove options param from userInitiatedNotify
liamaharon Apr 26, 2019
89327cf
Move notify method to assistInstance
liamaharon Apr 26, 2019
862596a
Add notity docs
liamaharon Apr 26, 2019
b60a15a
Merge branch 'develop' of github.com:liamaharon/assist into feature/c…
liamaharon Apr 26, 2019
073ddff
Improve notify docs
liamaharon Apr 26, 2019
66f2bd9
Improve comment
liamaharon Apr 26, 2019
137f395
Don't call setHeight when no notifications
liamaharon Apr 26, 2019
63f4108
Fix userInitiatedNotify validation
liamaharon Apr 26, 2019
505d039
Improve docs
liamaharon Apr 26, 2019
d9939d6
Merge branch 'develop' of github.com:blocknative/assist into feature/…
liamaharon Apr 29, 2019
a870a54
Merge branch 'develop' of github.com:blocknative/assist into feature/…
liamaharon Apr 30, 2019
6690204
Fix notify function signature in docs
liamaharon Apr 30, 2019
8d47b82
Improve notify docs example
liamaharon Apr 30, 2019
eff9c13
Improve import syntax
liamaharon Apr 30, 2019
95a7efd
Test for a potential difficult to detect bug
liamaharon Apr 30, 2019
83fc717
Merge branch 'develop' of github.com:blocknative/assist into feature/…
liamaharon May 1, 2019
af0a9b6
Fix notify customTimeout not working sometimes
liamaharon May 1, 2019
8827995
Mention HMTL can be embedded in customNotify msgs
liamaharon May 1, 2019
da119a8
Show timer in custom "pending" notification
liamaharon May 2, 2019
37d2974
Add ui-rendering tests for custom notify events
liamaharon May 2, 2019
9ee565d
Test that customTimeout works
liamaharon May 2, 2019
88138f0
Specify notify customTimeout in an options object
liamaharon May 2, 2019
d817573
- Fix customNotify bug
liamaharon May 2, 2019
dd3e5bb
Specify customTimeout type
liamaharon May 3, 2019
92639f6
Log custom events to the server
liamaharon May 6, 2019
c29dcfe
Improve test
liamaharon May 6, 2019
7e7c3f6
Update yarn.lock
liamaharon May 6, 2019
ad5d3b1
Improve tests
liamaharon May 6, 2019
0497849
Improve comment
liamaharon May 6, 2019
848a456
Validate customCode
liamaharon May 6, 2019
b148c2e
Limit customCode to 24 characters
liamaharon May 6, 2019
e327fa0
Improve user-initiated-notify tests
liamaharon May 6, 2019
8c9f73d
Increase test run speed
liamaharon May 6, 2019
1fb0a74
Improve customCode default
liamaharon May 6, 2019
b423ebb
change eventCode for custom notification
cmeisl May 9, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,48 @@ assistInstance.getState()
})
```

### `notify(type, message, options)`

Trigger a custom UI notification

#### Parameters

`type` - `String`: One of: ['success', 'pending', 'error'] (**Required**)

`message` - `String`: The message to display in the notification. HTML can be embedded in the string. (**Required**)

`options` - `Object`: Further customize the notification

```js
options = {
customTimeout: Number, // Specify how many ms the notification should exist. Set to -1 for no timeout.
customCode: String // An identifier for this notify call
}
```

options.customTimeout defaults: { success: 2000, pending: 5000, error: 5000 }

#### Returns

`Function`

- a function that when called will dismiss the notification

#### Examples

```javascript
// Display a success notification with an embedded link for 5000ms
assistInstance.notify('success', 'Operation was a success! Click <a href="https://example.com" target="_blank">here</a> to view more', { customTimeout: 5000 });

// Display a pending notification, load data from an imaginary backend
// and dismiss the pending notification only when the data is loaded
const dismiss = assistInstance.notify('pending', 'Loading data...', { customTimeout: -1 });
myEventEmitter.emit('fetch-data-from-backend')
myEventEmitter.on('data-from-backend-loaded', () => {
dismiss()
})
```

## Contribute

### Installing Dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2181,3 +2181,67 @@ exports[`dom-rendering event onboard-welcomeUser should trigger correct DOM rend
</div>
</div>"
`;

exports[`dom-rendering event userInitiatedNotify-error should trigger correct DOM render 1`] = `
"

<div class=\\"\\" id=\\"blocknative-notifications\\" style=\\"transform: translate(600px); right: 0px; bottom: 0px;\\"><div class=\\"bn-notifications-scroll\\" style=\\"overflow-y: initial; height: auto;\\"><ul class=\\"bn-notifications\\"><li class=\\"bn-notification bn-failed bn-error bn-undefined \\" style=\\"transform: translate(600px);\\">
<span class=\\"bn-status-icon \\">

</span>
<div class=\\"bn-notification-info\\">
<p>error custom msg</p>
<p class=\\"bn-notification-meta\\">
<a href=\\"#\\" class=\\"bn-timestamp\\">12:00 AM</a>
<span class=\\"bn-duration bn-duration-hidden\\"> -
<i class=\\"bn-clock\\"></i>
<span class=\\"bn-duration-time\\">NaN sec</span>
</span>
</p>
</div>
</li></ul></div><a class=\\"\\" id=\\"bn-transaction-branding\\" href=\\"https://www.blocknative.com/\\" target=\\"_blank\\" style=\\"float: right;\\"></a></div>"
`;

exports[`dom-rendering event userInitiatedNotify-pending should trigger correct DOM render 1`] = `
"

<div class=\\"\\" id=\\"blocknative-notifications\\" style=\\"transform: translate(600px); right: 0px; bottom: 0px;\\"><div class=\\"bn-notifications-scroll\\" style=\\"overflow-y: initial; height: auto;\\"><ul class=\\"bn-notifications\\"><li class=\\"bn-notification bn-progress bn-pending bn-cf7fc5a7-1498-419e-8bf9-654d06af5534 \\" style=\\"transform: translate(600px);\\">
<span class=\\"bn-status-icon \\">
<div class=\\"progress-tooltip \\">
<div class=\\"progress-tooltip-inner\\">
You will be notified when this transaction is completed.
</div>
</div>
</span>
<div class=\\"bn-notification-info\\">
<p>pending custom msg</p>
<p class=\\"bn-notification-meta\\">
<a href=\\"#\\" class=\\"bn-timestamp\\">12:00 AM</a>
<span class=\\"bn-duration\\"> -
<i class=\\"bn-clock\\"></i>
<span class=\\"bn-duration-time\\">606 min</span>
</span>
</p>
</div>
</li></ul></div><a class=\\"\\" id=\\"bn-transaction-branding\\" href=\\"https://www.blocknative.com/\\" target=\\"_blank\\" style=\\"float: right;\\"></a></div>"
`;

exports[`dom-rendering event userInitiatedNotify-success should trigger correct DOM render 1`] = `
"

<div class=\\"\\" id=\\"blocknative-notifications\\" style=\\"transform: translate(600px); right: 0px; bottom: 0px;\\"><div class=\\"bn-notifications-scroll\\" style=\\"overflow-y: initial; height: auto;\\"><ul class=\\"bn-notifications\\"><li class=\\"bn-notification bn-complete bn-success bn-undefined \\" style=\\"transform: translate(600px);\\">
<span class=\\"bn-status-icon \\">

</span>
<div class=\\"bn-notification-info\\">
<p>success custom msg</p>
<p class=\\"bn-notification-meta\\">
<a href=\\"#\\" class=\\"bn-timestamp\\">12:00 AM</a>
<span class=\\"bn-duration bn-duration-hidden\\"> -
<i class=\\"bn-clock\\"></i>
<span class=\\"bn-duration-time\\">NaN sec</span>
</span>
</p>
</div>
</li></ul></div><a class=\\"\\" id=\\"bn-transaction-branding\\" href=\\"https://www.blocknative.com/\\" target=\\"_blank\\" style=\\"float: right;\\"></a></div>"
`;
19 changes: 19 additions & 0 deletions src/__integration-tests__/ui-rendering/event-definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,25 @@ const mockTxFactory = ({ nonce, startTime } = {}) => {
* }
*/
export default {
success: {
categories: ['userInitiatedNotify'],
customStates: [
{ config: { messages: { success: () => 'success custom msg' } } }
]
},
pending: {
categories: ['userInitiatedNotify'],
params: { transaction: mockTxFactory({ startTime: true }) },
customStates: [
{ config: { messages: { pending: () => 'pending custom msg' } } }
]
},
error: {
categories: ['userInitiatedNotify'],
customStates: [
{ config: { messages: { error: () => 'error custom msg' } } }
]
},
browserFail: {
categories: ['onboard'],
clickHandlers: new Set(['onClose'])
Expand Down
1 change: 1 addition & 0 deletions src/__tests__/js/init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ test('Returns the dassist object', () => {
expect(assist).toHaveProperty('Contract')
expect(assist).toHaveProperty('Transaction')
expect(assist).toHaveProperty('getState')
expect(assist).toHaveProperty('notify')
})

test('Fails if we try to decorate without a web3 instance', () => {
Expand Down
121 changes: 121 additions & 0 deletions src/__tests__/logic/user-initiated-notify.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import da from '~/js'
import userInitiatedNotify, {
defaultTimeout
} from '~/js/logic/user-initiated-notify'
import { updateState, initialState, state } from '~/js/helpers/state'
import * as events from '~/js/helpers/events'

jest.useFakeTimers()
const ONE_MIN_IN_MS = 60000

describe('user-initiated-notify.js', () => {
describe('userInitiatedNotify', () => {
const message = 'some-msg'
const customEventCodes = ['success', 'pending', 'error']
test('throws if eventCode is invalid', () => {
expect(() => userInitiatedNotify('invalid code', message)).toThrow()
})
customEventCodes.forEach(eventCode => {
describe(`type ${eventCode}`, () => {
test('throws if customTimeout is not a number', () => {
expect(() =>
userInitiatedNotify(eventCode, message, { customTimeout: '123' })
).toThrow()
})
test('throws if customCode is not a string', () => {
expect(() =>
userInitiatedNotify(eventCode, message, { customCode: 123 })
).toThrow()
})
test('throws if customCode is greater than 24 characters', () => {
const customCode = '0123456789012345678901234'
expect(() =>
userInitiatedNotify(eventCode, message, { customCode })
).toThrow()
})
test('when no customCode is specified the correct default is passed to logEvent', () => {
const logEventSpy = jest
.spyOn(events.lib, 'logEvent')
.mockImplementation(() => {})
userInitiatedNotify(eventCode, message)
const lastCallIndex = logEventSpy.mock.calls.length - 1
expect(logEventSpy.mock.calls[lastCallIndex][0]).toMatchObject({
customCode: `custom ${eventCode}`
})
logEventSpy.mockRestore()
})
test('when a customCode is specified it is included in the object passed to logEvent', () => {
const customCode = '123'
const logEventSpy = jest
.spyOn(events.lib, 'logEvent')
.mockImplementation(() => {})
userInitiatedNotify(eventCode, message, { customCode })
const lastCallIndex = logEventSpy.mock.calls.length - 1
expect(logEventSpy.mock.calls[lastCallIndex][0]).toMatchObject({
customCode
})
logEventSpy.mockRestore()
})
test('triggers notification and defaults to correct timeout', () => {
userInitiatedNotify('success', message)
// notification should exist after being triggered
expect(
state.iframeDocument.body.innerHTML.includes(message)
).toBeTruthy()
// advance time past the defaultTimeout
jest.advanceTimersByTime(defaultTimeout(eventCode) + 500)
// notification should have timed out
expect(
state.iframeDocument.body.innerHTML.includes(message)
).toBeFalsy()
})
test(`doesn't throw if dismiss is called after the notification has already timed out`, () => {
expect(() => {
const notify = userInitiatedNotify(eventCode, message, {
customTimeout: 100
})
setTimeout(() => {
notify()
}, 1000)
jest.advanceTimersByTime(500) // trigger timeout
jest.runOnlyPendingTimers() // trigger notify
}).not.toThrow()
})
test(`doesn't throw if notification times out after dismiss was already called`, () => {
expect(() => {
const notify = userInitiatedNotify(eventCode, message, {
customTimeout: 1000
})
setTimeout(() => {
notify()
}, 100)
jest.advanceTimersByTime(500) // trigger notify
jest.runOnlyPendingTimers() // trigger timeout
}).not.toThrow()
})
test(`setting customTimeout overrides the default timeout`, () => {
userInitiatedNotify(eventCode, message, { customTimeout: 500 })
jest.advanceTimersByTime(1000)
expect(
state.iframeDocument.body.innerHTML.includes(message)
).toBeFalsy()
})
test(`setting customTimeout to -1 stops it automatically timing out`, () => {
userInitiatedNotify(eventCode, message, { customTimeout: -1 })
jest.advanceTimersByTime(ONE_MIN_IN_MS)
expect(
state.iframeDocument.body.innerHTML.includes(message)
).toBeTruthy()
})
})
})
})
})

beforeEach(() => {
da.init({ dappId: '123' })
})

afterEach(() => {
updateState(initialState)
})
16 changes: 14 additions & 2 deletions src/js/helpers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,20 @@ export function handleEvent(eventObj, clickHandlers) {
eventCode === 'txSpeedUp' ||
eventCode === 'txCancel'

// If not a server event then log it
!serverEvent && lib.logEvent(eventObj)
let eventToLog = { ...eventObj }

// If dealing with a custom notification the logged event
// should have it's event and category code changed
if (categoryCode === 'userInitiatedNotify') {
eventToLog = {
...eventToLog,
categoryCode: 'custom',
eventCode: 'txNotification'
}
}

// Log everything that isn't a server event
!serverEvent && lib.logEvent(eventToLog)

// If tx status is 'completed', UI has been already handled
if (eventCode === 'txConfirmed' || eventCode === 'txConfirmedClient') {
Expand Down
3 changes: 3 additions & 0 deletions src/js/helpers/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export function eventCodeToType(eventCode) {
case 'txSent':
case 'txSpeedUp':
case 'txCancel':
case 'pending':
return 'progress'
case 'txSendFail':
case 'txStall':
Expand All @@ -114,9 +115,11 @@ export function eventCodeToType(eventCode) {
case 'txRepeat':
case 'txAwaitingApproval':
case 'txConfirmReminder':
case 'error':
return 'failed'
case 'txConfirmed':
case 'txConfirmedClient':
case 'success':
return 'complete'
default:
return undefined
Expand Down
4 changes: 3 additions & 1 deletion src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import assistStyles from '~/css/styles.css'

import { state, updateState } from './helpers/state'
import { handleEvent } from './helpers/events'
import notify from './logic/user-initiated-notify'
import {
legacyCall,
legacySend,
Expand Down Expand Up @@ -96,7 +97,8 @@ function init(config) {
onboard,
Contract,
Transaction,
getState
getState,
notify
}

getState().then(state => {
Expand Down
56 changes: 56 additions & 0 deletions src/js/logic/user-initiated-notify.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import uuid from 'uuid/v4'
import { handleEvent } from '~/js/helpers/events'
import { getAllByQuery, removeNotification } from '~/js/views/dom'

export const defaultTimeout = eventCode => {
if (eventCode === 'success') return 2000
if (eventCode === 'pending') return 5000
if (eventCode === 'error') return 5000
throw new Error('Invalid eventCode')
}

// Allow the developer to spawn custom notifications
export default function userInitiatedNotify(
eventCode,
message,
{
customTimeout = defaultTimeout(eventCode),
customCode = `custom ${eventCode}`
} = {}
) {
// Validate message
if (typeof message !== 'string') throw new Error('Message is required')
// Validate eventCode
if (
eventCode !== 'success' &&
eventCode !== 'pending' &&
eventCode !== 'error'
)
throw new Error(`eventCode must be one of: ['success', 'pending', 'error']`)
// Validate customTimeout
if (customTimeout && typeof customTimeout !== 'number')
throw new Error('customTimeout must be a number')
// Validate customCode
if (customCode) {
if (typeof customCode !== 'string')
throw new Error('customCode must be a string')
if (customCode.length > 24)
throw new Error('customCode must be less than 24 characters')
}

const id = uuid()
handleEvent({
eventCode,
categoryCode: 'userInitiatedNotify',
transaction: { id, startTime: Date.now() },
inlineCustomMsgs: { [eventCode]: () => message },
customTimeout: customTimeout !== -1 && customTimeout,
customCode
})

// Return a callback the user can use to dismiss the notification
const notification = getAllByQuery(`.bn-${id}`)[0]
return () => {
removeNotification(notification)
}
}
Loading