Skip to content

Commit

Permalink
refactor: account selection page
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Apr 22, 2022
1 parent 59aa057 commit a8fc798
Show file tree
Hide file tree
Showing 29 changed files with 332 additions and 145 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@
"ts-node": "10.4",
"ts-unused-exports": "7.0.3",
"tsconfig-paths-webpack-plugin": "3.5.2",
"type-fest": "2.12.2",
"typescript": "4.5.4",
"vm-browserify": "1.1.2",
"web-ext": "6.2.0",
Expand Down
24 changes: 24 additions & 0 deletions src/app/common/actions/send-request-account-response.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { sendRequestAccountResponseToTab } from './send-request-account-response';
import { sendMessageToTab } from '@shared/messages';

jest.mock('@shared/messages', () => ({
sendMessageToTab: jest.fn().mockImplementation(() => null),
}));

describe(sendRequestAccountResponseToTab.name, () => {
it('must only return to app with public keys', () => {
sendRequestAccountResponseToTab({
tabId: '2',
id: '1',
account: {
stxPublicKey: 'pubKey1',
dataPublicKey: 'dataKey1',
stxPrivateKey: 'lskdjfjsldf',
} as any,
});
expect(sendMessageToTab).toHaveBeenCalledTimes(1);
expect(sendMessageToTab).toHaveBeenCalledWith(2, '1', {
result: [{ dataPublicKey: 'dataKey1', stxPublicKey: 'pubKey1' }],
});
});
});
22 changes: 22 additions & 0 deletions src/app/common/actions/send-request-account-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { SoftwareWalletAccountWithAddress } from '@app/store/accounts/account.models';
import { sendMessageToTab } from '@shared/messages';

interface SendRequestAccountResponseToTabArgs {
tabId: string;
id: string;
account: SoftwareWalletAccountWithAddress;
}
export function sendRequestAccountResponseToTab(args: SendRequestAccountResponseToTabArgs) {
const { tabId, id, account } = args;

const safeAccountKeys = {
stxPublicKey: account.stxPublicKey,
dataPublicKey: account.dataPublicKey,
};

return sendMessageToTab(parseInt(tabId), id, { result: [safeAccountKeys] });
}

export function sendUserDeniesAccountRequest({ tabId, id }: { tabId: string; id: string }) {
return sendMessageToTab(parseInt(tabId), id, { error: { code: 4000, message: 'lskdjflksjdfl' } });
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function useSaveAuthRequest() {
appURL: new URL(origin),
});

navigate(RouteUrls.ChooseAccount);
navigate(RouteUrls.AccountAuthentication);
},
[saveAuthRequest, navigate]
);
Expand Down
8 changes: 4 additions & 4 deletions src/app/common/hooks/use-key-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { clearSessionLocalData } from '@app/common/store-utils';
import { createNewAccount, stxChainActions } from '@app/store/chains/stx-chain.actions';
import { keyActions } from '@app/store/keys/key.actions';
import { useAnalytics } from './analytics/use-analytics';
import { sendMessage } from '@shared/messages';
import { sendMessageToBackground } from '@shared/messages';
import { InternalMethods } from '@shared/message-types';
import { inMemoryKeyActions } from '@app/store/in-memory-key/in-memory-key.actions';

Expand All @@ -23,7 +23,7 @@ export function useKeyActions() {

generateWalletKey() {
const secretKey = generateSecretKey(256);
sendMessage({
sendMessageToBackground({
method: InternalMethods.ShareInMemoryKeyToBackground,
payload: { secretKey, keyId: 'default' },
});
Expand All @@ -45,12 +45,12 @@ export function useKeyActions() {
async signOut() {
void analytics.track('sign_out');
dispatch(keyActions.signOut());
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
sendMessageToBackground({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
clearSessionLocalData();
},

lockWallet() {
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
sendMessageToBackground({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
return dispatch(inMemoryKeyActions.lockWallet());
},
}),
Expand Down
28 changes: 28 additions & 0 deletions src/app/components/account/account-title.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { BoxProps } from '@stacks/ui';

import { SoftwareWalletAccountWithAddress } from '@app/store/accounts/account.models';
import { Title } from '../typography';

interface AccountTitlePlaceholderProps extends BoxProps {
account: SoftwareWalletAccountWithAddress;
}
export function AccountTitlePlaceholder({ account, ...rest }: AccountTitlePlaceholderProps) {
const name = `Account ${account?.index + 1}`;
return (
<Title fontSize={2} lineHeight="1rem" fontWeight="400" {...rest}>
{name}
</Title>
);
}

interface AccountTitleProps extends BoxProps {
account: SoftwareWalletAccountWithAddress;
name: string;
}
export function AccountTitle({ account, name, ...rest }: AccountTitleProps) {
return (
<Title fontSize={2} lineHeight="1rem" fontWeight="400" {...rest}>
{name}
</Title>
);
}
24 changes: 24 additions & 0 deletions src/app/features/account-picker/account-picker.layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Flex, Stack, Text } from '@stacks/ui';

import { AppIcon } from '@app/components/app-icon';
import { Title } from '@app/components/typography';

interface AccountPickerLayoutProps {
appName?: string;
children: React.ReactNode;
}
export function AccountPickerLayout(props: AccountPickerLayoutProps) {
const { appName, children } = props;
return (
<Flex flexDirection="column" px={['loose', 'unset']} width="100%">
<Stack spacing="loose" textAlign="center">
<AppIcon mt="extra-loose" mb="loose" size="72px" />
<Stack spacing="base">
<Title fontSize={4}>Choose an account</Title>
<Text textStyle="caption">to connect to {appName}</Text>
</Stack>
</Stack>
{children}
</Flex>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { useCallback, Suspense, memo, useState, useMemo } from 'react';
import { Suspense, memo, useMemo } from 'react';
import { FiPlusCircle } from 'react-icons/fi';
import { Virtuoso } from 'react-virtuoso';
import { Box, BoxProps, color, FlexProps, Spinner, Stack } from '@stacks/ui';
import { Box, color, FlexProps, Spinner, Stack } from '@stacks/ui';
import { truncateMiddle } from '@stacks/ui-utils';

import { Caption, Text, Title } from '@app/components/typography';
import { Caption, Text } from '@app/components/typography';
import { useAccountDisplayName } from '@app/common/hooks/account/use-account-names';
import { useWallet } from '@app/common/hooks/use-wallet';

import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state';
import { useCreateAccount } from '@app/common/hooks/account/use-create-account';
import type { SoftwareWalletAccountWithAddress } from '@app/store/accounts/account.models';
Expand All @@ -20,34 +20,11 @@ import {
import { slugify } from '@app/common/utils';
import { useAccounts, useHasCreatedAccount } from '@app/store/accounts/account.hooks';
import { useAddressBalances } from '@app/query/balance/balance.hooks';
import { AccountTitle, AccountTitlePlaceholder } from '@app/components/account/account-title';

const loadingProps = { color: '#A1A7B3' };
const getLoadingProps = (loading: boolean) => (loading ? loadingProps : {});

interface AccountTitlePlaceholderProps extends BoxProps {
account: SoftwareWalletAccountWithAddress;
}
const AccountTitlePlaceholder = ({ account, ...rest }: AccountTitlePlaceholderProps) => {
const name = `Account ${account?.index + 1}`;
return (
<Title fontSize={2} lineHeight="1rem" fontWeight="400" {...rest}>
{name}
</Title>
);
};

interface AccountTitleProps extends BoxProps {
account: SoftwareWalletAccountWithAddress;
name: string;
}
const AccountTitle = ({ account, name, ...rest }: AccountTitleProps) => {
return (
<Title fontSize={2} lineHeight="1rem" fontWeight="400" {...rest}>
{name}
</Title>
);
};

interface AccountItemProps extends FlexProps {
selectedAddress?: string | null;
isLoading: boolean;
Expand Down Expand Up @@ -84,11 +61,7 @@ const AccountItem = memo((props: AccountItemProps) => {
/>
}
>
<AccountTitle
name={name}
{...getLoadingProps(showLoadingProps)}
account={account}
/>
<AccountTitle name={name} account={account} />
</Suspense>
<Stack alignItems="center" spacing="6px" isInline>
<Caption fontSize={0} {...getLoadingProps(showLoadingProps)}>
Expand Down Expand Up @@ -131,21 +104,14 @@ const AddAccountAction = memo(() => {
);
});

export const Accounts = memo(() => {
const { wallet, finishSignIn } = useWallet();
const accounts = useAccounts();
const { decodedAuthRequest } = useOnboardingState();
const [selectedAccount, setSelectedAccount] = useState<number | null>(null);

const signIntoAccount = useCallback(
async (index: number) => {
setSelectedAccount(index);
await finishSignIn(index);
},
[finishSignIn]
);
interface AccountPickerProps {
selectedAccountIndex: number | null;
onAccountSelected(index: number): void;
}
export function AccountPicker(props: AccountPickerProps) {
const { onAccountSelected, selectedAccountIndex } = props;

if (!wallet || !accounts || !decodedAuthRequest) return null;
const accounts = useAccounts();

return (
<>
Expand All @@ -158,12 +124,12 @@ export const Accounts = memo(() => {
itemContent={(index, account) => (
<AccountItem
account={account}
isLoading={selectedAccount === index}
onSelectAccount={signIntoAccount}
isLoading={selectedAccountIndex === index}
onSelectAccount={() => onAccountSelected(index)}
/>
)}
/>
</Box>
</>
);
});
}
45 changes: 45 additions & 0 deletions src/app/pages/account-authentication/account-authentication.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { memo, useCallback, useEffect, useState } from 'react';

import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { useWallet } from '@app/common/hooks/use-wallet';
import { useAppDetails } from '@app/common/hooks/auth/use-app-details';
import { Header } from '@app/components/header';
import { AccountPicker } from '@app/features/account-picker/accounts';
import { useOnboardingState } from '@app/common/hooks/auth/use-onboarding-state';
import { useAccounts } from '@app/store/accounts/account.hooks';
import { AccountPickerLayout } from '@app/features/account-picker/account-picker.layout';

export const AuthenticateAccount = memo(() => {
const accounts = useAccounts();
const { name: appName } = useAppDetails();
const { decodedAuthRequest } = useOnboardingState();
const { cancelAuthentication, wallet, finishSignIn } = useWallet();
const [selectedAccountIndex, setSelectedAccountIndex] = useState<number | null>(null);

useRouteHeader(<Header hideActions />);

const signIntoAccount = async (index: number) => {
setSelectedAccountIndex(index);
await finishSignIn(index);
};

const handleUnmount = useCallback(async () => {
cancelAuthentication();
}, [cancelAuthentication]);

useEffect(() => {
window.addEventListener('beforeunload', handleUnmount);
return () => window.removeEventListener('beforeunload', handleUnmount);
}, [handleUnmount]);

if (!wallet || !accounts || !decodedAuthRequest) return null;

return (
<AccountPickerLayout appName={appName}>
<AccountPicker
onAccountSelected={index => signIntoAccount(index)}
selectedAccountIndex={selectedAccountIndex}
/>
</AccountPickerLayout>
);
});
58 changes: 58 additions & 0 deletions src/app/pages/choose-account-request/account-request.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { logger } from '@shared/logger';

import { useRouteHeader } from '@app/common/hooks/use-route-header';
import { useAppDetails } from '@app/common/hooks/auth/use-app-details';
import { Header } from '@app/components/header';
import { AccountPicker } from '@app/features/account-picker/accounts';
import { useAccounts } from '@app/store/accounts/account.hooks';
import { AccountPickerLayout } from '@app/features/account-picker/account-picker.layout';
import {
sendRequestAccountResponseToTab,
sendUserDeniesAccountRequest,
} from '@app/common/actions/send-request-account-response';

import { useAccountRequestSearchParams } from './use-account-request-search-params';
import { useEffect } from 'react';

export function AccountRequest() {
const accounts = useAccounts();
const { name: appName } = useAppDetails();

const { tabId, id } = useAccountRequestSearchParams();

useRouteHeader(<Header hideActions />);

const returnAccountDetailsToApp = (index: number) => {
if (!accounts) throw new Error('Cannot request account details with no account');

if (!tabId || !id) {
logger.error('Missing either tabId or uuid. Both values are necessary to respond to app');
return;
}

sendRequestAccountResponseToTab({ tabId, id, account: accounts[index] });
window.close();
};

const handleUnmount = () => {
if (!tabId || !id) {
logger.error('Missing either tabId or uuid. Both values are necessary to respond to app');
return;
}
sendUserDeniesAccountRequest({ tabId, id });
};

useEffect(() => {
window.addEventListener('beforeunload', handleUnmount);
return () => window.removeEventListener('beforeunload', handleUnmount);
}, []);

return (
<AccountPickerLayout appName={appName}>
<AccountPicker
onAccountSelected={index => returnAccountDetailsToApp(index)}
selectedAccountIndex={null}
/>
</AccountPickerLayout>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';

export function useAccountRequestSearchParams() {
const [searchParams] = useSearchParams();

return useMemo(
() => ({
tabId: searchParams.get('tabId'),
id: searchParams.get('id'),
}),
[searchParams]
);
}
Loading

0 comments on commit a8fc798

Please sign in to comment.