mirror of
https://github.com/zhigang1992/xverse-web-extension.git
synced 2026-04-29 13:15:45 +08:00
Merge feature/onboarding into imamahzafar/feat/home-screen-dashboard
This commit is contained in:
4
.babelrc
4
.babelrc
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"presets": ["env", "react"],
|
||||
"plugins": ["react-refresh/babel", "babel-plugin-styled-components"]
|
||||
}
|
||||
7
.babelrc.js
Normal file
7
.babelrc.js
Normal file
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
presets: ['env', 'react'],
|
||||
plugins: [
|
||||
'babel-plugin-styled-components',
|
||||
process.env.NODE_ENV === 'development' && require.resolve('react-refresh/babel'),
|
||||
].filter(Boolean),
|
||||
};
|
||||
1685
package-lock.json
generated
1685
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -4,13 +4,23 @@
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@stacks/transactions": "^4.3.7",
|
||||
"c32check": "^2.0.0",
|
||||
"@stacks/encryption": "4.3.5",
|
||||
"@stacks/keychain": "4.3.5",
|
||||
"@stacks/network": "4.3.5",
|
||||
"@stacks/transactions": "4.3.5",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"argon2-browser": "^1.18.0",
|
||||
"bignumber.js": "^9.1.0",
|
||||
"c32check": "^2.0.0",
|
||||
"bip32": "^2.0.6",
|
||||
"bip39": "^3.0.3",
|
||||
"bitcoinjs-lib": "^5.2.0",
|
||||
"bn.js": "^5.2.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"i18next": "^21.9.1",
|
||||
"moment": "^2.29.4",
|
||||
"react": "^18.2.0",
|
||||
"react-content-loader": "^6.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
@@ -18,11 +28,15 @@
|
||||
"react-modal": "^3.15.1",
|
||||
"react-number-format": "^5.0.0",
|
||||
"react-qr-code": "^2.0.8",
|
||||
"react-router-dom": "^6.4.0",
|
||||
"react-select": "^5.4.0",
|
||||
"react-spinners-css": "^2.0.1",
|
||||
"react-switch": "^7.0.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"react-redux": "^7.2.1",
|
||||
"react-router-dom": "^6.4.0",
|
||||
"redux": "^4.0.5",
|
||||
"redux-saga": "^1.1.3",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"styled-components": "^5.3.5"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -33,7 +47,8 @@
|
||||
"style": "prettier --write \"src/**/*.{ts,tsx}\""
|
||||
},
|
||||
"resolutions": {
|
||||
"styled-components": "^5"
|
||||
"styled-components": "^5",
|
||||
"buffer": "6.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -44,6 +59,7 @@
|
||||
"devDependencies": {
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@types/argon2-browser": "^1.18.1",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/react": "^18.0.20",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
@@ -56,6 +72,7 @@
|
||||
"babel-plugin-styled-components": "^2.0.7",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"base64-loader": "^1.0.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"css-loader": "^6.7.1",
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import configureStore from '@stores/index';
|
||||
import Theme from '../theme';
|
||||
import GlobalStyle from '../theme/global';
|
||||
import '../locales';
|
||||
import router from './routes';
|
||||
|
||||
function App(): JSX.Element {
|
||||
const store = configureStore();
|
||||
return (
|
||||
<>
|
||||
<GlobalStyle />
|
||||
<ThemeProvider theme={Theme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
<Provider store={store}>
|
||||
<ThemeProvider theme={Theme}>
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
27
src/app/components/guards/auth.tsx
Normal file
27
src/app/components/guards/auth.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { StoreState } from '@stores/root/reducer';
|
||||
import { getEncryptedSeed } from '@utils/localStorage';
|
||||
import { ReactNode } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: ReactNode,
|
||||
}
|
||||
|
||||
function AuthGuard({ children }: AuthGuardProps) {
|
||||
const encryptedSeedPhrase = getEncryptedSeed();
|
||||
const {
|
||||
isLocked,
|
||||
} = useSelector((state: StoreState) => ({
|
||||
...state.walletState,
|
||||
}));
|
||||
|
||||
if (encryptedSeedPhrase && isLocked) return <Navigate to="/login" />;
|
||||
|
||||
if (!encryptedSeedPhrase) return <Navigate to="/landing" />;
|
||||
|
||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
||||
export default AuthGuard;
|
||||
@@ -1,16 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const StyledHeader = styled.h1`
|
||||
font-size: 1.5em;
|
||||
text-align: center;
|
||||
font-family: Satoshi-Regular;
|
||||
color: ${(props) => props.theme.colors.action.classic};
|
||||
`;
|
||||
|
||||
function Header():JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'common' });
|
||||
return <StyledHeader>{t('appName')}</StyledHeader>;
|
||||
}
|
||||
|
||||
export default Header;
|
||||
136
src/app/core/constants/constants.ts
Normal file
136
src/app/core/constants/constants.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { Network } from 'app/core/types/networks';
|
||||
|
||||
export type CurrencyTypes = 'STX' | 'BTC' | 'FT' | 'NFT';
|
||||
|
||||
export type AuthenticationState =
|
||||
| 'Authenticated'
|
||||
| 'NotAuthenticated'
|
||||
| 'Loading'
|
||||
| 'ShouldAuthenticate'
|
||||
| 'ShouldAuthenticatePassword';
|
||||
|
||||
export type StackingState =
|
||||
| 'Loading'
|
||||
| 'NotStacking'
|
||||
| 'Pending'
|
||||
| 'Delegated'
|
||||
| 'Stacking'
|
||||
| 'Completed'
|
||||
| 'Revoked'
|
||||
| 'Error';
|
||||
|
||||
export const ENTROPY_BYTES = 16;
|
||||
|
||||
export interface SettingsNetwork {
|
||||
name: Network;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export const initialNetworksList: SettingsNetwork[] = [
|
||||
{
|
||||
name: 'Mainnet',
|
||||
address: 'https://stacks-node-api.mainnet.stacks.co',
|
||||
},
|
||||
{
|
||||
name: 'Testnet',
|
||||
address: 'https://stacks-node-api.testnet.stacks.co',
|
||||
},
|
||||
];
|
||||
|
||||
export type NftType = 'Image' | 'Video';
|
||||
|
||||
/**
|
||||
* pagination limit of confirmed and mempool transactions
|
||||
*/
|
||||
export const PAGINATION_LIMIT = 20;
|
||||
|
||||
export const BTC_PATH = 'm/49\'/0\'/0\'/0/0';
|
||||
|
||||
export const BTC_PATH_WITHOUT_INDEX = 'm/49\'/0\'/0\'/0/';
|
||||
|
||||
export const BTC_TESTNET_PATH_WITHOUT_INDEX = 'm/49\'/1\'/0\'/0/';
|
||||
|
||||
export const STX_PATH_WITHOUT_INDEX = 'm/44\'/5757\'/0\'/0/';
|
||||
|
||||
export const WALLET_DATA_VERSION = 1;
|
||||
|
||||
export const BITCOIN_DUST_AMOUNT_SATS = 5500;
|
||||
|
||||
export const BNS_CONTRACT_PRINCIPAL = 'SP000000000000000000002Q6VF78.bns';
|
||||
|
||||
/**
|
||||
* if the app is in background and comes to foreground after the refresh time
|
||||
* the app data should be refresh
|
||||
*/
|
||||
export const REFRESH_TIME = 5;
|
||||
|
||||
/**
|
||||
* contract id of send_many transaction type
|
||||
*/
|
||||
export const SEND_MANY_TOKEN_TRANSFER_CONTRACT_PRINCIPAL = 'SP3FBR2AGK5H9QBDH3EEN6DF8EK8JY7RX8QJ5SVTE.send-many-memo';
|
||||
|
||||
export const WEAK_PASSWORD_LENGTH = 5;
|
||||
export const MEDIUM_PASSWORD_LENGTH = 8;
|
||||
export const STRONG_PASSWORD_LENGTH = 12;
|
||||
|
||||
/**
|
||||
* terms of service and privacy policy links
|
||||
*/
|
||||
export const TERMS_LINK = 'https://xverse.app/terms';
|
||||
export const PRIVACY_POLICY_LINK = 'https://xverse.app/privacy';
|
||||
|
||||
export const STACKING_URL = 'https://docs.stacks.co/understand-stacks/stacking';
|
||||
export const BRINK_STACKING_URL = 'https://bitcoinlove.stacks.org';
|
||||
|
||||
/**
|
||||
* btc base urls
|
||||
*/
|
||||
export const BTC_BASE_URI_MAINNET = 'https://api.blockcypher.com/v1/btc/main/addrs/';
|
||||
export const BTC_BASE_URI_TESTNET = 'https://api.blockcypher.com/v1/btc/test3/addrs/';
|
||||
|
||||
export const READ_MORE_SIP_012_LINK = 'https://github.com/hirosystems/sips/blob/draft/sip-012/sips/sip-012/sip-012-cost-limits-network-upgrade.md';
|
||||
|
||||
export const CYCLE_FOR_VOTE = 21;
|
||||
|
||||
/**
|
||||
* multiply stx transaction fees by this number
|
||||
*/
|
||||
export const STX_FEE_MULTIPLIER = 50;
|
||||
|
||||
export const STX_DELEGATION_FEE_MULTIPLIER = 1700;
|
||||
|
||||
/**
|
||||
* change this base url to localhost url for testing
|
||||
* prod base url: https://api.xverse.app
|
||||
*/
|
||||
export const XVERSE_API_BASE_URL_PROD = 'https://api.xverse.app';
|
||||
export const XVERSE_API_BASE_URL_TEST = 'http://localhost:3000';
|
||||
|
||||
export const XVERSE_API_BASE_URL = XVERSE_API_BASE_URL_PROD;
|
||||
|
||||
/**
|
||||
* minimum stx to stack for bitcoin for brink stacking
|
||||
*/
|
||||
export const MIN_BTC_BRINK_STACKING = 10;
|
||||
|
||||
export const BRINK_STACKING_CONTRACT_NAME = 'bitcoin-brink-pool';
|
||||
|
||||
export const STX_STACKING_CONTRACT_NAME = 'xverse-pool-v2';
|
||||
|
||||
export const AVAILABLE_SEARCH_ENGINES = [
|
||||
'google',
|
||||
'bing',
|
||||
'duckduckgo',
|
||||
'yahoo',
|
||||
] as const;
|
||||
|
||||
export const STX_NFT_LINK = 'https://stxnft.com/';
|
||||
|
||||
export const BNS_CONTRACT = 'SP000000000000000000002Q6VF78.bns::names';
|
||||
|
||||
export const TRANSAC_URL = 'https://global.transak.com';
|
||||
export const TRANSAC_API_KEY = '8636faeb-2dd7-41e3-986e-b99db6f63903';
|
||||
|
||||
export const MOON_PAY_URL = 'https://buy.moonpay.com';
|
||||
export const MOON_PAY_API_KEY = 'pk_live_8YeOjOzFqHUG1qi2G6NPA4N1tZAWFihK';
|
||||
export const MIX_PANEL_TOKEN: string = 'eebe8b99e520e760aa38f222b7de0ccf';
|
||||
9
src/app/core/types/accounts.ts
Normal file
9
src/app/core/types/accounts.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface Account {
|
||||
id: number;
|
||||
stxAddress: string;
|
||||
btcAddress: string;
|
||||
masterPubKey: string;
|
||||
stxPublicKey: string;
|
||||
btcPublicKey: string;
|
||||
bnsName?: string;
|
||||
}
|
||||
7
src/app/core/types/networks.ts
Normal file
7
src/app/core/types/networks.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export type Network = 'Mainnet' | 'Testnet';
|
||||
export type NetworkType = 'Mainnet' | 'Testnet';
|
||||
|
||||
export type SettingsNetwork = {
|
||||
type: NetworkType;
|
||||
address: string;
|
||||
};
|
||||
143
src/app/core/wallet.ts
Normal file
143
src/app/core/wallet.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import * as bip39 from 'bip39';
|
||||
import * as bip32 from 'bip32';
|
||||
import {
|
||||
ChainID,
|
||||
getPublicKey,
|
||||
createStacksPrivateKey,
|
||||
publicKeyToString,
|
||||
TransactionVersion,
|
||||
getAddressFromPrivateKey,
|
||||
} from '@stacks/transactions';
|
||||
import { deriveRootKeychainFromMnemonic } from '@stacks/keychain';
|
||||
import { ecPrivateKeyToHexString } from '@stacks/encryption';
|
||||
import {
|
||||
BIP32Interface, ECPair, payments, networks,
|
||||
} from 'bitcoinjs-lib';
|
||||
import BN from 'bn.js';
|
||||
import { encryptSeedPhrase } from '@utils/encryptionUtils';
|
||||
import { storeEncryptedSeed } from '@utils/localStorage';
|
||||
import {
|
||||
BTC_PATH_WITHOUT_INDEX,
|
||||
BTC_TESTNET_PATH_WITHOUT_INDEX,
|
||||
STX_PATH_WITHOUT_INDEX,
|
||||
} from './constants/constants';
|
||||
import { Network } from './types/networks';
|
||||
|
||||
const entropyBytes = 16;
|
||||
|
||||
export const derivationPaths = {
|
||||
[ChainID.Mainnet]: STX_PATH_WITHOUT_INDEX,
|
||||
[ChainID.Testnet]: STX_PATH_WITHOUT_INDEX,
|
||||
};
|
||||
|
||||
export function getDerivationPath(chain: ChainID, index: BN) {
|
||||
return `${derivationPaths[chain]}${index.toString()}`;
|
||||
}
|
||||
|
||||
export function getBitcoinDerivationPath(index: BN, network: Network = 'Mainnet') {
|
||||
return network === 'Mainnet'
|
||||
? `${BTC_PATH_WITHOUT_INDEX}${index.toString()}`
|
||||
: `${BTC_TESTNET_PATH_WITHOUT_INDEX}${index.toString()}`;
|
||||
}
|
||||
|
||||
export function deriveStxAddressChain(chain: ChainID, index: BN = new BN(0)) {
|
||||
return (rootNode: BIP32Interface) => {
|
||||
const childKey = rootNode.derivePath(getDerivationPath(chain, index));
|
||||
if (!childKey.privateKey) {
|
||||
throw new Error('Unable to derive private key from `rootNode`, bip32 master keychain');
|
||||
}
|
||||
const privateKey = ecPrivateKeyToHexString(childKey.privateKey);
|
||||
const txVersion = chain === ChainID.Mainnet
|
||||
? TransactionVersion.Mainnet
|
||||
: TransactionVersion.Testnet;
|
||||
return {
|
||||
childKey,
|
||||
address: getAddressFromPrivateKey(privateKey, txVersion),
|
||||
privateKey,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export async function walletFromSeedPhrase(
|
||||
mnemonic: string,
|
||||
index: BN = new BN(0),
|
||||
network: Network = 'Mainnet',
|
||||
): Promise<{
|
||||
stxAddress: string;
|
||||
btcAddress: string;
|
||||
masterPubKey: string;
|
||||
stxPublicKey: string;
|
||||
btcPublicKey: string;
|
||||
seedPhrase: string;
|
||||
}> {
|
||||
const rootNode = await deriveRootKeychainFromMnemonic(mnemonic);
|
||||
const deriveStxAddressKeychain = deriveStxAddressChain(
|
||||
network === 'Mainnet' ? ChainID.Mainnet : ChainID.Testnet,
|
||||
index,
|
||||
);
|
||||
const { childKey, address, privateKey } = deriveStxAddressKeychain(rootNode);
|
||||
const stxAddress = address;
|
||||
|
||||
const seed = await bip39.mnemonicToSeed(mnemonic);
|
||||
const master = bip32.fromSeed(seed);
|
||||
const masterPubKey = master.publicKey.toString('hex');
|
||||
const stxPublicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(privateKey)));
|
||||
|
||||
// derive segwit btc address
|
||||
|
||||
const btcChild = master.derivePath(getBitcoinDerivationPath(index, network));
|
||||
|
||||
const keyPair = ECPair.fromPrivateKey(btcChild.privateKey!);
|
||||
const segwitBtcAddress = payments.p2sh({
|
||||
redeem: payments.p2wpkh({
|
||||
pubkey: keyPair.publicKey,
|
||||
network: network === 'Mainnet' ? networks.bitcoin : networks.testnet,
|
||||
}),
|
||||
pubkey: keyPair.publicKey,
|
||||
network: network === 'Mainnet' ? networks.bitcoin : networks.testnet,
|
||||
});
|
||||
const btcAddress = segwitBtcAddress.address!;
|
||||
const btcPublicKey = keyPair.publicKey.toString('hex');
|
||||
return {
|
||||
stxAddress,
|
||||
btcAddress,
|
||||
masterPubKey,
|
||||
stxPublicKey,
|
||||
btcPublicKey,
|
||||
seedPhrase: mnemonic,
|
||||
};
|
||||
}
|
||||
|
||||
export async function newWallet(): Promise<{
|
||||
stxAddress: string;
|
||||
btcAddress: string;
|
||||
masterPubKey: string;
|
||||
stxPublicKey: string;
|
||||
btcPublicKey: string;
|
||||
seedPhrase: string;
|
||||
}> {
|
||||
const entropy = crypto.getRandomValues(new Uint8Array(entropyBytes));
|
||||
const mnemonic = bip39.entropyToMnemonic(entropy);
|
||||
return walletFromSeedPhrase(mnemonic);
|
||||
}
|
||||
|
||||
export async function getBtcPrivateKey(
|
||||
seedPhrase: string,
|
||||
index: BN = new BN(0),
|
||||
network: Network = 'Mainnet',
|
||||
): Promise<string> {
|
||||
const seed = await bip39.mnemonicToSeed(seedPhrase);
|
||||
const master = bip32.fromSeed(seed);
|
||||
|
||||
const btcChild = master.derivePath(getBitcoinDerivationPath(index, network));
|
||||
return btcChild.privateKey!.toString('hex');
|
||||
}
|
||||
|
||||
export const storeWalletSeed = async (seedphrase: string, password: string) => {
|
||||
try {
|
||||
const encryptedSeed = await encryptSeedPhrase(seedphrase, password);
|
||||
storeEncryptedSeed(encryptedSeed);
|
||||
} catch (err) {
|
||||
console.log('Failed to save Seed');
|
||||
}
|
||||
};
|
||||
@@ -12,6 +12,11 @@ import SendStxScreen from '@screens/sendStx';
|
||||
import TransactionStatus from '@screens/transactionStatus';
|
||||
import SendBtcScreen from '@screens/sendBtc';
|
||||
import ConfirmBtcTransaction from '@screens/confrimBtcTransaction';
|
||||
import BackupWallet from '@screens/backupWallet';
|
||||
import CreateWalletSuccess from '@screens/createWalletSuccess';
|
||||
import CreatePassword from '@screens/createPassword';
|
||||
import AuthGuard from '@components/guards/auth';
|
||||
import Login from '@screens/login';
|
||||
|
||||
const router = createHashRouter([
|
||||
{
|
||||
@@ -19,7 +24,7 @@ const router = createHashRouter([
|
||||
element: <ScreenContainer />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
path: 'landing',
|
||||
element: <Landing />,
|
||||
},
|
||||
{
|
||||
@@ -27,8 +32,12 @@ const router = createHashRouter([
|
||||
element: <Onboarding />,
|
||||
},
|
||||
{
|
||||
path: 'home',
|
||||
element: <Home />,
|
||||
index: true,
|
||||
element: (
|
||||
<AuthGuard>
|
||||
<Home />
|
||||
</AuthGuard>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'legal',
|
||||
@@ -39,33 +48,49 @@ const router = createHashRouter([
|
||||
element: <ManageTokens />,
|
||||
},
|
||||
{
|
||||
path: 'accountList',
|
||||
element: <AccountList />,
|
||||
},
|
||||
{
|
||||
path:'receive',
|
||||
element:<Receive/>
|
||||
},
|
||||
{
|
||||
path:'send-stx',
|
||||
element:<SendStxScreen/>
|
||||
},
|
||||
{
|
||||
path:'send-btc',
|
||||
element:<SendBtcScreen/>
|
||||
},
|
||||
{
|
||||
path:'confirm-stx-tx',
|
||||
element:<ConfirmStxTransaction/>
|
||||
},
|
||||
{
|
||||
path:'confirm-btc-tx',
|
||||
element:<ConfirmBtcTransaction/>
|
||||
},
|
||||
{
|
||||
path:'tx-status',
|
||||
element:<TransactionStatus/>
|
||||
},
|
||||
path: 'accountList',
|
||||
element: <AccountList />,
|
||||
},
|
||||
{
|
||||
path: 'receive',
|
||||
element: <Receive />,
|
||||
},
|
||||
{
|
||||
path: 'send-stx',
|
||||
element: <SendStxScreen />,
|
||||
},
|
||||
{
|
||||
path: 'send-btc',
|
||||
element: <SendBtcScreen />,
|
||||
},
|
||||
{
|
||||
path: 'confirm-stx-tx',
|
||||
element: <ConfirmStxTransaction />,
|
||||
},
|
||||
{
|
||||
path: 'confirm-btc-tx',
|
||||
element: <ConfirmBtcTransaction />,
|
||||
},
|
||||
{
|
||||
path: 'tx-status',
|
||||
element: <TransactionStatus />,
|
||||
},
|
||||
{
|
||||
path: 'backup',
|
||||
element: <BackupWallet />,
|
||||
},
|
||||
{
|
||||
path: 'create-password',
|
||||
element: <CreatePassword />,
|
||||
},
|
||||
{
|
||||
path: 'create-wallet-success',
|
||||
element: <CreateWalletSuccess />,
|
||||
},
|
||||
{
|
||||
path: 'login',
|
||||
element: <Login />,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
106
src/app/screens/backupWallet/index.tsx
Normal file
106
src/app/screens/backupWallet/index.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import backup from '@assets/img/backupWallet/backup.svg';
|
||||
import styled from 'styled-components';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Container = styled.div((props) => ({
|
||||
flex: 1,
|
||||
paddingLeft: props.theme.spacing(8),
|
||||
paddingRight: props.theme.spacing(8),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}));
|
||||
|
||||
const IconContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flex: 0.6,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const ContentContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flex: 0.4,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const Title = styled.h1((props) => ({
|
||||
...props.theme.body_bold_l,
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const SubTitle = styled.h2((props) => ({
|
||||
...props.theme.body_l,
|
||||
textAlign: 'center',
|
||||
marginTop: props.theme.spacing(4),
|
||||
color: props.theme.colors.white['200'],
|
||||
}));
|
||||
|
||||
const BackupActionsContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: props.theme.spacing(20),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const BackupButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const SkipBackupButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
border: '1px solid #272A44',
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
function BackupWallet(): JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'BACKUP_WALLET_SCREEN' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleBackup = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
navigate('/create-password');
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<IconContainer>
|
||||
<img src={backup} alt="backup" width={208} />
|
||||
</IconContainer>
|
||||
<ContentContainer>
|
||||
<Title>{t('SCREEN_TITLE')}</Title>
|
||||
<SubTitle>{t('SCREEN_SUBTITLE')}</SubTitle>
|
||||
<BackupActionsContainer>
|
||||
<SkipBackupButton onClick={handleSkip}>
|
||||
{t('BACKUP_SKIP_BUTTON')}
|
||||
</SkipBackupButton>
|
||||
<BackupButton onClick={handleBackup}>
|
||||
{t('BACKUP_BUTTON')}
|
||||
</BackupButton>
|
||||
</BackupActionsContainer>
|
||||
</ContentContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default BackupWallet;
|
||||
147
src/app/screens/createPassword/confirmPassword.tsx
Normal file
147
src/app/screens/createPassword/confirmPassword.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import Eye from '@assets/img/createPassword/Eye.svg';
|
||||
import EyeSlash from '@assets/img/createPassword/EyeSlash.svg';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface ConfirmPasswordProps {
|
||||
confirmPassword: string,
|
||||
password: string,
|
||||
setConfirmPassword: (confirmPassword: string) => void,
|
||||
handleContinue: () => void,
|
||||
handleBack: () => void,
|
||||
}
|
||||
|
||||
const NewPasswordContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
marginTop: props.theme.spacing(24),
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const PasswordInputContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
border: '1px solid #303354;',
|
||||
borderRadius: props.theme.radius(1),
|
||||
paddingLeft: props.theme.spacing(4),
|
||||
paddingRight: props.theme.spacing(4),
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
const PasswordInputLabel = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const PasswordInput = styled.input((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
height: 44,
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}));
|
||||
|
||||
const ButtonsContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
alignItems: 'flex-end',
|
||||
marginBottom: props.theme.spacing(20),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const ContinueButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const BackButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
border: '1px solid #272A44',
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const ErrorMessage = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'center',
|
||||
color: props.theme.colors.feedback.error,
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
function ConfirmPassword(props: ConfirmPasswordProps): JSX.Element {
|
||||
const {
|
||||
confirmPassword,
|
||||
password,
|
||||
setConfirmPassword,
|
||||
handleContinue,
|
||||
handleBack,
|
||||
} = props;
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' });
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const handleTogglePasswordView = () => {
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
setConfirmPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const onClickContinue = () => {
|
||||
if (confirmPassword === password) {
|
||||
handleContinue();
|
||||
} else {
|
||||
setError(t('CONFIRM_PASSWORD_MATCH_ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NewPasswordContainer>
|
||||
<PasswordInputLabel>{t('TEXT_INPUT_CONFIRM_PASSWORD_LABEL')}</PasswordInputLabel>
|
||||
<PasswordInputContainer>
|
||||
<PasswordInput type={isPasswordVisible ? 'text' : 'password'} value={confirmPassword} onChange={handlePasswordChange} />
|
||||
<button type="button" onClick={handleTogglePasswordView} style={{ background: 'none' }}>
|
||||
<img src={isPasswordVisible ? Eye : EyeSlash} alt="show-password" height={24} />
|
||||
</button>
|
||||
</PasswordInputContainer>
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
{error}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
<ButtonsContainer>
|
||||
<BackButton onClick={handleBack}>
|
||||
{t('BACK_BUTTON')}
|
||||
</BackButton>
|
||||
<ContinueButton onClick={onClickContinue}>
|
||||
{t('CONTINUE_BUTTON')}
|
||||
</ContinueButton>
|
||||
</ButtonsContainer>
|
||||
</NewPasswordContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ConfirmPassword;
|
||||
112
src/app/screens/createPassword/index.tsx
Normal file
112
src/app/screens/createPassword/index.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import PasswordIcon from '@assets/img/createPassword/Password.svg';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import NewPassword from './newPassword';
|
||||
import ConfirmPassword from './confirmPassword';
|
||||
import { StoreState } from '@stores/root/reducer';
|
||||
import { storeWalletSeed } from '@core/wallet';
|
||||
|
||||
const Container = styled.div((props) => ({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: props.theme.spacing(8),
|
||||
}));
|
||||
|
||||
const StepsContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: props.theme.spacing(10),
|
||||
justifyContent: 'center',
|
||||
}));
|
||||
|
||||
const StepDot = styled.div((props) => ({
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: 4,
|
||||
backgroundColor: props.active
|
||||
? props.theme.colors.action.classic
|
||||
: props.theme.colors.background.elevation3,
|
||||
marginRight: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
const HeaderText = styled.h1((props) => ({
|
||||
...props.theme.body_bold_l,
|
||||
textAlign: 'center',
|
||||
marginTop: props.theme.spacing(15),
|
||||
}));
|
||||
|
||||
const HeaderContainer = styled.div((props) => ({
|
||||
marginTop: props.theme.spacing(32),
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
function CreatePassword(): JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' });
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [confirmPassword, setConfirmPassword] = useState<string>('');
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const { seedPhrase } = useSelector((state: StoreState) => ({
|
||||
...state.walletState,
|
||||
}));
|
||||
|
||||
const handleContinuePasswordCreation = () => {
|
||||
setCurrentStepIndex(1);
|
||||
};
|
||||
|
||||
const handleConfirmPassword = async () => {
|
||||
await storeWalletSeed(seedPhrase, password);
|
||||
navigate('/create-wallet-success');
|
||||
};
|
||||
|
||||
const handleNewPasswordBack = () => {
|
||||
navigate('/backup');
|
||||
};
|
||||
|
||||
const handleConfirmPasswordBack = () => {
|
||||
setCurrentStepIndex(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<StepsContainer>
|
||||
{Array(2)
|
||||
.fill(0)
|
||||
.map((view, index) => (
|
||||
<StepDot active={index === currentStepIndex} key={index.toString() + 1} />
|
||||
))}
|
||||
</StepsContainer>
|
||||
<HeaderContainer>
|
||||
<img src={PasswordIcon} alt="passoword" />
|
||||
<HeaderText>
|
||||
{currentStepIndex === 0 ? t('CREATE_PASSWORD_TITLE') : t('CONFIRM_PASSWORD_TITLE')}
|
||||
</HeaderText>
|
||||
</HeaderContainer>
|
||||
{currentStepIndex === 0 ? (
|
||||
<NewPassword
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
handleContinue={handleContinuePasswordCreation}
|
||||
handleBack={handleNewPasswordBack}
|
||||
/>
|
||||
) : (
|
||||
<ConfirmPassword
|
||||
password={password}
|
||||
confirmPassword={confirmPassword}
|
||||
setConfirmPassword={setConfirmPassword}
|
||||
handleContinue={handleConfirmPassword}
|
||||
handleBack={handleConfirmPasswordBack}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreatePassword;
|
||||
224
src/app/screens/createPassword/newPassword.tsx
Normal file
224
src/app/screens/createPassword/newPassword.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled, { useTheme } from 'styled-components';
|
||||
import Eye from '@assets/img/createPassword/Eye.svg';
|
||||
import EyeSlash from '@assets/img/createPassword/EyeSlash.svg';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
interface NewPasswordProps {
|
||||
password: string,
|
||||
setPassword: (password: string) => void,
|
||||
handleContinue: () => void,
|
||||
handleBack: () => void,
|
||||
}
|
||||
|
||||
const NewPasswordContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
marginTop: props.theme.spacing(24),
|
||||
flex: 1,
|
||||
}));
|
||||
|
||||
const PasswordInputContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
border: '1px solid #303354;',
|
||||
borderRadius: props.theme.radius(1),
|
||||
paddingLeft: props.theme.spacing(4),
|
||||
paddingRight: props.theme.spacing(4),
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
const PasswordInputLabel = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const PasswordInput = styled.input((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
height: 44,
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}));
|
||||
|
||||
const PasswordStrengthContainer = styled.div((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
marginTop: props.theme.spacing(8),
|
||||
justifyContent: 'space-between',
|
||||
}));
|
||||
|
||||
const StrengthBar = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
height: 4,
|
||||
backgroundColor: props.strengthColor,
|
||||
color: props.strengthColor,
|
||||
width: '50%',
|
||||
}));
|
||||
|
||||
const ButtonsContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
flex: 1,
|
||||
alignItems: 'flex-end',
|
||||
marginBottom: props.theme.spacing(20),
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
const ContinueButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const BackButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
border: '1px solid #272A44',
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '48%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const ErrorMessage = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'center',
|
||||
color: props.theme.colors.feedback.error,
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
export enum PasswordStrength {
|
||||
WEAK = 5,
|
||||
MEDIUM = 8,
|
||||
STRONG = 12,
|
||||
EMPTY = 0,
|
||||
}
|
||||
|
||||
function NewPassword(props: NewPasswordProps): JSX.Element {
|
||||
const {
|
||||
password,
|
||||
setPassword,
|
||||
handleContinue,
|
||||
handleBack,
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'CREATE_PASSWORD_SCREEN' });
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||
const [passwordStrength, setPasswordStrength] = useState<PasswordStrength>(PasswordStrength.EMPTY);
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (password !== '') {
|
||||
const passwordLength = password.length;
|
||||
if (passwordLength <= PasswordStrength.WEAK) {
|
||||
setPasswordStrength(PasswordStrength.WEAK);
|
||||
} else if (passwordLength <= PasswordStrength.MEDIUM) {
|
||||
setPasswordStrength(PasswordStrength.MEDIUM);
|
||||
} else if (passwordLength >= PasswordStrength.STRONG) {
|
||||
setPasswordStrength(PasswordStrength.MEDIUM);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
setPasswordStrength(PasswordStrength.EMPTY);
|
||||
};
|
||||
}, [password, setPasswordStrength]);
|
||||
|
||||
const handleTogglePasswordView = () => {
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
};
|
||||
|
||||
function renderStrengthBar() {
|
||||
if (password !== '') {
|
||||
if (password.length <= PasswordStrength.WEAK) {
|
||||
return (
|
||||
<PasswordStrengthContainer>
|
||||
{t('PASSWORD_STRENGTH_LABEL')}
|
||||
<StrengthBar strengthColor={theme.colors.feedback.error} />
|
||||
{t('PASSWORD_STRENGTH_WEAK')}
|
||||
</PasswordStrengthContainer>
|
||||
);
|
||||
} if (password.length <= PasswordStrength.MEDIUM) {
|
||||
return (
|
||||
<PasswordStrengthContainer>
|
||||
{t('PASSWORD_STRENGTH_LABEL')}
|
||||
<StrengthBar strengthColor={theme.colors.feedback.caution} />
|
||||
{t('PASSWORD_STRENGTH_MEDIUM')}
|
||||
</PasswordStrengthContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PasswordStrengthContainer>
|
||||
{t('PASSWORD_STRENGTH_LABEL')}
|
||||
<StrengthBar strengthColor={theme.colors.feedback.success} />
|
||||
{t('PASSWORD_STRENGTH_STRONG')}
|
||||
</PasswordStrengthContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PasswordStrengthContainer>
|
||||
{t('PASSWORD_STRENGTH_LABEL')}
|
||||
<StrengthBar strengthColor={theme.colors.background.elevation2} />
|
||||
</PasswordStrengthContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const handlePasswordChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const onClickContinue = () => {
|
||||
if (password && passwordStrength !== PasswordStrength.WEAK) {
|
||||
handleContinue();
|
||||
} else {
|
||||
setError(t('PASSWORD_STRENGTH_ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NewPasswordContainer>
|
||||
<PasswordInputLabel>{t('TEXT_INPUT_NEW_PASSWORD_LABEL')}</PasswordInputLabel>
|
||||
<PasswordInputContainer>
|
||||
<PasswordInput type={isPasswordVisible ? 'text' : 'password'} value={password} onChange={handlePasswordChange} />
|
||||
<button type="button" onClick={handleTogglePasswordView} style={{ background: 'none' }}>
|
||||
<img src={isPasswordVisible ? Eye : EyeSlash} alt="show-password" height={24} />
|
||||
</button>
|
||||
</PasswordInputContainer>
|
||||
{error && (
|
||||
<ErrorMessage>
|
||||
{error}
|
||||
</ErrorMessage>
|
||||
)}
|
||||
{password.length > 0 ? renderStrengthBar() : null}
|
||||
<ButtonsContainer>
|
||||
<BackButton onClick={handleBack}>
|
||||
{t('BACK_BUTTON')}
|
||||
</BackButton>
|
||||
<ContinueButton onClick={onClickContinue}>
|
||||
{t('CONTINUE_BUTTON')}
|
||||
</ContinueButton>
|
||||
</ButtonsContainer>
|
||||
</NewPasswordContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewPassword;
|
||||
67
src/app/screens/createWalletSuccess/index.tsx
Normal file
67
src/app/screens/createWalletSuccess/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import CheckCircle from '@assets/img/createWalletSuccess/CheckCircle.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const ContentContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: props.theme.spacing(8),
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
}));
|
||||
|
||||
const Title = styled.h1((props) => ({
|
||||
...props.theme.headline_s,
|
||||
color: props.theme.colors.white['0'],
|
||||
marginTop: props.theme.spacing(8),
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const Subtitle = styled.h2((props) => ({
|
||||
...props.theme.body_m,
|
||||
color: props.theme.colors.white['400'],
|
||||
marginTop: props.theme.spacing(8),
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const ContinueButton = styled.button((props) => ({
|
||||
...props.theme.body_m,
|
||||
color: props.theme.colors.white['0'],
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
borderRadius: props.theme.radius(1),
|
||||
marginLeft: props.theme.spacing(8),
|
||||
marginRight: props.theme.spacing(8),
|
||||
marginBottom: props.theme.spacing(30),
|
||||
height: 44,
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
function CreateWalletSuccess(): JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'WALLET_SUCCESS_SCREEN' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleOpenWallet = () => {
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContentContainer>
|
||||
<img src={CheckCircle} alt="success" />
|
||||
<Title>
|
||||
{t('SCREEN_TITLE')}
|
||||
</Title>
|
||||
<Subtitle>
|
||||
{t('SCREEN_SUBTITLE')}
|
||||
</Subtitle>
|
||||
</ContentContainer>
|
||||
<ContinueButton onClick={handleOpenWallet}>
|
||||
{t('OPEN_WALLET_BUTTON')}
|
||||
</ContinueButton>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default CreateWalletSuccess;
|
||||
@@ -2,6 +2,9 @@ import styled from 'styled-components';
|
||||
import logo from '@assets/img/full_logo_vertical.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setWalletAction } from '@stores/wallet/actions/actionCreators';
|
||||
import { newWallet } from '@core/wallet';
|
||||
|
||||
const TopSectionContainer = styled.div({
|
||||
display: 'flex',
|
||||
@@ -38,7 +41,7 @@ const CreateButton = styled.button((props) => ({
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
marginBottom: props.theme.spacing(10),
|
||||
marginBottom: props.theme.spacing(8),
|
||||
width: '100%',
|
||||
height: 44,
|
||||
}));
|
||||
@@ -64,9 +67,12 @@ const ButtonText = styled.div((props) => ({
|
||||
function Landing(): JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'LANDING_SCREEN' });
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const handlePressAction = () => {
|
||||
const handlePressAction = async () => {
|
||||
navigate('/onboarding');
|
||||
const wallet = await newWallet();
|
||||
dispatch(setWalletAction(wallet));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,89 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import LinkIcon from '@assets/img/linkIcon.svg';
|
||||
import { PRIVACY_POLICY_LINK, TERMS_LINK } from 'app/core/constants/constants';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { saveIsTermsAccepted } from '@utils/localStorage';
|
||||
|
||||
const Container = styled.div((props) => ({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
padding: props.theme.spacing(8),
|
||||
}));
|
||||
|
||||
const Title = styled.h1((props) => ({
|
||||
...props.theme.tile_text,
|
||||
color: props.theme.colors.white['0'],
|
||||
marginTop: props.theme.spacing(20),
|
||||
}));
|
||||
|
||||
const SubTitle = styled.h1((props) => ({
|
||||
...props.theme.body_l,
|
||||
color: props.theme.colors.white['200'],
|
||||
marginTop: props.theme.spacing(8),
|
||||
}));
|
||||
|
||||
const ActionButton = styled.a((props) => ({
|
||||
...props.theme.body_m,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
marginTop: props.theme.spacing(8),
|
||||
color: props.theme.colors.white['0'],
|
||||
}));
|
||||
|
||||
const ActionButtonsContainer = styled.div((props) => ({
|
||||
marginTop: props.theme.spacing(20),
|
||||
}));
|
||||
|
||||
const AcceptButtonContainer = styled.div((props) => ({
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'flex-end',
|
||||
marginBottom: props.theme.spacing(20),
|
||||
}));
|
||||
|
||||
const AcceptButton = styled.button((props) => ({
|
||||
...props.theme.body_bold_m,
|
||||
display: 'flex',
|
||||
color: props.theme.colors.white['0'],
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginTop: props.theme.spacing(8),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
borderRadius: props.theme.radius(1),
|
||||
height: 44,
|
||||
width: '100%',
|
||||
}));
|
||||
|
||||
function LegalLinks() {
|
||||
return <></>;
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'LEGAL_SCREEN' });
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLegalAccept = () => {
|
||||
saveIsTermsAccepted(true);
|
||||
navigate('/backup');
|
||||
};
|
||||
return (
|
||||
<Container>
|
||||
<Title>{t('SCREEN_TITLE')}</Title>
|
||||
<SubTitle>{t('SCREEN_SUBTITLE')}</SubTitle>
|
||||
<ActionButtonsContainer>
|
||||
<ActionButton href={TERMS_LINK} target="_blank">
|
||||
{t('TERMS_SERVICES_LINK_BUTTON')}
|
||||
<img src={LinkIcon} alt="terms" />
|
||||
</ActionButton>
|
||||
<ActionButton href={PRIVACY_POLICY_LINK} target="_blank">
|
||||
{t('PRIVACY_POLICY_LINK_BUTTON')}
|
||||
<img src={LinkIcon} alt="privacy" />
|
||||
</ActionButton>
|
||||
</ActionButtonsContainer>
|
||||
<AcceptButtonContainer>
|
||||
<AcceptButton onClick={handleLegalAccept}>{t('ACCEPT_LEGAL_BUTTON')}</AcceptButton>
|
||||
</AcceptButtonContainer>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default LegalLinks;
|
||||
|
||||
155
src/app/screens/login/index.tsx
Normal file
155
src/app/screens/login/index.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import logo from '@assets/img/full_logo_vertical.svg';
|
||||
import styled from 'styled-components';
|
||||
import Eye from '@assets/img/createPassword/Eye.svg';
|
||||
import EyeSlash from '@assets/img/createPassword/EyeSlash.svg';
|
||||
import { useState } from 'react';
|
||||
import { decryptSeedPhrase } from '@utils/encryptionUtils';
|
||||
import { getEncryptedSeed } from '@utils/localStorage';
|
||||
import { walletFromSeedPhrase } from '@core/wallet';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { setWalletAction } from '@stores/wallet/actions/actionCreators';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const ScreenContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flex: 1,
|
||||
paddingLeft: props.theme.spacing(9),
|
||||
paddingRight: props.theme.spacing(9),
|
||||
}));
|
||||
|
||||
const AppVersion = styled.p((props) => ({
|
||||
...props.theme.body_xs,
|
||||
color: props.theme.colors.white['0'],
|
||||
textAlign: 'right',
|
||||
marginTop: props.theme.spacing(8),
|
||||
}));
|
||||
|
||||
const TopSectionContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginTop: props.theme.spacing(50),
|
||||
marginBottom: props.theme.spacing(15),
|
||||
}));
|
||||
|
||||
const PasswordInputLabel = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'left',
|
||||
}));
|
||||
|
||||
const PasswordInputContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
border: '1px solid #303354;',
|
||||
paddingLeft: props.theme.spacing(8),
|
||||
paddingRight: props.theme.spacing(8),
|
||||
borderRadius: props.theme.radius(1),
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
const PasswordInput = styled.input((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
height: 44,
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '100%',
|
||||
border: 'none',
|
||||
}));
|
||||
|
||||
const LandingTitle = styled.h1((props) => ({
|
||||
...props.theme.tile_text,
|
||||
paddingTop: props.theme.spacing(15),
|
||||
paddingLeft: props.theme.spacing(34),
|
||||
paddingRight: props.theme.spacing(34),
|
||||
color: props.theme.colors.white['200'],
|
||||
textAlign: 'center',
|
||||
}));
|
||||
|
||||
const VerifyButton = styled.button((props) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
color: props.theme.colors.white['0'],
|
||||
marginTop: props.theme.spacing(8),
|
||||
width: '100%',
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
const ErrorMessage = styled.h2((props) => ({
|
||||
...props.theme.body_medium_m,
|
||||
textAlign: 'left',
|
||||
color: props.theme.colors.feedback.error,
|
||||
marginTop: props.theme.spacing(4),
|
||||
}));
|
||||
|
||||
function Login(): JSX.Element {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'LOGIN_SCREEN' });
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState<boolean>(false);
|
||||
const [password, setPassword] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [isVerifying, setIsVerifying] = useState(false);
|
||||
|
||||
const handleTogglePasswordView = () => {
|
||||
setIsPasswordVisible(!isPasswordVisible);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
if (error) {
|
||||
setError('');
|
||||
}
|
||||
setPassword(event.currentTarget.value);
|
||||
};
|
||||
|
||||
const handleVerifyPassword = async () => {
|
||||
setIsVerifying(true);
|
||||
const seed = getEncryptedSeed();
|
||||
try {
|
||||
if (seed) {
|
||||
const decrypted = await decryptSeedPhrase(seed, password);
|
||||
const wallet = await walletFromSeedPhrase(decrypted);
|
||||
dispatch(setWalletAction(wallet));
|
||||
setIsVerifying(false);
|
||||
navigate('/');
|
||||
}
|
||||
} catch (err) {
|
||||
setIsVerifying(false);
|
||||
setError(t('VERIFY_PASSWORD_ERROR'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenContainer>
|
||||
<AppVersion>V 1.0.0</AppVersion>
|
||||
<TopSectionContainer>
|
||||
<img src={logo} width={100} alt="logo" />
|
||||
<LandingTitle>{t('WELCOME_MESSAGE_FIRST_LOGIN')}</LandingTitle>
|
||||
</TopSectionContainer>
|
||||
<PasswordInputLabel>{t('PASSWORD_INPUT_LABEL')}</PasswordInputLabel>
|
||||
<PasswordInputContainer>
|
||||
<PasswordInput
|
||||
type={isPasswordVisible ? 'text' : 'password'}
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
placeholder={t('PASSWORD_INPUT_PLACEHOLDER')}
|
||||
/>
|
||||
<button type="button" onClick={handleTogglePasswordView} style={{ background: 'none' }}>
|
||||
<img src={isPasswordVisible ? Eye : EyeSlash} alt="show-password" height={24} />
|
||||
</button>
|
||||
</PasswordInputContainer>
|
||||
{error && <ErrorMessage>{error}</ErrorMessage>}
|
||||
<VerifyButton onClick={handleVerifyPassword}>
|
||||
{t('VERIFY_PASSWORD_BUTTON')}
|
||||
</VerifyButton>
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
export default Login;
|
||||
@@ -5,6 +5,7 @@ import onboarding3 from '@assets/img/onboarding/onboarding3.svg';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import styled from 'styled-components';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { getIsTermsAccepted, saveHasFinishedOnboarding } from '@utils/localStorage';
|
||||
|
||||
const OnBoardingDotsContainer = styled.div((props) => ({
|
||||
display: 'flex',
|
||||
@@ -41,7 +42,7 @@ const OnboardingTitle = styled.h1((props) => ({
|
||||
const OnboardingSubTitle = styled.h1((props) => ({
|
||||
...props.theme.body_l,
|
||||
textAlign: 'center',
|
||||
marginTop: props.theme.spacing(10),
|
||||
marginTop: props.theme.spacing(8),
|
||||
color: props.theme.colors.white['200'],
|
||||
}));
|
||||
const OnBoardingActionsContainer = styled.div((props) => ({
|
||||
@@ -62,7 +63,7 @@ const OnBoardingNextButton = styled.button((props) => ({
|
||||
backgroundColor: props.theme.colors.action.classic,
|
||||
marginLeft: props.theme.spacing(8),
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '45%',
|
||||
flex: 1,
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
@@ -75,9 +76,8 @@ const OnBoardingSkipButton = styled.button((props) => (
|
||||
borderRadius: props.theme.radius(1),
|
||||
backgroundColor: props.theme.colors.background.elevation0,
|
||||
border: '1px solid #272A44',
|
||||
marginLeft: props.theme.spacing(8),
|
||||
color: props.theme.colors.white['0'],
|
||||
width: '45%',
|
||||
flex: 1,
|
||||
height: 44,
|
||||
}));
|
||||
|
||||
@@ -110,28 +110,36 @@ function Onboarding(): JSX.Element {
|
||||
{
|
||||
image: onboarding2,
|
||||
imageWidth: 163,
|
||||
title: t('ONBOARDING_1_TITlE'),
|
||||
title: t('ONBOARDING_2_TITlE'),
|
||||
subtitle: t('ONBOARDING_2_SUBTITlE'),
|
||||
},
|
||||
{
|
||||
image: onboarding3,
|
||||
imageWidth: 192,
|
||||
title: t('ONBOARDING_1_TITlE'),
|
||||
title: t('ONBOARDING_3_TITlE'),
|
||||
subtitle: t('ONBOARDING_3_SUBTITlE'),
|
||||
},
|
||||
];
|
||||
|
||||
const handleClickNext = () => {
|
||||
setCurrentStepIndex(currentStepIndex + 1);
|
||||
};
|
||||
|
||||
const handleSkip = () => {
|
||||
navigate('/home');
|
||||
saveHasFinishedOnboarding(true);
|
||||
const isLegalAccepted = getIsTermsAccepted();
|
||||
if (isLegalAccepted) {
|
||||
navigate('/backup');
|
||||
} else {
|
||||
navigate('/legal');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<OnBoardingDotsContainer>
|
||||
{onboardingViews.map((view, index) => (
|
||||
<OnboardingDot active={index === currentStepIndex} />
|
||||
<OnboardingDot active={index === currentStepIndex} key={view.title} />
|
||||
))}
|
||||
</OnBoardingDotsContainer>
|
||||
<OnBoardingImage
|
||||
@@ -144,11 +152,11 @@ function Onboarding(): JSX.Element {
|
||||
<OnboardingSubTitle>{onboardingViews[currentStepIndex].subtitle}</OnboardingSubTitle>
|
||||
</OnBoardingContentContainer>
|
||||
{currentStepIndex === onboardingViews.length - 1 ? (
|
||||
<OnBoardingContinueButton onClick={handleSkip}>Continue</OnBoardingContinueButton>
|
||||
<OnBoardingContinueButton onClick={handleSkip}>{t('ONBOARDING_CONTINUE_BUTTON')}</OnBoardingContinueButton>
|
||||
) : (
|
||||
<OnBoardingActionsContainer>
|
||||
<OnBoardingSkipButton onClick={handleSkip}>Skip</OnBoardingSkipButton>
|
||||
<OnBoardingNextButton onClick={handleClickNext}>Next</OnBoardingNextButton>
|
||||
<OnBoardingSkipButton onClick={handleSkip}>{t('ONBOARDING_SKIP_BUTTON')}</OnBoardingSkipButton>
|
||||
<OnBoardingNextButton onClick={handleClickNext}>{t('ONBOARDING_NEXT_BUTTON')}</OnBoardingNextButton>
|
||||
</OnBoardingActionsContainer>
|
||||
)}
|
||||
</>
|
||||
|
||||
11
src/app/stores/index.ts
Normal file
11
src/app/stores/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import rootReducer from './root/reducer';
|
||||
|
||||
const configureStore = () => {
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware));
|
||||
return store;
|
||||
};
|
||||
|
||||
export default configureStore;
|
||||
11
src/app/stores/root/reducer.ts
Normal file
11
src/app/stores/root/reducer.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import walletReducer from '../wallet/reducers/walletReducer';
|
||||
|
||||
const appReducer = combineReducers({
|
||||
walletState: walletReducer,
|
||||
});
|
||||
|
||||
const rootReducer = (state: any, action: any) => appReducer(state, action);
|
||||
|
||||
export type StoreState = ReturnType<typeof rootReducer>;
|
||||
export default rootReducer;
|
||||
16
src/app/stores/wallet/actions/actionCreators.ts
Normal file
16
src/app/stores/wallet/actions/actionCreators.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import * as actions from './types';
|
||||
|
||||
export function setWalletAction(
|
||||
wallet: actions.WalletData,
|
||||
): actions.SetWallet {
|
||||
return {
|
||||
type: actions.SetWalletKey,
|
||||
wallet,
|
||||
};
|
||||
}
|
||||
|
||||
export function ResetWalletAction(): actions.ResetWallet {
|
||||
return {
|
||||
type: actions.ResetWalletKey,
|
||||
};
|
||||
}
|
||||
22
src/app/stores/wallet/actions/types.ts
Normal file
22
src/app/stores/wallet/actions/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const SetWalletKey = 'SetWallet';
|
||||
export const ResetWalletKey = 'ResetWallet';
|
||||
|
||||
export interface WalletData {
|
||||
stxAddress: string;
|
||||
btcAddress: string;
|
||||
masterPubKey: string;
|
||||
stxPublicKey: string;
|
||||
btcPublicKey: string;
|
||||
seedPhrase: string;
|
||||
}
|
||||
|
||||
export interface SetWallet {
|
||||
type: typeof SetWalletKey;
|
||||
wallet: WalletData;
|
||||
}
|
||||
|
||||
export interface ResetWallet {
|
||||
type: typeof ResetWalletKey;
|
||||
}
|
||||
|
||||
export type WalletActions = SetWallet | ResetWallet;
|
||||
52
src/app/stores/wallet/reducers/walletReducer.ts
Normal file
52
src/app/stores/wallet/reducers/walletReducer.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Network } from 'app/core/types/networks';
|
||||
import { Account } from 'app/core/types/accounts';
|
||||
import { WalletActions, SetWalletKey, ResetWalletKey } from '../actions/types';
|
||||
|
||||
interface WalletState {
|
||||
stxAddress: string;
|
||||
btcAddress: string;
|
||||
masterPubKey: string;
|
||||
stxPublicKey: string;
|
||||
btcPublicKey: string;
|
||||
accountsList: Account[];
|
||||
selectedAccount: Account | null;
|
||||
network: Network;
|
||||
seedPhrase: string;
|
||||
isLocked: boolean;
|
||||
}
|
||||
|
||||
const initialWalletState: WalletState = {
|
||||
stxAddress: '',
|
||||
btcAddress: '',
|
||||
masterPubKey: '',
|
||||
stxPublicKey: '',
|
||||
btcPublicKey: '',
|
||||
network: 'Mainnet',
|
||||
accountsList: [],
|
||||
selectedAccount: null,
|
||||
seedPhrase: '',
|
||||
isLocked: true,
|
||||
};
|
||||
|
||||
const walletReducer = (
|
||||
// eslint-disable-next-line @typescript-eslint/default-param-last
|
||||
state: WalletState = initialWalletState,
|
||||
action: WalletActions,
|
||||
) => {
|
||||
switch (action.type) {
|
||||
case SetWalletKey:
|
||||
return {
|
||||
...state,
|
||||
...action.wallet,
|
||||
isLocked: false,
|
||||
};
|
||||
case ResetWalletKey:
|
||||
return {
|
||||
...state,
|
||||
...initialWalletState,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
export default walletReducer;
|
||||
47
src/app/utils/encryptionUtils.ts
Normal file
47
src/app/utils/encryptionUtils.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import argon2 from 'argon2-browser';
|
||||
import { encryptMnemonic, decryptMnemonic } from '@stacks/encryption';
|
||||
import { getSalt, saveSalt } from './localStorage';
|
||||
|
||||
function generateRandomKey(bytesCount: number): string {
|
||||
const randomValues = Array.from(crypto.getRandomValues(new Uint8Array(bytesCount)));
|
||||
return randomValues.map((val) => `00${val.toString(16)}`.slice(-2)).join('');
|
||||
}
|
||||
|
||||
async function generateKeyArgon2(password: string, salt: string): Promise<string> {
|
||||
try {
|
||||
const result = await argon2.hash({
|
||||
pass: password,
|
||||
salt,
|
||||
time: 3,
|
||||
mem: 64 * 1024,
|
||||
parallelism: 4,
|
||||
hashLen: 48,
|
||||
type: argon2.ArgonType.Argon2i,
|
||||
});
|
||||
return result.hashHex;
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function encryptSeedPhrase(seed: string, password: string): Promise<string> {
|
||||
const salt = generateRandomKey(16);
|
||||
saveSalt(salt);
|
||||
const argonHash = await generateKeyArgon2(password, salt);
|
||||
const encryptedBuffer = await encryptMnemonic(seed, argonHash);
|
||||
return encryptedBuffer.toString('hex');
|
||||
}
|
||||
|
||||
export async function decryptSeedPhrase(encryptedSeed: string, password: string): Promise<string> {
|
||||
const salt = getSalt();
|
||||
try {
|
||||
if (salt) {
|
||||
const pw = await generateKeyArgon2(password, salt);
|
||||
const secretKey = await decryptMnemonic(Buffer.from(encryptedSeed, 'hex'), pw);
|
||||
return secretKey;
|
||||
}
|
||||
return await Promise.reject(Error('Invalid Password'));
|
||||
} catch (err) {
|
||||
return Promise.reject(Error('Invalid Password'));
|
||||
}
|
||||
}
|
||||
92
src/app/utils/localStorage.ts
Normal file
92
src/app/utils/localStorage.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { SettingsNetwork } from 'app/core/constants/constants';
|
||||
import { Account } from 'app/core/types/accounts';
|
||||
|
||||
const userPrefBackupRemindKey = 'UserPref:BackupRemind';
|
||||
const selectedNetworkKey = 'selectedNetwork';
|
||||
const accountsListKey = 'AccountsList';
|
||||
const selectedAccountKey = 'SelectedAccount';
|
||||
const isTermsAccepted = 'isTermsAccepted';
|
||||
const hasFinishedOnboardingKey = 'hasFinishedOnboarding';
|
||||
const saltKey = 'salt';
|
||||
const encryptedSeedKey = 'encSeed';
|
||||
|
||||
export function saveMultiple(items: { [x: string]: string }) {
|
||||
const itemKeys = Object.keys(items);
|
||||
itemKeys.forEach((key) => {
|
||||
localStorage.setItem(key, items[key]);
|
||||
});
|
||||
}
|
||||
|
||||
export function saveHasFinishedOnboarding(finished: boolean) {
|
||||
localStorage.setItem(hasFinishedOnboardingKey, finished.toString());
|
||||
}
|
||||
|
||||
export function getHasFinishedOnboarding(): boolean {
|
||||
const accepted = localStorage.getItem(hasFinishedOnboardingKey);
|
||||
if (accepted !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function saveIsTermsAccepted(termsDisplayed: boolean) {
|
||||
localStorage.setItem(isTermsAccepted, termsDisplayed.toString());
|
||||
}
|
||||
|
||||
export function getIsTermsAccepted(): boolean {
|
||||
const accepted = localStorage.getItem(isTermsAccepted);
|
||||
if (accepted !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function saveUserPrefBackupRemind(nextBackupRemind: string) {
|
||||
localStorage.setItem(userPrefBackupRemindKey, nextBackupRemind);
|
||||
}
|
||||
|
||||
export function getUserPrefBackupRemind(): string | null {
|
||||
return localStorage.getItem(userPrefBackupRemindKey);
|
||||
}
|
||||
|
||||
export function saveSelectedNetwork(network: SettingsNetwork) {
|
||||
localStorage.setItem(selectedNetworkKey, JSON.stringify(network));
|
||||
}
|
||||
|
||||
export function getSelectedNetwork(): SettingsNetwork {
|
||||
const mainnetNetwork: SettingsNetwork = {
|
||||
name: 'Mainnet',
|
||||
address: 'https://stacks-node-api.mainnet.stacks.co',
|
||||
};
|
||||
const network = localStorage.getItem(selectedNetworkKey);
|
||||
if (!network) {
|
||||
return mainnetNetwork;
|
||||
}
|
||||
const selected: SettingsNetwork = JSON.parse(network);
|
||||
return selected;
|
||||
}
|
||||
|
||||
export function saveAccountsList(accounts: Account[]) {
|
||||
localStorage.setItem(accountsListKey, JSON.stringify(accounts));
|
||||
}
|
||||
|
||||
export function saveSelectedAccount(account: Account) {
|
||||
const jsonAccount = JSON.stringify(account);
|
||||
return localStorage.setItem(selectedAccountKey, jsonAccount);
|
||||
}
|
||||
|
||||
export function saveSalt(salt: string) {
|
||||
localStorage.setItem(saltKey, salt);
|
||||
}
|
||||
|
||||
export function getSalt() {
|
||||
return localStorage.getItem(saltKey);
|
||||
}
|
||||
|
||||
export function storeEncryptedSeed(seed: string) {
|
||||
localStorage.setItem(encryptedSeedKey, seed);
|
||||
}
|
||||
|
||||
export function getEncryptedSeed() {
|
||||
return localStorage.getItem(encryptedSeedKey);
|
||||
}
|
||||
85
src/assets/img/backupWallet/backup.svg
Normal file
85
src/assets/img/backupWallet/backup.svg
Normal file
@@ -0,0 +1,85 @@
|
||||
<svg width="208" height="199" viewBox="0 0 208 199" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M90.5399 185.782C90.5399 189.896 87.2042 193.232 83.0895 193.232C78.9748 193.232 75.6391 189.896 75.6391 185.782C75.6391 181.667 78.9748 178.331 83.0895 178.331C87.2042 178.331 90.5399 181.667 90.5399 185.782ZM93.8387 186.983C93.2412 192.391 88.6566 196.597 83.0895 196.597C77.1165 196.597 72.2744 191.755 72.2744 185.782C72.2744 179.809 77.1165 174.967 83.0895 174.967C88.3216 174.967 92.6859 178.682 93.6882 183.618L147.58 183.618V186.983H143.734L143.734 199.001L140.369 199.001V186.983H137.967V199.001H134.603V186.983L93.8387 186.983Z" fill="url(#paint0_linear_364_37529)"/>
|
||||
<circle cx="166.007" cy="69.0957" r="0.902241" transform="rotate(-90 166.007 69.0957)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="69.0957" r="0.902241" transform="rotate(-90 193.284 69.0957)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="55.457" r="0.902241" transform="rotate(-90 166.007 55.457)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="55.457" r="0.902241" transform="rotate(-90 193.284 55.457)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="41.8183" r="0.902241" transform="rotate(-90 166.007 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="41.8183" r="0.902241" transform="rotate(-90 193.284 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="28.1806" r="0.902241" transform="rotate(-90 166.007 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="28.1806" r="0.902241" transform="rotate(-90 193.284 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="41.8183" r="0.902241" transform="rotate(-90 166.007 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="41.8183" r="0.902241" transform="rotate(-90 193.284 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="14.542" r="0.902241" transform="rotate(-90 166.007 14.542)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="14.542" r="0.902241" transform="rotate(-90 193.284 14.542)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="28.1806" r="0.902241" transform="rotate(-90 166.007 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="28.1806" r="0.902241" transform="rotate(-90 193.284 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="166.007" cy="0.902203" r="0.902241" transform="rotate(-90 166.007 0.902203)" fill="#4C5187"/>
|
||||
<circle cx="193.284" cy="0.902203" r="0.902241" transform="rotate(-90 193.284 0.902203)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="69.0957" r="0.902241" transform="rotate(-90 179.644 69.0957)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="69.0957" r="0.902241" transform="rotate(-90 206.921 69.0957)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="55.457" r="0.902241" transform="rotate(-90 179.644 55.457)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="55.457" r="0.902241" transform="rotate(-90 206.921 55.457)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="41.8183" r="0.902241" transform="rotate(-90 179.644 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="41.8183" r="0.902241" transform="rotate(-90 206.921 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="28.1806" r="0.902241" transform="rotate(-90 179.644 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="28.1806" r="0.902241" transform="rotate(-90 206.921 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="41.8183" r="0.902241" transform="rotate(-90 179.644 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="41.8183" r="0.902241" transform="rotate(-90 206.921 41.8183)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="14.542" r="0.902241" transform="rotate(-90 179.644 14.542)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="14.542" r="0.902241" transform="rotate(-90 206.921 14.542)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="28.1806" r="0.902241" transform="rotate(-90 179.644 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="28.1806" r="0.902241" transform="rotate(-90 206.921 28.1806)" fill="#4C5187"/>
|
||||
<circle cx="179.644" cy="0.902203" r="0.902241" transform="rotate(-90 179.644 0.902203)" fill="#4C5187"/>
|
||||
<circle cx="206.921" cy="0.902203" r="0.902241" transform="rotate(-90 206.921 0.902203)" fill="#4C5187"/>
|
||||
<circle cx="69.2687" cy="166.021" r="0.902241" transform="rotate(180 69.2687 166.021)" fill="#4C5187"/>
|
||||
<circle cx="69.2687" cy="138.744" r="0.902241" transform="rotate(180 69.2687 138.744)" fill="#4C5187"/>
|
||||
<circle cx="55.6305" cy="166.021" r="0.902241" transform="rotate(180 55.6305 166.021)" fill="#4C5187"/>
|
||||
<circle cx="55.6305" cy="138.744" r="0.902241" transform="rotate(180 55.6305 138.744)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="166.021" r="0.902241" transform="rotate(180 41.9913 166.021)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="138.744" r="0.902241" transform="rotate(180 41.9913 138.744)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="166.021" r="0.902241" transform="rotate(180 28.3541 166.021)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="138.744" r="0.902241" transform="rotate(180 28.3541 138.744)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="166.021" r="0.902241" transform="rotate(180 41.9913 166.021)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="138.744" r="0.902241" transform="rotate(180 41.9913 138.744)" fill="#4C5187"/>
|
||||
<circle cx="14.7149" cy="166.021" r="0.902241" transform="rotate(180 14.7149 166.021)" fill="#4C5187"/>
|
||||
<circle cx="14.7149" cy="138.744" r="0.902241" transform="rotate(180 14.7149 138.744)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="166.021" r="0.902241" transform="rotate(180 28.3541 166.021)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="138.744" r="0.902241" transform="rotate(180 28.3541 138.744)" fill="#4C5187"/>
|
||||
<circle cx="1.07481" cy="166.021" r="0.902241" transform="rotate(180 1.07481 166.021)" fill="#4C5187"/>
|
||||
<circle cx="1.07481" cy="138.744" r="0.902241" transform="rotate(180 1.07481 138.744)" fill="#4C5187"/>
|
||||
<circle cx="69.2687" cy="152.384" r="0.902241" transform="rotate(180 69.2687 152.384)" fill="#4C5187"/>
|
||||
<circle cx="69.2687" cy="125.106" r="0.902241" transform="rotate(180 69.2687 125.106)" fill="#4C5187"/>
|
||||
<circle cx="55.6305" cy="152.384" r="0.902241" transform="rotate(180 55.6305 152.384)" fill="#4C5187"/>
|
||||
<circle cx="55.6305" cy="125.106" r="0.902241" transform="rotate(180 55.6305 125.106)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="152.384" r="0.902241" transform="rotate(180 41.9913 152.384)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="125.106" r="0.902241" transform="rotate(180 41.9913 125.106)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="152.384" r="0.902241" transform="rotate(180 28.3541 152.384)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="125.106" r="0.902241" transform="rotate(180 28.3541 125.106)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="152.384" r="0.902241" transform="rotate(180 41.9913 152.384)" fill="#4C5187"/>
|
||||
<circle cx="41.9913" cy="125.106" r="0.902241" transform="rotate(180 41.9913 125.106)" fill="#4C5187"/>
|
||||
<circle cx="14.7149" cy="152.384" r="0.902241" transform="rotate(180 14.7149 152.384)" fill="#4C5187"/>
|
||||
<circle cx="14.7149" cy="125.106" r="0.902241" transform="rotate(180 14.7149 125.106)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="152.384" r="0.902241" transform="rotate(180 28.3541 152.384)" fill="#4C5187"/>
|
||||
<circle cx="28.3541" cy="125.106" r="0.902241" transform="rotate(180 28.3541 125.106)" fill="#4C5187"/>
|
||||
<circle cx="1.07481" cy="152.384" r="0.902241" transform="rotate(180 1.07481 152.384)" fill="#4C5187"/>
|
||||
<circle cx="1.07481" cy="125.106" r="0.902241" transform="rotate(180 1.07481 125.106)" fill="#4C5187"/>
|
||||
<path d="M50.1631 27.8797C50.1631 23.6321 53.6064 20.1888 57.8539 20.1888H163.603C167.85 20.1888 171.294 23.6321 171.294 27.8797V133.912C171.294 138.16 167.85 141.603 163.603 141.603H57.8539C53.6064 141.603 50.1631 138.16 50.1631 133.912V27.8797Z" fill="url(#paint1_linear_364_37529)"/>
|
||||
<path d="M56.0718 50.9004C56.0718 49.6586 57.0784 48.652 58.3202 48.652H60.5686V63.2667H58.3202C57.0784 63.2667 56.0718 62.26 56.0718 61.0183V50.9004Z" fill="#12151E"/>
|
||||
<path d="M56.0718 92.8685C56.0718 91.6268 57.0784 90.6201 58.3202 90.6201H60.5686V105.235H58.3202C57.0784 105.235 56.0718 104.228 56.0718 102.986V92.8685Z" fill="#12151E"/>
|
||||
<path d="M60.5454 38.4545C60.5454 34.6317 63.6444 31.5328 67.4672 31.5328H153.989C157.812 31.5328 160.911 34.6317 160.911 38.4545V115.363C160.911 119.185 157.812 122.284 153.989 122.284H67.4672C63.6444 122.284 60.5454 119.185 60.5454 115.363V38.4545Z" stroke="#12151E" stroke-width="1.53816"/>
|
||||
<path d="M108.208 97.2154C117.754 97.2154 125.492 89.4767 125.492 79.9307C125.492 70.3846 117.754 62.646 108.208 62.646C98.6615 62.646 90.9229 70.3846 90.9229 79.9307C90.9229 89.4767 98.6615 97.2154 108.208 97.2154Z" fill="#12151E" fill-opacity="0.5"/>
|
||||
<path d="M111.719 93.9831C121.265 93.9831 129.003 86.2444 129.003 76.6984C129.003 67.1523 121.265 59.4137 111.719 59.4137C102.173 59.4137 94.4341 67.1523 94.4341 76.6984C94.4341 86.2444 102.173 93.9831 111.719 93.9831Z" fill="white" fill-opacity="0.6"/>
|
||||
<path d="M113.76 77.399C114.592 76.9456 115.249 76.2281 115.628 75.3596C116.006 74.4912 116.085 73.5215 115.851 72.6035C115.617 71.6854 115.084 70.8715 114.336 70.2902C113.588 69.7089 112.668 69.3933 111.72 69.3933C110.773 69.3933 109.853 69.7089 109.105 70.2902C108.357 70.8715 107.823 71.6854 107.59 72.6035C107.356 73.5215 107.434 74.4912 107.813 75.3596C108.191 76.2281 108.848 76.9456 109.68 77.399L107.579 82.3011C107.499 82.4861 107.465 82.6882 107.482 82.8894C107.499 83.0905 107.565 83.2843 107.675 83.4534C107.785 83.6224 107.936 83.7615 108.113 83.858C108.29 83.9545 108.489 84.0054 108.691 84.0062H114.75C114.952 84.0054 115.15 83.9545 115.327 83.858C115.505 83.7615 115.655 83.6224 115.765 83.4534C115.876 83.2843 115.942 83.0905 115.959 82.8894C115.975 82.6882 115.942 82.4861 115.861 82.3011L113.76 77.399Z" fill="#12151E"/>
|
||||
<path d="M50.1631 134.59H171.294C171.294 139.368 167.42 143.242 162.641 143.242H58.8153C54.0368 143.242 50.1631 139.368 50.1631 134.59Z" fill="#647094"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_364_37529" x1="72.2744" y1="186.984" x2="147.58" y2="186.984" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#F2A900"/>
|
||||
<stop offset="1" stop-color="#F9D277"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_364_37529" x1="171.294" y1="20.1888" x2="60.7741" y2="146.621" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0.0001" stop-color="#A2ACC8"/>
|
||||
<stop offset="1" stop-color="#7B87AD"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
4
src/assets/img/createPassword/Eye.svg
Normal file
4
src/assets/img/createPassword/Eye.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5.24927C4.5 5.24927 1.5 12 1.5 12C1.5 12 4.5 18.7493 12 18.7493C19.5 18.7493 22.5 12 22.5 12C22.5 12 19.5 5.24927 12 5.24927Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12 15.75C14.0711 15.75 15.75 14.0711 15.75 12C15.75 9.92893 14.0711 8.25 12 8.25C9.92893 8.25 8.25 9.92893 8.25 12C8.25 14.0711 9.92893 15.75 12 15.75Z" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 572 B |
7
src/assets/img/createPassword/EyeSlash.svg
Normal file
7
src/assets/img/createPassword/EyeSlash.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.5 3.75L19.5 20.25" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.5223 14.7749C13.7864 15.4438 12.8149 15.793 11.8215 15.7457C10.8281 15.6983 9.89422 15.2583 9.22524 14.5225C8.55626 13.7866 8.20698 12.8151 8.25424 11.8217C8.3015 10.8283 8.74142 9.89439 9.47724 9.22534" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.93698 6.43066C3.11486 8.36618 1.5 12 1.5 12C1.5 12 4.5 18.7493 12 18.7493C13.7572 18.7633 15.4926 18.3585 17.0623 17.5685" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.5576 15.8531C21.6015 14.0224 22.5005 12 22.5005 12C22.5005 12 19.5005 5.24928 12.0005 5.24928C11.3509 5.24822 10.7024 5.30103 10.0615 5.40717" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M12.7061 8.31641C13.5031 8.46945 14.229 8.8768 14.775 9.47734C15.3209 10.0779 15.6574 10.8392 15.734 11.6472" stroke="white" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
13
src/assets/img/createPassword/Password.svg
Normal file
13
src/assets/img/createPassword/Password.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.25098 8.74976V31.2498" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M16.25 14.9996V19.9996" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.499 18.4529L16.249 19.9998" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M13.3125 24.0472L16.25 20.0004" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.1875 24.0472L16.25 20.0004" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 18.4529L16.25 19.9998" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M31.25 14.9996V19.9996" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M26.501 18.4529L31.251 19.9998" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M28.3115 24.0472L31.249 20.0004" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M34.1875 24.0472L31.25 20.0004" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M36 18.4529L31.25 19.9998" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
4
src/assets/img/createWalletSuccess/CheckCircle.svg
Normal file
4
src/assets/img/createWalletSuccess/CheckCircle.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="88" height="88" viewBox="0 0 88 88" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M59.125 35.75L38.9582 55L28.875 45.375" stroke="#51D6A6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M44 77C62.2254 77 77 62.2254 77 44C77 25.7746 62.2254 11 44 11C25.7746 11 11 25.7746 11 44C11 62.2254 25.7746 77 44 77Z" stroke="#51D6A6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 448 B |
5
src/assets/img/linkIcon.svg
Normal file
5
src/assets/img/linkIcon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.875 7.8125L16.8744 3.12562L12.1875 3.125" stroke="white" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.2476 8.7522L16.8726 3.1272" stroke="white" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M14.375 11.25V16.25C14.375 16.4158 14.3092 16.5747 14.1919 16.6919C14.0747 16.8092 13.9158 16.875 13.75 16.875H3.75C3.58424 16.875 3.42527 16.8092 3.30806 16.6919C3.19085 16.5747 3.125 16.4158 3.125 16.25V6.25C3.125 6.08424 3.19085 5.92527 3.30806 5.80806C3.42527 5.69085 3.58424 5.625 3.75 5.625H8.75" stroke="white" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
@@ -10,7 +10,10 @@
|
||||
"ONBOARDING_2_TITlE": "Stack your STX to earn rewards in Bitcoin",
|
||||
"ONBOARDING_2_SUBTITlE": "Earn Bitcoin yield on your STX without them ever leaving your wallet.",
|
||||
"ONBOARDING_3_TITlE": "Access your favorites Dapps from your wallet",
|
||||
"ONBOARDING_3_SUBTITlE": "From marketplaces to DeFi, and so much more."
|
||||
"ONBOARDING_3_SUBTITlE": "From marketplaces to DeFi, and so much more.",
|
||||
"ONBOARDING_NEXT_BUTTON": "Next",
|
||||
"ONBOARDING_SKIP_BUTTON": "Skip",
|
||||
"ONBOARDING_CONTINUE_BUTTON": "Continue"
|
||||
},
|
||||
"DASHBOARD_SCREEN": {
|
||||
"TOTAL_BALANCE": "total balance",
|
||||
@@ -91,5 +94,45 @@
|
||||
"CLOSE":"Close",
|
||||
"FAILED":"Transaction Failed",
|
||||
"RETRY":"Try again"
|
||||
},
|
||||
"LEGAL_SCREEN": {
|
||||
"SCREEN_TITLE": "Legal",
|
||||
"SCREEN_SUBTITLE": "Please review the Xverse Wallet Privacy Policy and Terms of Services.",
|
||||
"TERMS_SERVICES_LINK_BUTTON": "Terms of Service",
|
||||
"PRIVACY_POLICY_LINK_BUTTON": "Privacy Policy",
|
||||
"ACCEPT_LEGAL_BUTTON": "Accept"
|
||||
},
|
||||
"BACKUP_WALLET_SCREEN": {
|
||||
"SCREEN_TITLE": "Backup your wallet",
|
||||
"SCREEN_SUBTITLE": "Your seedphrase is the unique key that allows you to recover your wallet.",
|
||||
"BACKUP_BUTTON": "Backup now",
|
||||
"BACKUP_SKIP_BUTTON": "Backup later"
|
||||
},
|
||||
"CREATE_PASSWORD_SCREEN": {
|
||||
"CREATE_PASSWORD_TITLE": "Enter your new password",
|
||||
"CONFIRM_PASSWORD_TITLE": "Confirm your new password",
|
||||
"TEXT_INPUT_NEW_PASSWORD_LABEL": "New Password",
|
||||
"TEXT_INPUT_CONFIRM_PASSWORD_LABEL": "Confirm Password",
|
||||
"CONTINUE_BUTTON": "Continue",
|
||||
"BACK_BUTTON": "Back",
|
||||
"PASSWORD_STRENGTH_LABEL": "Security level",
|
||||
"PASSWORD_STRENGTH_WEAK": "Weak",
|
||||
"PASSWORD_STRENGTH_MEDIUM": "Medium",
|
||||
"PASSWORD_STRENGTH_STRONG": "Strong",
|
||||
"PASSWORD_STRENGTH_ERROR": "You need to enter a stronger password",
|
||||
"CONFIRM_PASSWORD_MATCH_ERROR": "please make sure your passwords match"
|
||||
},
|
||||
"WALLET_SUCCESS_SCREEN": {
|
||||
"SCREEN_TITLE": "Wallet created successfully",
|
||||
"SCREEN_SUBTITLE": "Your new wallet has been created successfully, you can now connect to your wallet.",
|
||||
"OPEN_WALLET_BUTTON": "Open Wallet"
|
||||
},
|
||||
"LOGIN_SCREEN": {
|
||||
"WELCOME_MESSAGE_FIRST_LOGIN": "Welcome!",
|
||||
"WELCOME_MESSAGE": "Welcome back!",
|
||||
"PASSWORD_INPUT_LABEL": "Password",
|
||||
"PASSWORD_INPUT_PLACEHOLDER": "Enter your password",
|
||||
"VERIFY_PASSWORD_BUTTON": "Verify",
|
||||
"VERIFY_PASSWORD_ERROR": "Incorrect Password"
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,19 @@
|
||||
{
|
||||
"name": "Xverse Wallet",
|
||||
"description": "A Bitcoin Wallet for Web3",
|
||||
"manifest_version": 3,
|
||||
"manifest_version": 2,
|
||||
"options_page": "options.html",
|
||||
"action": {
|
||||
"browser_action": {
|
||||
"default_popup": "popup.html",
|
||||
"default_icon": "assets/img/xverse_icon.png",
|
||||
"default_title": "Xverse Wallet"
|
||||
},
|
||||
"chrome_url_overrides": {},
|
||||
"content_security_policy": "script-src 'self' 'wasm-eval'; object-src 'self'; frame-src 'none'; frame-ancestors 'none';",
|
||||
"background": {},
|
||||
"icons": {
|
||||
"128": "assets/img/xverse_icon.png"
|
||||
},
|
||||
"devtools_page": "devtools.html",
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": ["assets/img/xverse_icon.png"],
|
||||
"matches": []
|
||||
}
|
||||
]
|
||||
"web_accessible_resources": ["assets/img/xverse_icon.png"]
|
||||
}
|
||||
|
||||
@@ -42,6 +42,13 @@ const GlobalStyle = createGlobalStyle`
|
||||
* p {
|
||||
margin: 0;
|
||||
}
|
||||
* a {
|
||||
text-decoration: none;
|
||||
}
|
||||
* a:hover {
|
||||
text-decoration:none;
|
||||
cursor:pointer;
|
||||
}
|
||||
*,
|
||||
*::after,
|
||||
*::before {
|
||||
|
||||
@@ -23,7 +23,9 @@
|
||||
"@hooks/*": ["app/hooks/*"],
|
||||
"@assets/*": ["assets/*"],
|
||||
"@screens/*": ["app/screens/*"],
|
||||
"@utils/*":["app/utils/*"],
|
||||
"@stores/*": ["app/stores/*"],
|
||||
"@utils/*": ["app/utils/*"],
|
||||
"@core/*": ["app/core/*"],
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
|
||||
@@ -6,8 +6,6 @@ process.env.ASSET_PATH = '/';
|
||||
var webpack = require('webpack'),
|
||||
config = require('../webpack.config');
|
||||
|
||||
delete config.xverseWallet;
|
||||
|
||||
config.mode = 'production';
|
||||
|
||||
webpack(config, function (err) {
|
||||
|
||||
@@ -9,6 +9,18 @@ const ReactRefreshTypeScript = require('react-refresh-typescript');
|
||||
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
||||
|
||||
|
||||
const aliases = {
|
||||
// alias stacks.js packages to their esm (default prefers /dist/polyfill)
|
||||
'@stacks/auth': '@stacks/auth/dist/esm',
|
||||
'@stacks/common': '@stacks/common/dist/esm',
|
||||
'@stacks/encryption': '@stacks/encryption/dist/esm',
|
||||
'@stacks/network': '@stacks/network/dist/esm',
|
||||
'@stacks/profile': '@stacks/profile/dist/esm',
|
||||
'@stacks/storage': '@stacks/storage/dist/esm',
|
||||
'@stacks/transactions': '@stacks/transactions/dist/esm',
|
||||
'@stacks/keychain': '@stacks/keychain/dist/esm',
|
||||
};
|
||||
|
||||
const ASSET_PATH = process.env.ASSET_PATH || '/';
|
||||
const SRC_ROOT_PATH = path.join(__dirname, '../', 'src');
|
||||
const BUILD_ROOT_PATH = path.join(__dirname, '../', 'build');
|
||||
@@ -28,6 +40,7 @@ var options = {
|
||||
publicPath: ASSET_PATH,
|
||||
},
|
||||
module: {
|
||||
noParse: /\.wasm$/,
|
||||
rules: [
|
||||
{
|
||||
test: /\.(css)$/,
|
||||
@@ -64,7 +77,18 @@ var options = {
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.wasm$/,
|
||||
// Tells WebPack that this module should be included as
|
||||
// base64-encoded binary file and not as code
|
||||
loader: 'base64-loader',
|
||||
// Disables WebPack's opinion where WebAssembly should be,
|
||||
// makes it think that it's not WebAssembly
|
||||
//
|
||||
// Error: WebAssembly module is included in initial chunk.
|
||||
type: 'javascript/auto',
|
||||
},
|
||||
],
|
||||
},
|
||||
resolve: {
|
||||
@@ -72,6 +96,12 @@ var options = {
|
||||
extensions: fileExtensions
|
||||
.map((extension) => '.' + extension)
|
||||
.concat(['.js', '.jsx', '.ts', '.tsx', '.css']),
|
||||
alias: aliases,
|
||||
fallback: {
|
||||
stream: require.resolve('stream-browserify'),
|
||||
crypto: require.resolve('crypto-browserify'),
|
||||
fs: false,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
new CleanWebpackPlugin({ verbose: false }),
|
||||
@@ -117,7 +147,11 @@ var options = {
|
||||
chunks: ['popup'],
|
||||
cache: false,
|
||||
}),
|
||||
new webpack.ProvidePlugin({
|
||||
Buffer: ['buffer', 'Buffer'],
|
||||
}),
|
||||
],
|
||||
|
||||
infrastructureLogging: {
|
||||
level: 'info',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user