bnc-assist
Advanced tools
Comparing version 0.6.2 to 0.7.0
@@ -6,1 +6,7 @@ // eslint-disable-next-line import/no-extraneous-dependencies | ||
MockDate.set('1/1/2010') | ||
// Set a single userAgent to use across all development environments | ||
Object.defineProperty(window.navigator, 'userAgent', { | ||
value: | ||
'Mozilla/ 5.0(linux) AppleWebKit / 537.36(KHTML, like Gecko) jsdom / 11.12.0' | ||
}) |
{ | ||
"name": "bnc-assist", | ||
"version": "0.6.2", | ||
"version": "0.7.0", | ||
"description": "Blocknative Assist js library for Dapp developers", | ||
@@ -10,2 +10,3 @@ "main": "lib/assist.min.js", | ||
"lint": "eslint src/ --fix", | ||
"pretest": "multidep multidep.json", | ||
"test": "TZ=Europe/Paris jest" | ||
@@ -66,2 +67,3 @@ }, | ||
"mockdate": "^2.0.2", | ||
"multidep": "^2.0.2", | ||
"prettier": "^1.15.2", | ||
@@ -82,2 +84,2 @@ "regenerator-runtime": "^0.13.1", | ||
] | ||
} | ||
} |
@@ -46,3 +46,3 @@ # Assist.js | ||
The library uses [semantic versioning](https://semver.org/spec/v2.0.0.html). | ||
The current version is 0.6.2. | ||
The current version is 0.7.0. | ||
There are minified and non-minified versions. | ||
@@ -52,7 +52,7 @@ Put this script at the top of your `<head>` | ||
```html | ||
<script src="https://assist.blocknative.com/0-6-2/assist.js"></script> | ||
<script src="https://assist.blocknative.com/0-7-0/assist.js"></script> | ||
<!-- OR... --> | ||
<script src="https://assist.blocknative.com/0-6-2/assist.min.js"></script> | ||
<script src="https://assist.blocknative.com/0-7-0/assist.min.js"></script> | ||
``` | ||
@@ -416,2 +416,75 @@ | ||
### `updateStyle(style)` | ||
#### Parameters | ||
`style` - `Object`: Object containing new style information (**Required**) | ||
```javascript | ||
const style = { | ||
darkMode: Boolean, // Set Assist UI to dark mode | ||
css: String, // Custom css string to overide Assist default styles | ||
notificationsPosition: String, // Defines which corner transaction notifications will be positioned. Options: 'topLeft', 'topRight', 'bottomRight', 'bottomLeft'. ['bottomRight'] | ||
} | ||
``` | ||
#### Examples | ||
```javascript | ||
// Enable dark mode | ||
const style = { | ||
darkMode: true | ||
} | ||
assistInstance.updateStyle(style) | ||
// Disable dark mode and set notification background to black | ||
const style = { | ||
darkMode: false, | ||
css: `.bn-notification { background: black }` | ||
} | ||
assistInstance.updateStyle(style) | ||
``` | ||
### `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 | ||
```javascript | ||
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 | ||
@@ -418,0 +491,0 @@ |
@@ -35,2 +35,21 @@ import { initialState } from './index.test' | ||
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: { | ||
@@ -37,0 +56,0 @@ categories: ['onboard'], |
import 'jest-dom/extend-expect' | ||
import assist from '~/js' | ||
import { createIframe } from '~/js/helpers/iframe' | ||
import styles from '~/css/styles.css' | ||
import { updateState } from '~/js/helpers/state' | ||
import { updateState, initialState } from '~/js/helpers/state' | ||
import { handleEvent } from '~/js/helpers/events' | ||
@@ -15,1 +17,54 @@ test('iframe gets rendered to document', () => { | ||
}) | ||
// Check that updating notificationsPosition doesn't throw any errors | ||
describe('when the initial notification position is bottomRight', () => { | ||
let notificationsPosition | ||
let config | ||
beforeEach(() => { | ||
notificationsPosition = 'bottomRight' | ||
config = { dappId: '123', style: { notificationsPosition } } | ||
}) | ||
describe('and there are no notifications in the DOM', () => { | ||
test(`changing the notification position to topLeft doesn't throw`, () => { | ||
const da = assist.init(config) | ||
da.updateStyle({ notificationsPosition: 'topLeft' }) | ||
}) | ||
}) | ||
describe('and there are notifications in the DOM', () => { | ||
test(`changing the notification position to topLeft doesn't throw`, () => { | ||
const da = assist.init(config) | ||
handleEvent({ eventCode: 'txPending', categoryCode: 'activeTransaction' }) | ||
handleEvent({ eventCode: 'txSent', categoryCode: 'activeTransaction' }) | ||
handleEvent({ eventCode: 'txFailed', categoryCode: 'activeTransaction' }) | ||
da.updateStyle({ notificationsPosition: 'topLeft' }) | ||
}) | ||
}) | ||
}) | ||
describe('when the initial notification position is topLeft', () => { | ||
let notificationsPosition | ||
let config | ||
beforeEach(() => { | ||
notificationsPosition = 'topLeft' | ||
config = { dappId: '123', style: { notificationsPosition } } | ||
}) | ||
describe('and there are no notifications in the DOM', () => { | ||
test(`changing the notification position to bottomRight doesn't throw`, () => { | ||
const da = assist.init(config) | ||
da.updateStyle({ notificationsPosition: 'bottomRight' }) | ||
}) | ||
}) | ||
describe('and there are notifications in the DOM', () => { | ||
test(`changing the notification position to bottomRight doesn't throw`, () => { | ||
const da = assist.init(config) | ||
handleEvent({ eventCode: 'txPending', categoryCode: 'activeTransaction' }) | ||
handleEvent({ eventCode: 'txSent', categoryCode: 'activeTransaction' }) | ||
handleEvent({ eventCode: 'txFailed', categoryCode: 'activeTransaction' }) | ||
da.updateStyle({ notificationsPosition: 'bottomRight' }) | ||
}) | ||
}) | ||
}) | ||
afterEach(() => { | ||
updateState(initialState) | ||
}) |
@@ -11,3 +11,2 @@ import Web3 from 'web3' | ||
jest.mock('../../js/helpers/browser') | ||
jest.mock('../../js/helpers/state') | ||
jest.mock('../../js/helpers/iframe') | ||
@@ -18,3 +17,5 @@ | ||
// this is a little hacky but it's easier than creating a __mocks__ directory just for this case | ||
websocket.openWebsocketConnection = jest.fn(() => ({ then: jest.fn() })) | ||
websocket.openWebsocketConnection = jest.fn(() => { | ||
jest.fn() | ||
}) | ||
@@ -54,2 +55,3 @@ const assist = da.init({ dappId: 'something' }) | ||
) | ||
try { | ||
@@ -107,3 +109,3 @@ assist.Contract(contract) | ||
const web3 = new Web3('ws://example.com') | ||
stateMock.state.validApiKey = true | ||
stateMock.state = { validApiKey: true, accessToAccounts: true } | ||
da.init({ dappId: 'something', web3, headlessMode: true }) | ||
@@ -115,3 +117,4 @@ expect(iframeMock.createIframe).toHaveBeenCalledTimes(0) | ||
beforeEach(() => { | ||
stateMock.updateState(stateMock.initialState) | ||
events.handleEvent.mockClear() | ||
}) |
@@ -18,5 +18,17 @@ import eventToUI from '~/js/views/event-to-ui' | ||
// 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: 'notification' | ||
} | ||
} | ||
// Log everything that isn't a server event | ||
!serverEvent && lib.logEvent(eventToLog) | ||
// If tx status is 'completed', UI has been already handled | ||
@@ -23,0 +35,0 @@ if (eventCode === 'txConfirmed' || eventCode === 'txConfirmedClient') { |
@@ -1,2 +0,2 @@ | ||
import { positionElement } from '~/js/views/dom' | ||
import { positionElement, updateNotificationsPosition } from '~/js/views/dom' | ||
import darkModeStyles from '~/css/dark-mode.css' | ||
@@ -6,5 +6,52 @@ | ||
export function updateStyle({ darkMode, css, notificationsPosition }) { | ||
const { iframeDocument } = state | ||
const darkModeStyleElement = iframeDocument.getElementById('dark-mode-style') | ||
const customCssStyleElement = iframeDocument.getElementById( | ||
'custom-css-style' | ||
) | ||
// update darkMode | ||
if (typeof darkMode === 'boolean') { | ||
const newConfig = { | ||
...state.config, | ||
style: { | ||
...state.config.style, | ||
darkMode | ||
} | ||
} | ||
darkModeStyleElement.innerHTML = darkMode ? darkModeStyles : '' | ||
updateState({ config: newConfig }) | ||
} | ||
// update custom css | ||
if (css) { | ||
const newConfig = { | ||
...state.config, | ||
style: { | ||
...state.config.style, | ||
css | ||
} | ||
} | ||
customCssStyleElement.innerHTML = css | ||
updateState({ config: newConfig }) | ||
} | ||
// update notifications position | ||
if (notificationsPosition) { | ||
const newConfig = { | ||
...state.config, | ||
style: { | ||
...state.config.style, | ||
notificationsPosition | ||
} | ||
} | ||
updateState({ | ||
config: newConfig | ||
}) | ||
updateNotificationsPosition() | ||
} | ||
} | ||
export function createIframe(browserDocument, assistStyles, style = {}) { | ||
const { darkMode, css } = style | ||
const initialIframeContent = ` | ||
@@ -16,8 +63,4 @@ <html> | ||
</style> | ||
<style> | ||
${darkMode ? darkModeStyles : ''} | ||
</style> | ||
<style> | ||
${css || ''} | ||
</style> | ||
<style id="dark-mode-style"></style> | ||
<style id="custom-css-style"></style> | ||
</head> | ||
@@ -43,2 +86,3 @@ <body></body> | ||
updateState({ iframe, iframeDocument: iDocument, iframeWindow: iWindow }) | ||
updateStyle(style) | ||
} | ||
@@ -45,0 +89,0 @@ |
@@ -43,1 +43,33 @@ export const initialState = { | ||
} | ||
export function filteredState() { | ||
const { | ||
mobileDevice, | ||
validBrowser, | ||
currentProvider, | ||
web3Wallet, | ||
accessToAccounts, | ||
walletLoggedIn, | ||
walletEnabled, | ||
accountAddress, | ||
accountBalance, | ||
minimumBalance, | ||
userCurrentNetworkId, | ||
correctNetwork | ||
} = state | ||
return { | ||
mobileDevice, | ||
validBrowser, | ||
currentProvider, | ||
web3Wallet, | ||
accessToAccounts, | ||
walletLoggedIn, | ||
walletEnabled, | ||
accountAddress, | ||
accountBalance, | ||
minimumBalance, | ||
userCurrentNetworkId, | ||
correctNetwork | ||
} | ||
} |
@@ -109,2 +109,3 @@ import uuid from 'uuid/v4' | ||
case 'txCancel': | ||
case 'pending': | ||
return 'progress' | ||
@@ -118,5 +119,7 @@ case 'txSendFail': | ||
case 'txConfirmReminder': | ||
case 'error': | ||
return 'failed' | ||
case 'txConfirmed': | ||
case 'txConfirmedClient': | ||
case 'success': | ||
return 'complete' | ||
@@ -123,0 +126,0 @@ default: |
@@ -5,4 +5,5 @@ import '@babel/polyfill' | ||
import { state, updateState } from './helpers/state' | ||
import { state, updateState, filteredState } from './helpers/state' | ||
import { handleEvent } from './helpers/events' | ||
import notify from './logic/user-initiated-notify' | ||
import { | ||
@@ -20,3 +21,3 @@ legacyCall, | ||
import { getOverloadedMethodKeys } from './helpers/utilities' | ||
import { createIframe } from './helpers/iframe' | ||
import { createIframe, updateStyle } from './helpers/iframe' | ||
import { | ||
@@ -101,3 +102,5 @@ getTransactionQueueFromStorage, | ||
Transaction, | ||
getState | ||
getState, | ||
updateStyle, | ||
notify | ||
} | ||
@@ -198,3 +201,3 @@ | ||
resolve('User is ready to transact') | ||
resolve(filteredState()) | ||
}) | ||
@@ -236,3 +239,3 @@ } | ||
const ready = await prepareForTransaction('onboard').catch(error => { | ||
await prepareForTransaction('onboard').catch(error => { | ||
removeItem('onboarding') | ||
@@ -243,3 +246,3 @@ reject(error) | ||
removeItem('onboarding') | ||
resolve(ready) | ||
resolve(filteredState()) | ||
}) | ||
@@ -286,5 +289,6 @@ } | ||
contractObj._jsonInterface || | ||
Object.keys(contractObj.abiModel.abi.methods).map( | ||
key => contractObj.abiModel.abi.methods[key].abiItem | ||
) | ||
Object.keys(contractObj.abiModel.abi.methods) | ||
// remove any arrays from the ABI, they contain redundant information | ||
.filter(key => !Array.isArray(contractObj.abiModel.abi.methods[key])) | ||
.map(key => contractObj.abiModel.abi.methods[key].abiItem) | ||
@@ -371,42 +375,40 @@ const contractClone = Object.create(Object.getPrototypeOf(contractObj)) | ||
const methodsKeys = Object.keys(contractObj[key]) | ||
// go through all the methods in the contract ABI and derive | ||
// the 'methods' key of the delegated contract from them | ||
newContractObj.methods = abi.reduce((methodsObj, methodAbi) => { | ||
const { name, type, constant, inputs } = methodAbi | ||
newContractObj.methods = abi.reduce((obj, methodAbi) => { | ||
const { name, type, constant } = methodAbi | ||
// if not function, do nothing with it | ||
if (type !== 'function') { | ||
return obj | ||
return methodsObj | ||
} | ||
// if we have seen this key, then we have already dealt with it | ||
if (seenMethods.includes(name)) { | ||
return obj | ||
// every method can called like contract.methods[methodName](...args). | ||
// add a methodName key to methodsObj allowing it to be called that way. | ||
// it only needs to be assigned once | ||
if (!seenMethods.includes(name)) { | ||
const method = contractObj.methods[name] | ||
methodsObj[name] = (...args) => | ||
constant | ||
? modernCall(method, name, args) | ||
: modernSend(method, name, args) | ||
seenMethods.push(name) | ||
} | ||
seenMethods.push(name) | ||
// add a key to methods allowing the current method to be called | ||
// like contract.methods[`${methodName}(${...args})`](...args) | ||
let overloadedMethodKey | ||
if (inputs.length > 0) { | ||
overloadedMethodKey = `${name}(${getOverloadedMethodKeys(inputs)})` | ||
} else { | ||
overloadedMethodKey = `${name}()` | ||
} | ||
const method = contractObj.methods[name] | ||
const overloadedMethodKeys = methodsKeys.filter( | ||
methodKey => methodKey.split('(')[0] === name && methodKey !== name | ||
) | ||
obj[name] = (...args) => | ||
const overloadedMethod = contractObj.methods[overloadedMethodKey] | ||
methodsObj[overloadedMethodKey] = (...args) => | ||
constant | ||
? modernCall(method, name, args) | ||
: modernSend(method, name, args) | ||
? modernCall(overloadedMethod, name, args) | ||
: modernSend(overloadedMethod, name, args) | ||
if (overloadedMethodKeys.length > 0) { | ||
overloadedMethodKeys.forEach(key => { | ||
const method = contractObj.methods[key] | ||
obj[key] = (...args) => | ||
constant | ||
? modernCall(method, name, args) | ||
: modernSend(method, name, args) | ||
}) | ||
} | ||
return obj | ||
return methodsObj | ||
}, {}) | ||
@@ -467,31 +469,3 @@ } | ||
await checkUserEnvironment() | ||
const { | ||
mobileDevice, | ||
validBrowser, | ||
currentProvider, | ||
web3Wallet, | ||
accessToAccounts, | ||
walletLoggedIn, | ||
walletEnabled, | ||
accountAddress, | ||
accountBalance, | ||
minimumBalance, | ||
userCurrentNetworkId, | ||
correctNetwork | ||
} = state | ||
resolve({ | ||
mobileDevice, | ||
validBrowser, | ||
currentProvider, | ||
web3Wallet, | ||
accessToAccounts, | ||
walletLoggedIn, | ||
walletEnabled, | ||
accountAddress, | ||
accountBalance, | ||
minimumBalance, | ||
userCurrentNetworkId, | ||
correctNetwork | ||
}) | ||
resolve(filteredState()) | ||
}) | ||
@@ -498,0 +472,0 @@ } |
@@ -20,2 +20,76 @@ import { state } from '~/js/helpers/state' | ||
// Update UI styles based on the current style.notificationsPosition value | ||
export function updateNotificationsPosition() { | ||
const { notificationsPosition } = state.config.style | ||
if (!notificationsPosition) return | ||
positionElement(state.iframe) | ||
// Position notificationsContainer and reorder it's elements | ||
const notificationsContainer = state.iframeDocument.getElementById( | ||
'blocknative-notifications' | ||
) | ||
if (notificationsContainer) { | ||
const brand = notificationsContainer.querySelector( | ||
'#bn-transaction-branding' | ||
) | ||
const scroll = notificationsContainer.querySelector( | ||
'.bn-notifications-scroll' | ||
) | ||
if (notificationsPosition.includes('top')) { | ||
notificationsContainer.insertBefore(brand, scroll) | ||
} else { | ||
notificationsContainer.insertBefore(scroll, brand) | ||
} | ||
positionElement(notificationsContainer) | ||
} | ||
// Update existing status-icon positions | ||
const statusIcons = [ | ||
...state.iframeDocument.getElementsByClassName('bn-status-icon') | ||
] | ||
statusIcons.forEach(icon => { | ||
notificationsPosition.includes('Left') | ||
? icon.classList.add('bn-float-right') | ||
: icon.classList.remove('bn-float-right') | ||
}) | ||
// Update existing progress tooltip positions | ||
const progressTooltips = [ | ||
...state.iframeDocument.getElementsByClassName('progress-tooltip') | ||
] | ||
progressTooltips.forEach(tooltip => { | ||
notificationsPosition.includes('Left') | ||
? tooltip.classList.add('bn-left') | ||
: tooltip.classList.remove('bn-left') | ||
}) | ||
// Update brand position | ||
const brand = state.iframeDocument.getElementById('bn-transaction-branding') | ||
if (brand) { | ||
notificationsPosition.includes('Left') | ||
? (brand.style.float = 'initial') | ||
: (brand.style.float = 'right') | ||
} | ||
// Update existing notifications borders | ||
const notifications = [ | ||
...state.iframeDocument.getElementsByClassName('bn-notification') | ||
] | ||
notifications.forEach(n => { | ||
notificationsPosition.includes('Left') | ||
? n.classList.add('bn-right-border') | ||
: n.classList.remove('bn-right-border') | ||
}) | ||
// Update notifications-scroll position | ||
const scrolls = [ | ||
...state.iframeDocument.getElementsByClassName('bn-notifications-scroll') | ||
] | ||
scrolls.forEach(s => { | ||
notificationsPosition === 'topRight' | ||
? (s.style.float = 'right') | ||
: delete s.style.float | ||
}) | ||
} | ||
export function createElementString(type, className, innerHTML) { | ||
@@ -409,3 +483,3 @@ return ` | ||
setTimeout(() => { | ||
if (parent.contains(element)) { | ||
if (parent && parent.contains(element)) { | ||
parent.removeChild(element) | ||
@@ -449,6 +523,8 @@ if (parent !== state.iframeDocument.body) { | ||
const scrollContainer = getByQuery('.bn-notifications-scroll') | ||
setTimeout( | ||
() => setHeight(scrollContainer, 'initial', 'auto'), | ||
timeouts.changeUI | ||
) | ||
if (scrollContainer) { | ||
setTimeout( | ||
() => setHeight(scrollContainer, 'initial', 'auto'), | ||
timeouts.changeUI | ||
) | ||
} | ||
} | ||
@@ -489,2 +565,4 @@ | ||
const scrollContainer = getByQuery('.bn-notifications-scroll') | ||
// if no notifications to manipulate return | ||
if (!scrollContainer) return | ||
const maxHeight = window.innerHeight | ||
@@ -498,2 +576,3 @@ const brandingHeight = getById('bn-transaction-branding').clientHeight + 26 | ||
setHeight(scrollContainer, 'scroll', maxHeight - brandingHeight) | ||
scrollContainer.scrollTop = maxHeight * 4 | ||
} else { | ||
@@ -500,0 +579,0 @@ setHeight(scrollContainer, 'initial', 'auto') |
@@ -87,2 +87,7 @@ import { state } from '~/js/helpers/state' | ||
txCancel: notificationsUI | ||
}, | ||
userInitiatedNotify: { | ||
success: notificationsUI, | ||
pending: notificationsUI, | ||
error: notificationsUI | ||
} | ||
@@ -164,3 +169,5 @@ } | ||
inlineCustomMsgs, | ||
eventCode | ||
eventCode, | ||
categoryCode, | ||
customTimeout | ||
}) { | ||
@@ -181,3 +188,4 @@ // treat txConfirmedClient as txConfirm | ||
eventCode === 'txStall' || | ||
eventCode === 'txSpeedUp' | ||
eventCode === 'txSpeedUp' || | ||
eventCode === 'pending' | ||
@@ -281,7 +289,10 @@ const showTime = | ||
if (type === 'complete') { | ||
const notificationShouldTimeout = | ||
(type === 'complete' && categoryCode !== 'userInitiatedNotify') || | ||
customTimeout | ||
if (notificationShouldTimeout) { | ||
setTimeout(() => { | ||
removeNotification(notification) | ||
setTimeout(setNotificationsHeight, timeouts.changeUI) | ||
}, timeouts.autoRemoveNotification) | ||
}, customTimeout || timeouts.autoRemoveNotification) | ||
} | ||
@@ -288,0 +299,0 @@ } |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
2227205
82
9126
518
38