From 92e32738fc5f567506b4e099dca24d2e0d5324bd Mon Sep 17 00:00:00 2001
From: jamaljsr <1356600+jamaljsr@users.noreply.github.com>
Date: Tue, 9 Jun 2020 15:31:12 -0400
Subject: [PATCH 01/19] ui: fix style and layout issues on smaller screens
---
app/public/index.html | 1 -
app/src/App.scss | 1 +
app/src/__tests__/store/settingsStore.spec.ts | 29 +++++++++++++++----
app/src/components/layout/Layout.tsx | 7 ++++-
app/src/store/stores/settingsStore.ts | 18 ++++++++++++
app/src/store/stores/uiStore.ts | 3 ++
6 files changed, 52 insertions(+), 7 deletions(-)
diff --git a/app/public/index.html b/app/public/index.html
index 017a815d8..5dfb05361 100644
--- a/app/public/index.html
+++ b/app/public/index.html
@@ -3,7 +3,6 @@
-
diff --git a/app/src/App.scss b/app/src/App.scss
index 30880b37d..0d7052f30 100644
--- a/app/src/App.scss
+++ b/app/src/App.scss
@@ -75,4 +75,5 @@ body,
#root {
min-height: 100vh;
width: 100%;
+ min-width: 1024px;
}
diff --git a/app/src/__tests__/store/settingsStore.spec.ts b/app/src/__tests__/store/settingsStore.spec.ts
index 1016f2e34..14c428a43 100644
--- a/app/src/__tests__/store/settingsStore.spec.ts
+++ b/app/src/__tests__/store/settingsStore.spec.ts
@@ -4,6 +4,13 @@ import { createStore, SettingsStore } from 'store';
describe('SettingsStore', () => {
let store: SettingsStore;
+ const runInWindowSize = (width: number, func: () => void) => {
+ const defaultWidth = window.innerWidth;
+ (window as any).innerWidth = width;
+ func();
+ (window as any).innerWidth = defaultWidth;
+ };
+
beforeEach(() => {
store = createStore().settingsStore;
});
@@ -24,11 +31,23 @@ describe('SettingsStore', () => {
expect(store.balanceMode).toEqual(BalanceMode.routing);
});
- it('should do nothing if nothing is saved in storage', () => {
- store.load();
+ it('should use defaults if nothing is saved in storage', () => {
+ runInWindowSize(1250, () => {
+ store.load();
+
+ expect(store.sidebarVisible).toEqual(true);
+ expect(store.unit).toEqual(Unit.sats);
+ expect(store.balanceMode).toEqual(BalanceMode.receive);
+ });
+ });
+
+ it('should auto hide sidebar if width is less than 1200', () => {
+ runInWindowSize(1100, () => {
+ store.load();
- expect(store.sidebarVisible).toEqual(true);
- expect(store.unit).toEqual(Unit.sats);
- expect(store.balanceMode).toEqual(BalanceMode.receive);
+ expect(store.sidebarVisible).toEqual(false);
+ expect(store.unit).toEqual(Unit.sats);
+ expect(store.balanceMode).toEqual(BalanceMode.receive);
+ });
});
});
diff --git a/app/src/components/layout/Layout.tsx b/app/src/components/layout/Layout.tsx
index a5203c88a..a936fdc24 100644
--- a/app/src/components/layout/Layout.tsx
+++ b/app/src/components/layout/Layout.tsx
@@ -22,7 +22,7 @@ const Styled = {
position: absolute;
top: 35px;
left: 10px;
- z-index: 1;
+ z-index: 2;
padding: 4px;
&:hover {
@@ -36,6 +36,7 @@ const Styled = {
position: fixed;
top: 0;
height: 100vh;
+ z-index: 1;
background-color: ${props => props.theme.colors.darkBlue};
overflow: hidden;
@@ -53,6 +54,10 @@ const Styled = {
margin-left: ${props => (props.collapsed ? '0' : '285px')};
padding: 0 15px;
transition: all 0.2s;
+
+ @media (max-width: 1200px) {
+ margin-left: 0;
+ }
`,
};
diff --git a/app/src/store/stores/settingsStore.ts b/app/src/store/stores/settingsStore.ts
index cad5924ab..7b76e658c 100644
--- a/app/src/store/stores/settingsStore.ts
+++ b/app/src/store/stores/settingsStore.ts
@@ -14,6 +14,9 @@ export default class SettingsStore {
/** determines if the sidebar nav is visible */
@observable sidebarVisible = true;
+ /** determines if the sidebar should collapse automatically for smaller screen widths */
+ @observable autoCollapse = false;
+
/** specifies which denomination to show units in */
@observable unit: Unit = Unit.sats;
@@ -36,6 +39,15 @@ export default class SettingsStore {
this._store.log.info('updated SettingsStore.showSidebar', toJS(this.sidebarVisible));
}
+ /**
+ * collapses the sidebar if `autoCollapse` is enabled
+ */
+ @action.bound autoCollapseSidebar() {
+ if (this.autoCollapse && this.sidebarVisible) {
+ this.sidebarVisible = false;
+ }
+ }
+
/**
* sets the unit to display throughout the app
*/
@@ -83,5 +95,11 @@ export default class SettingsStore {
this.balanceMode = settings.balanceMode;
this._store.log.info('loaded settings', settings);
}
+
+ // enable automatic sidebar collapsing for smaller screens
+ if (window.innerWidth && window.innerWidth <= 1200) {
+ this.autoCollapse = true;
+ this.sidebarVisible = false;
+ }
}
}
diff --git a/app/src/store/stores/uiStore.ts b/app/src/store/stores/uiStore.ts
index 8d4c643f1..938de635c 100644
--- a/app/src/store/stores/uiStore.ts
+++ b/app/src/store/stores/uiStore.ts
@@ -37,6 +37,7 @@ export default class UiStore {
@action.bound
goToLoop() {
this.page = 'loop';
+ this._store.settingsStore.autoCollapseSidebar();
this._store.log.info('Go to the Loop page');
}
@@ -44,6 +45,7 @@ export default class UiStore {
@action.bound
goToHistory() {
this.page = 'history';
+ this._store.settingsStore.autoCollapseSidebar();
this._store.log.info('Go to the History page');
}
@@ -52,6 +54,7 @@ export default class UiStore {
goToSettings() {
this.page = 'settings';
this.selectedSetting = 'general';
+ this._store.settingsStore.autoCollapseSidebar();
this._store.log.info('Go to the Settings page');
}
From f006e62ae79fdc62cd9198818bec2033c3cc71f9 Mon Sep 17 00:00:00 2001
From: jamaljsr <1356600+jamaljsr@users.noreply.github.com>
Date: Wed, 10 Jun 2020 16:11:04 -0400
Subject: [PATCH 02/19] chore: remove default ReactJS readme
---
app/README.md | 44 --------------------------------------------
1 file changed, 44 deletions(-)
delete mode 100644 app/README.md
diff --git a/app/README.md b/app/README.md
deleted file mode 100644
index 64e343e18..000000000
--- a/app/README.md
+++ /dev/null
@@ -1,44 +0,0 @@
-This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
-
-## Available Scripts
-
-In the project directory, you can run:
-
-### `yarn start`
-
-Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
-
-The page will reload if you make edits.
-You will also see any lint errors in the console.
-
-### `yarn test`
-
-Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
-
-### `yarn build`
-
-Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance.
-
-The build is minified and the filenames include the hashes.
-Your app is ready to be deployed!
-
-See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
-
-### `yarn eject`
-
-**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
-
-If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
-
-Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
-
-You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
-
-## Learn More
-
-You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
-
-To learn React, check out the [React documentation](https://reactjs.org/).
From 3f9d33a89d7f021f5eae92bc0a29cf323e80d7c6 Mon Sep 17 00:00:00 2001
From: jamaljsr <1356600+jamaljsr@users.noreply.github.com>
Date: Thu, 11 Jun 2020 10:31:43 -0400
Subject: [PATCH 03/19] swaps: limit max Loop amount to be based on channel
balances
---
.../components/loop/LoopPage.spec.tsx | 5 +-
.../components/loop/SwapWizard.spec.tsx | 4 +-
.../__tests__/store/buildSwapStore.spec.ts | 76 +++++++++++--
app/src/components/loop/LoopActions.tsx | 29 +++--
.../components/loop/swap/SwapConfigStep.tsx | 4 +-
app/src/i18n/locales/en-US.json | 1 +
app/src/store/stores/buildSwapStore.ts | 100 ++++++++++++++----
7 files changed, 179 insertions(+), 40 deletions(-)
diff --git a/app/src/__tests__/components/loop/LoopPage.spec.tsx b/app/src/__tests__/components/loop/LoopPage.spec.tsx
index ab97ed236..3d88e59b2 100644
--- a/app/src/__tests__/components/loop/LoopPage.spec.tsx
+++ b/app/src/__tests__/components/loop/LoopPage.spec.tsx
@@ -17,6 +17,7 @@ describe('LoopPage component', () => {
beforeEach(async () => {
store = createStore();
await store.fetchAllData();
+ await store.buildSwapStore.getTerms();
});
const render = () => {
@@ -106,7 +107,7 @@ describe('LoopPage component', () => {
store.channelStore.sortedChannels.slice(0, 1).forEach(c => {
store.buildSwapStore.toggleSelectedChannel(c.chanId);
});
- fireEvent.click(getByText('Loop In'));
+ fireEvent.click(getByText('Loop Out'));
expect(getByText('Step 1 of 2')).toBeInTheDocument();
});
@@ -117,7 +118,7 @@ describe('LoopPage component', () => {
store.channelStore.sortedChannels.slice(0, 1).forEach(c => {
store.buildSwapStore.toggleSelectedChannel(c.chanId);
});
- fireEvent.click(getByText('Loop In'));
+ fireEvent.click(getByText('Loop Out'));
expect(getByText('Step 1 of 2')).toBeInTheDocument();
fireEvent.click(getByText('arrow-left.svg'));
expect(getByText('Loop History')).toBeInTheDocument();
diff --git a/app/src/__tests__/components/loop/SwapWizard.spec.tsx b/app/src/__tests__/components/loop/SwapWizard.spec.tsx
index 1b59da906..3f2ca2ad6 100644
--- a/app/src/__tests__/components/loop/SwapWizard.spec.tsx
+++ b/app/src/__tests__/components/loop/SwapWizard.spec.tsx
@@ -63,10 +63,10 @@ describe('SwapWizard component', () => {
it('should update the amount when the slider changes', () => {
const { getByText, getByLabelText } = render();
const build = store.buildSwapStore;
- expect(+build.amount).toEqual(625000);
+ expect(+build.amountForSelected).toEqual(625000);
expect(getByText(`625,000 sats`)).toBeInTheDocument();
fireEvent.change(getByLabelText('range-slider'), { target: { value: '575000' } });
- expect(+build.amount).toEqual(575000);
+ expect(+build.amountForSelected).toEqual(575000);
expect(getByText(`575,000 sats`)).toBeInTheDocument();
});
});
diff --git a/app/src/__tests__/store/buildSwapStore.spec.ts b/app/src/__tests__/store/buildSwapStore.spec.ts
index 9b47e8178..733f0f86f 100644
--- a/app/src/__tests__/store/buildSwapStore.spec.ts
+++ b/app/src/__tests__/store/buildSwapStore.spec.ts
@@ -4,7 +4,9 @@ import { grpc } from '@improbable-eng/grpc-web';
import { waitFor } from '@testing-library/react';
import Big from 'big.js';
import { injectIntoGrpcUnary } from 'util/tests';
+import { lndChannel, loopTerms } from 'util/tests/sampleData';
import { BuildSwapStore, createStore, Store } from 'store';
+import { Channel } from 'store/models';
import { SWAP_ABORT_DELAY } from 'store/stores/buildSwapStore';
const grpcMock = grpc as jest.Mocked;
@@ -13,6 +15,18 @@ describe('BuildSwapStore', () => {
let rootStore: Store;
let store: BuildSwapStore;
+ const addChannel = (capacity: number, localBalance: number) => {
+ const remoteBalance = capacity - localBalance;
+ const lndChan = { ...lndChannel, capacity, localBalance, remoteBalance };
+ const channel = new Channel(rootStore, lndChan);
+ channel.chanId = `${channel.chanId}${rootStore.channelStore.channels.size}`;
+ rootStore.channelStore.channels.set(channel.chanId, channel);
+ };
+
+ const round = (amount: number) => {
+ return Math.floor(amount / store.AMOUNT_INCREMENT) * store.AMOUNT_INCREMENT;
+ };
+
beforeEach(async () => {
rootStore = createStore();
await rootStore.fetchAllData();
@@ -65,16 +79,21 @@ describe('BuildSwapStore', () => {
});
});
- it('should adjust the amount after fetching the loop terms', async () => {
- store.setAmount(Big(100));
+ it('should return the amount in between min/max by default', async () => {
await store.getTerms();
- expect(+store.amount).toBe(625000);
- store.setAmount(Big(5000000));
+ expect(+store.amountForSelected).toBe(625000);
+ });
+
+ it('should ensure amount is greater than the min terms', async () => {
+ store.setAmount(Big(loopTerms.minSwapAmount - 100));
await store.getTerms();
- expect(+store.amount).toBe(625000);
- store.setAmount(Big(500000));
+ expect(+store.amountForSelected).toBe(loopTerms.minSwapAmount);
+ });
+
+ it('should ensure amount is less than the max terms', async () => {
+ store.setAmount(Big(loopTerms.maxSwapAmount + 100));
await store.getTerms();
- expect(+store.amount).toBe(500000);
+ expect(+store.amountForSelected).toBe(loopTerms.maxSwapAmount);
});
it('should fetch a loop in quote', async () => {
@@ -224,4 +243,47 @@ describe('BuildSwapStore', () => {
expect(spy).not.toBeCalled();
spy.mockClear();
});
+
+ describe('min/max swap limits', () => {
+ beforeEach(() => {
+ rootStore.channelStore.channels.clear();
+ [
+ { capacity: 200000, local: 100000 },
+ { capacity: 100000, local: 50000 },
+ { capacity: 100000, local: 20000 },
+ ].forEach(({ capacity, local }) => addChannel(capacity, local));
+ });
+
+ it('should limit Loop In max based on all remote balances', async () => {
+ await store.getTerms();
+ store.setDirection(SwapDirection.IN);
+ // should be the sum of all remote balances minus the reserve
+ expect(+store.termsForDirection.max).toBe(round(230000 * 0.99));
+ });
+
+ it('should limit Loop In max based on selected remote balances', async () => {
+ store.toggleSelectedChannel(store.channels[0].chanId);
+ store.toggleSelectedChannel(store.channels[1].chanId);
+ await store.getTerms();
+ store.setDirection(SwapDirection.IN);
+ // should be the sum of the first two remote balances minus the reserve
+ expect(+store.termsForDirection.max).toBe(round(150000 * 0.99));
+ });
+
+ it('should limit Loop Out max based on all local balances', async () => {
+ await store.getTerms();
+ store.setDirection(SwapDirection.OUT);
+ // should be the sum of all local balances minus the reserve
+ expect(+store.termsForDirection.max).toBe(round(170000 * 0.99));
+ });
+
+ it('should limit Loop Out max based on selected local balances', async () => {
+ store.toggleSelectedChannel(store.channels[0].chanId);
+ store.toggleSelectedChannel(store.channels[1].chanId);
+ await store.getTerms();
+ store.setDirection(SwapDirection.OUT);
+ // should be the sum of the first two local balances minus the reserve
+ expect(+store.termsForDirection.max).toBe(round(150000 * 0.99));
+ });
+ });
});
diff --git a/app/src/components/loop/LoopActions.tsx b/app/src/components/loop/LoopActions.tsx
index 5660ecc5a..5bb892eab 100644
--- a/app/src/components/loop/LoopActions.tsx
+++ b/app/src/components/loop/LoopActions.tsx
@@ -2,6 +2,7 @@ import React, { useCallback } from 'react';
import { observer } from 'mobx-react-lite';
import { SwapDirection } from 'types/state';
import { usePrefixedTranslation } from 'hooks';
+import { formatSats } from 'util/formatters';
import { useStore } from 'store';
import { Button, Close, Pill, Refresh } from 'components/base';
import { styled } from 'components/theme';
@@ -15,6 +16,7 @@ const Styled = {
`,
ActionBar: styled.div`
display: inline-block;
+ width: 595px;
padding: 15px;
background-color: ${props => props.theme.colors.darkBlue};
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.5);
@@ -33,6 +35,7 @@ const Styled = {
margin-right: 50px;
`,
Note: styled.span`
+ display: inline-block;
margin-left: 20px;
font-size: ${props => props.theme.sizes.s};
color: ${props => props.theme.colors.gray};
@@ -42,7 +45,12 @@ const Styled = {
const LoopActions: React.FC = () => {
const { l } = usePrefixedTranslation('cmps.loop.LoopActions');
const { buildSwapStore } = useStore();
- const { setDirection, inferredDirection } = buildSwapStore;
+ const {
+ setDirection,
+ inferredDirection,
+ loopOutAllowed,
+ loopInAllowed,
+ } = buildSwapStore;
const handleLoopOut = useCallback(() => setDirection(SwapDirection.OUT), [
setDirection,
]);
@@ -59,24 +67,31 @@ const LoopActions: React.FC = () => {
{selectedCount}
{l('channelsSelected')}
- {!buildSwapStore.loopInAllowed && {l('loopInNote')}}
+ {!loopOutAllowed && !loopInAllowed ? (
+
+ {l('loopMinimumNote', {
+ min: formatSats(buildSwapStore.termsForDirection.min),
+ })}
+
+ ) : !loopInAllowed ? (
+ {l('loopInNote')}
+ ) : null}
) : (