Skip to content

Commit 6ced2d3

Browse files
committed
feat: multiple wallets
1 parent 30c9933 commit 6ced2d3

22 files changed

+888
-31
lines changed

locale/texts.pot

+1-1
Original file line numberDiff line numberDiff line change
@@ -2768,4 +2768,4 @@ msgstr ""
27682768

27692769
#: src/components/atomic-swap/ProposalBalanceTable.js:62
27702770
msgid "Receiving"
2771-
msgstr ""
2771+
msgstr ""

src/App.js

+51-5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import SentryPermission from './screens/SentryPermission';
2626
import UnknownTokens from './screens/UnknownTokens';
2727
import Signin from './screens/Signin';
2828
import LockedWallet from './screens/LockedWallet';
29+
import ChooseWallet from './screens/ChooseWallet';
2930
import NewWallet from './screens/NewWallet';
3031
import WalletType from './screens/WalletType';
3132
import SoftwareWalletWarning from './screens/SoftwareWalletWarning';
@@ -45,8 +46,9 @@ import RequestErrorModal from './components/RequestError';
4546
import { GlobalModalContext, MODAL_TYPES } from './components/GlobalModal';
4647
import createRequestInstance from './api/axiosInstance';
4748
import hathorLib from '@hathor/wallet-lib';
48-
import { IPC_RENDERER } from './constants';
4949
import AllAddresses from './screens/AllAddresses';
50+
import AddressList from './screens/AddressList';
51+
import { IPC_RENDERER, LEDGER_ENABLED } from './constants';
5052
import NFTList from './screens/NFTList';
5153
import { resetNavigateTo, updateLedgerClosed } from './actions/index';
5254
import { WALLET_STATUS } from './sagas/wallet';
@@ -80,7 +82,17 @@ function Root() {
8082
useEffect(() => {
8183
if (ledgerClosed) {
8284
LOCAL_STORE.lock();
83-
navigate('/locked/');
85+
wallet.removeHardwareWalletFromStorage();
86+
87+
// If there are other wallets, go to screen to choose wallet
88+
const firstWallet = wallet.getFirstWalletPrefix();
89+
if (firstWallet) {
90+
wallet.setWalletPrefix(firstWallet);
91+
navigate('/choose_wallet');
92+
} else {
93+
wallet.setWalletPrefix(null);
94+
navigate('/wallet_type/');
95+
}
8496
}
8597
}, [ledgerClosed]);
8698

@@ -91,6 +103,22 @@ function Root() {
91103
* - Ledger event handlers
92104
*/
93105
useEffect(() => {
106+
// When Ledger device loses connection or the app is closed
107+
if (this.props.ledgerClosed && !prevProps.ledgerClosed) {
108+
wallet.removeHardwareWalletFromStorage();
109+
110+
// If there are other wallets, go to screen to choose wallet
111+
const firstWallet = wallet.getFirstWalletPrefix();
112+
if (firstWallet) {
113+
wallet.setWalletPrefix(firstWallet);
114+
this.props.history.push('/choose_wallet');
115+
} else {
116+
wallet.setWalletPrefix(null);
117+
this.props.history.push('/wallet_type/');
118+
}
119+
}
120+
121+
// ---
94122
// Detect a previous instalation and migrate
95123
storageUtils.migratePreviousLocalStorage();
96124

@@ -215,6 +243,7 @@ function Root() {
215243
<Route path="/signin" element={<StartedComponent children={ <Signin />} loaded={false} />} />
216244
<Route path="/hardware_wallet" element={<StartedComponent children={ <StartHardwareWallet /> } loaded={false} />} />
217245
<Route path="/locked" element={<DefaultComponent children={<LockedWallet />} />} />
246+
<Route path="/choose_wallet" element={<DefaultComponent children={<ChooseWallet />} />} />
218247
<Route path="/welcome" element={<Welcome />} />
219248
<Route path="/loading_addresses" element={<LoadingAddresses />} />
220249
<Route path="/permission" element={<SentryPermission />} />
@@ -233,7 +262,7 @@ function LoadedWalletComponent({ children }) {
233262

234263
// If was closed and is loaded we need to redirect to locked screen
235264
if ((!isServerScreen) && (LOCAL_STORE.wasClosed() || LOCAL_STORE.isLocked()) && (!LOCAL_STORE.isHardwareWallet())) {
236-
return <Navigate to={ '/locked/' } />;
265+
return <Redirect to={{ pathname: '/choose_wallet/' }} />;
237266
}
238267

239268
// We allow server screen to be shown from locked screen to allow the user to
@@ -303,18 +332,29 @@ function StartedComponent({children, loaded: routeRequiresWalletToBeLoaded}) {
303332
const isServerScreen = location.pathname === '/server';
304333
// Wallet is locked, go to locked screen
305334
if (LOCAL_STORE.isLocked() && !isServerScreen && !LOCAL_STORE.isHardwareWallet()) {
306-
return <Navigate to={ '/locked/' } replace />;
335+
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
307336
}
308337

309338
// Route requires the wallet to be loaded, render it
310339
if (routeRequiresWalletToBeLoaded || isServerScreen) {
311340
return <LoadedWalletComponent children={children} />;
312341
}
313342

343+
// XXX: Wallet reseted?
344+
const firstWallet = wallet.getFirstWalletPrefix();
345+
if (firstWallet && LOCAL_STORE.prefix === '') {
346+
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
347+
}
348+
314349
// Route does not require wallet to be loaded. Redirect to wallet "home" screen
315350
return <Navigate to="/wallet/" replace />;
316351
}
317352

353+
const firstWallet = wallet.getFirstWalletPrefix();
354+
if (firstWallet && hathorLib.storage.store.prefix === '') {
355+
return <Redirect to={{pathname: '/choose_wallet/'}}/>;
356+
}
357+
318358
// Wallet is not loaded, but it is still loading addresses. Go to the loading screen
319359
if (loadingAddresses) {
320360
const location = useLocation();
@@ -378,7 +418,13 @@ function DefaultComponent({ children }) {
378418
LOCAL_STORE.isHardwareWallet()) {
379419
// This will redirect the page to Wallet Type screen
380420
LOCAL_STORE.cleanWallet();
381-
return <Navigate to={ '/wallet_type/' } />;
421+
wallet.removeHardwareWalletFromStorage();
422+
hathorLib.wallet.unlock();
423+
const firstWallet = wallet.getFirstWalletPrefix();
424+
if (firstWallet) {
425+
return <Redirect to={{ pathname: '/choose_wallet/' }} />;
426+
}
427+
return <Redirect to={{ pathname: '/wallet_type/' }} />;
382428
}
383429

384430
// Render the navigation top bar, the component and the error handling modal

src/actions/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,12 @@ export const metadataLoaded = data => ({ type: 'metadata_loaded', payload: data
171171
/**
172172
* Remove token metadata after unregister token
173173
*/
174-
export const removeTokenMetadata = data => ({ type: 'remove_token_metadata', payload: data });
174+
export const removeTokenMetadata = data => ({ type: "remove_token_metadata", payload: data });
175+
176+
/**
177+
* Set the wallet name during start process
178+
*/
179+
export const setInitWalletName = name => ({type: "set_init_wallet_name", payload: name});
175180

176181
/**
177182
* Partially update history and balance

src/components/ModalResetAllData.js

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ class ModalResetAllData extends React.Component {
2929
componentDidMount() {
3030
$('#confirmResetModal').modal('show');
3131
$('#confirmResetModal').on('hidden.bs.modal', this.props.onClose);
32+
// when the modal closes, make sures the fields are reset
33+
// XXX: Check if hide and hidden expressions are redundant
34+
$('#confirmResetModal').on('hide.bs.modal', () => {
35+
this.onDismiss();
36+
});
3237
}
3338

3439
componentWillUnmount() {

src/components/Navigation.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { t } from 'ttag';
1111
import logo from '../assets/images/hathor-white-logo.png';
1212
import Version from './Version';
1313
import ServerStatus from './ServerStatus';
14+
import WalletStatus from './WalletStatus';
1415
import helpers from '../utils/helpers';
1516
import { useSelector } from 'react-redux';
1617
import { FEATURE_TOGGLE_DEFAULTS, NANO_CONTRACTS_FEATURE_TOGGLE } from '../constants';
@@ -73,6 +74,7 @@ function Navigation() {
7374
</li>}
7475
</ul>
7576
<div className="navbar-right d-flex flex-row align-items-center navigation-search">
77+
<WalletStatus />
7678
<ServerStatus />
7779
<Version />
7880
</div>

src/components/TokenBar.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,11 @@ export default function TokenBar () {
7878
*/
7979
const lockWallet = () => {
8080
LOCAL_STORE.lock();
81-
navigate('/locked/');
82-
};
81+
if (hathorLib.wallet.isHardwareWallet()) {
82+
wallet.removeHardwareWalletFromStorage();
83+
}
84+
navigate('/choose_wallet/');
85+
}
8386

8487
/**
8588
* Called when user clicks to go to settings, then redirects to settings screen

src/components/TokenHistory.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class TokenHistory extends React.Component {
5656
* firstHash {string} ID of the first transaction being shown
5757
* lastHash {string} ID of the last transaction being shown
5858
* reference {string} ID of the reference transaction used when clicking on pagination button
59-
* direction {string} 'previous' or 'next', dependending on which pagination button the user has clicked
59+
* direction {string} 'previous' or 'next', depending on which pagination button the user has clicked
6060
* transactions {Array} List of transactions to be shown in the screen
6161
* shouldFetch {Boolean} If should fetch more history (when the fetch returns 0 elements, should be set to false)
6262
*/

src/components/WalletStatus.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* Copyright (c) Hathor Labs and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import React from 'react';
9+
import { t } from 'ttag'
10+
11+
import hathorLib from '@hathor/wallet-lib';
12+
13+
/**
14+
* Component that shows the current wallet name in use
15+
*
16+
* @memberof Components
17+
*/
18+
function WalletStatus(props) {
19+
if (hathorLib.wallet.isHardwareWallet()) {
20+
return null;
21+
}
22+
const listOfWallets = hathorLib.storage.store.getListOfWallets();
23+
const walletName = listOfWallets[hathorLib.storage.store.prefix].name;
24+
return (
25+
<div className="d-flex flex-column version-wrapper align-items-center">
26+
<span>{t`Current Wallet`}</span>
27+
<span>{walletName}</span>
28+
</div>
29+
);
30+
}
31+
32+
export default WalletStatus;

src/constants.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,6 @@ export const LEDGER_MAX_VERSION = '2.0.0';
227227
*/
228228
export const LEDGER_FIRST_CUSTOM_TOKEN_COMPATIBLE_VERSION = '1.1.0'
229229

230-
231230
/**
232231
* Wallet service URLs
233232
*/
@@ -286,3 +285,8 @@ export const colors = {
286285
* when changing the address of a nano contract
287286
*/
288287
export const NANO_UPDATE_ADDRESS_LIST_COUNT = 5;
288+
289+
/**
290+
* Hardware wallet name on storage
291+
*/
292+
export const HARDWARE_WALLET_NAME = '$HARDWARE%'

src/errors.js

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* Copyright (c) Hathor Labs and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
export class WalletDoesNotExistError extends Error {
9+
constructor(prefix) {
10+
super(`Wallet does not exist: ${prefix}`);
11+
this.name = "WalletDoesNotExistError";
12+
}
13+
};
14+
15+
export class WalletAlreadyExistError extends Error {
16+
constructor() {
17+
super('Prefix already in use');
18+
this.name = "WalletAlreadyExistError";
19+
}
20+
};
21+
22+
export class InvalidWalletNameError extends Error {
23+
constructor() {
24+
super('Invalid wallet name');
25+
this.name = "InvalidWalletNameError";
26+
}
27+
};

src/featureFlags.js

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import events from 'events';
2+
import { UnleashClient } from 'unleash-proxy-client';
3+
import {
4+
UNLEASH_URL,
5+
UNLEASH_CLIENT_KEY,
6+
UNLEASH_POLLING_INTERVAL,
7+
} from './constants';
8+
9+
const IGNORE_WALLET_SERVICE_FLAG = 'featureFlags:ignoreWalletServiceFlag';
10+
11+
export class FeatureFlags extends events.EventEmitter {
12+
constructor(userId, network) {
13+
super();
14+
15+
this.userId = userId;
16+
this.network = network;
17+
this.walletServiceFlag = `wallet-service-wallet-desktop-${this.network}.rollout`;
18+
this.walletServiceEnabled = null;
19+
this.client = new UnleashClient({
20+
url: UNLEASH_URL,
21+
clientKey: UNLEASH_CLIENT_KEY,
22+
refreshInterval: UNLEASH_POLLING_INTERVAL,
23+
appName: `wallet-service-wallet-desktop`,
24+
});
25+
26+
this.client.on('update', () => {
27+
// Get current flag
28+
const walletServiceEnabled = this.client.isEnabled(this.walletServiceFlag);
29+
30+
// We should only emit an update if we already had a value on the instance
31+
// and if the value has changed
32+
if (this.walletServiceEnabled !== null && (
33+
this.walletServiceEnabled !== walletServiceEnabled
34+
)) {
35+
this.walletServiceEnabled = walletServiceEnabled;
36+
this.emit('wallet-service-enabled', walletServiceEnabled);
37+
}
38+
});
39+
}
40+
41+
/**
42+
* Uses the Hathor Unleash Server and Proxy to determine if the
43+
* wallet should use the WalletService facade or the old facade
44+
*
45+
* @params {string} userId An user identifier (e.g. the firstAddress)
46+
* @params {string} network The network name ('mainnet' or 'testnet')
47+
*
48+
* @return {boolean} The result from the unleash feature flag
49+
*/
50+
async shouldUseWalletService() {
51+
try {
52+
const shouldIgnore = await localStorage.getItem(IGNORE_WALLET_SERVICE_FLAG);
53+
if (shouldIgnore) {
54+
return false;
55+
}
56+
this.client.updateContext({ userId: this.userId });
57+
58+
// Start polling for feature flag updates
59+
await this.client.start();
60+
61+
// start() method will have already called the fetchToggles, so the flag should be enabled
62+
const isWalletServiceEnabled = this.client.isEnabled(this.walletServiceFlag);
63+
this.walletServiceEnabled = isWalletServiceEnabled;
64+
65+
return this.walletServiceEnabled;
66+
} catch (e) {
67+
// If our feature flag service is unavailable, we default to the
68+
// old facade
69+
return false;
70+
}
71+
}
72+
73+
/**
74+
* Sets the ignore flag on the storage to persist it between app restarts
75+
*/
76+
async ignoreWalletServiceFlag() {
77+
await localStorage.setItem(IGNORE_WALLET_SERVICE_FLAG, 'true');
78+
this.walletServiceEnabled = false;
79+
80+
// Stop the client from polling
81+
this.client.stop();
82+
}
83+
84+
/**
85+
* Removes the ignore flag from the storage
86+
*/
87+
static clearIgnoreWalletServiceFlag() {
88+
localStorage.removeItem(IGNORE_WALLET_SERVICE_FLAG);
89+
}
90+
}

0 commit comments

Comments
 (0)