diff --git a/src/action/channel.js b/src/action/channel.js index ed6949483..ed7df7e82 100644 --- a/src/action/channel.js +++ b/src/action/channel.js @@ -259,13 +259,24 @@ class ChannelAction { * the necessary error handling and notification display. * @return {Promise} */ - async closeSelectedChannel() { + async closeSelectedChannel(force = false) { try { const { selectedChannel } = this._store; this._nav.goChannels(); - await this.closeChannel({ channelPoint: selectedChannel.channelPoint }); + await this.closeChannel({ + channelPoint: selectedChannel.channelPoint, + force, + }); } catch (err) { - this._notification.display({ msg: 'Closing channel failed!', err }); + if ( + err && + err.details && + err.details.includes('try force closing it instead') + ) { + this._nav.goChannelForceDelete(); + } else { + this._notification.display({ msg: 'Closing channel failed!', err }); + } } } diff --git a/src/action/nav.js b/src/action/nav.js index cba7268ad..f52502404 100644 --- a/src/action/nav.js +++ b/src/action/nav.js @@ -110,6 +110,10 @@ class NavAction { this._store.route = 'ChannelDelete'; } + goChannelForceDelete() { + this._store.route = 'ChannelForceDelete'; + } + goChannelCreate() { this._store.route = 'ChannelCreate'; } diff --git a/src/computed/channel.js b/src/computed/channel.js index 6b99cabbe..8f5efbcbf 100644 --- a/src/computed/channel.js +++ b/src/computed/channel.js @@ -17,6 +17,7 @@ const ComputedChannel = store => { ); all.sort((a, b) => (a.active === b.active ? 0 : a.active ? -1 : 1)); all.forEach(c => { + c.isClosing = !c.status.includes('open'); c.statusLabel = toCaps(c.status); c.capacityLabel = toAmountLabel(c.capacity, settings); c.localBalanceLabel = toAmountLabel(c.localBalance, settings); diff --git a/src/view/channel-detail.js b/src/view/channel-detail.js index 0fa80bc9d..4bf658ae1 100644 --- a/src/view/channel-detail.js +++ b/src/view/channel-detail.js @@ -49,7 +49,11 @@ const ChannelDetailView = ({ store, nav }) => ( {store.selectedChannel.localBalanceLabel} {store.unitLabel} - diff --git a/src/view/channel-force-delete.js b/src/view/channel-force-delete.js new file mode 100644 index 000000000..980126352 --- /dev/null +++ b/src/view/channel-force-delete.js @@ -0,0 +1,55 @@ +import React from 'react'; +import { StyleSheet } from 'react-native'; +import { observer } from 'mobx-react'; +import PropTypes from 'prop-types'; +import Background from '../component/background'; +import MainContent from '../component/main-content'; +import { H1Text, CopyText } from '../component/text'; +import { FormStretcher } from '../component/form'; +import { Button, ButtonText, PillButton } from '../component/button'; +import { color } from '../component/style'; + +const styles = StyleSheet.create({ + copyTxt: { + marginTop: 10, + }, + deleteBtn: { + alignSelf: 'center', + backgroundColor: color.glas, + width: 400, + }, + cancelBtn: { + marginTop: 5, + marginBottom: 25, + }, +}); + +const ChannelForceDeleteView = ({ nav, channel }) => ( + + + + Force close channel? + + Mutual close of the channel failed. Force closing the channel will + result in a delay to access your funds. + + + channel.closeSelectedChannel(true)} + > + Force close this channel + + + + +); + +ChannelForceDeleteView.propTypes = { + nav: PropTypes.object.isRequired, + channel: PropTypes.object.isRequired, +}; + +export default observer(ChannelForceDeleteView); diff --git a/src/view/main.js b/src/view/main.js index 00d41ca0a..f71f66ba8 100644 --- a/src/view/main.js +++ b/src/view/main.js @@ -29,6 +29,7 @@ import Deposit from './deposit'; import Channel from './channel'; import ChannelDetail from './channel-detail'; import ChannelDelete from './channel-delete'; +import ChannelForceDelete from './channel-force-delete'; import ChannelCreate from './channel-create'; import Transaction from './transaction'; import Setting from './setting'; @@ -142,6 +143,9 @@ class MainView extends Component { {route === 'ChannelDelete' && ( )} + {route === 'ChannelForceDelete' && ( + + )} {route === 'ChannelCreate' && ( )} diff --git a/stories/screen-story.js b/stories/screen-story.js index 689b31a1b..ceccaea34 100644 --- a/stories/screen-story.js +++ b/stories/screen-story.js @@ -27,6 +27,7 @@ import TransactionDetail from '../src/view/transaction-detail'; import Channel from '../src/view/channel'; import ChannelDetail from '../src/view/channel-detail'; import ChannelDelete from '../src/view/channel-delete'; +import ChannelForceDelete from '../src/view/channel-force-delete'; import ChannelCreate from '../src/view/channel-create'; import Home from '../src/view/home'; import Deposit from '../src/view/deposit'; @@ -135,9 +136,23 @@ storiesOf('Screens', module) )) .add('Channel Details', () => ) + .add('Channel Details (Closing)', () => ( + c.status === 'pending-closing' + ), + }} + nav={nav} + /> + )) .add('Channel Delete', () => ( )) + .add('Channel Force Delete', () => ( + + )) .add('Channel Create', () => ( )) diff --git a/test/unit/action/channel.spec.js b/test/unit/action/channel.spec.js index 7f284d7dc..64c700028 100644 --- a/test/unit/action/channel.spec.js +++ b/test/unit/action/channel.spec.js @@ -274,6 +274,27 @@ describe('Action Channels Unit Tests', () => { }); }); + it('should force close open channel and navigate to channels view', async () => { + await channel.closeSelectedChannel(true); + expect(nav.goChannels, 'was called once'); + expect(channel.closeChannel, 'was called with', { + channelPoint: 'some-channel-point', + force: true, + }); + }); + + it('should navigate to force delete channel view in case of channel link error', async () => { + channel.closeChannel.rejects({ + code: 2, + details: + 'unable to gracefully close channel while peer is offline (try force closing it instead): channel link not found', + }); + await channel.closeSelectedChannel(); + expect(nav.goChannels, 'was called once'); + expect(notification.display, 'was not called'); + expect(nav.goChannelForceDelete, 'was called once'); + }); + it('should display notification in case of error event', async () => { channel.closeChannel.rejects(new Error('Boom!')); await channel.closeSelectedChannel(); diff --git a/test/unit/action/nav.spec.js b/test/unit/action/nav.spec.js index 54163d50f..4de7feb25 100644 --- a/test/unit/action/nav.spec.js +++ b/test/unit/action/nav.spec.js @@ -195,6 +195,13 @@ describe('Action Nav Unit Tests', () => { }); }); + describe('goChannelForceDelete()', () => { + it('should set correct route', () => { + nav.goChannelForceDelete(); + expect(store.route, 'to equal', 'ChannelForceDelete'); + }); + }); + describe('goTransactions()', () => { it('should set correct route', () => { nav.goTransactions(); diff --git a/test/unit/computed/channel.spec.js b/test/unit/computed/channel.spec.js index e374f62e4..9dafb1c12 100644 --- a/test/unit/computed/channel.spec.js +++ b/test/unit/computed/channel.spec.js @@ -54,6 +54,7 @@ describe('Computed Channels Unit Tests', () => { expect(store.computedChannels.length, 'to equal', 3); const ch = store.computedChannels.find(t => t.id === '0'); expect(ch.statusLabel, 'to equal', 'Open'); + expect(ch.isClosing, 'to equal', false); expect(ch.capacityLabel, 'to match', /0[,.]02005/); expect(ch.localBalanceLabel, 'to match', /0[,.]0199/); expect(ch.remoteBalanceLabel, 'to match', /0[,.]0001/); @@ -61,6 +62,8 @@ describe('Computed Channels Unit Tests', () => { expect(store.channelBalancePendingLabel, 'to match', /0[,.]006/); expect(store.channelBalanceClosingLabel, 'to match', /0[,.]005/); expect(store.showChannelAlert, 'to equal', false); + const ch2 = store.computedChannels.find(t => t.id === '2'); + expect(ch2.isClosing, 'to equal', true); }); it('should channel values in usd', () => {