Skip to content

Commit

Permalink
feat: request accounts
Browse files Browse the repository at this point in the history
Adds a new `request` mechanism through which dApps
can interact with the wallet with a JSON RPC API.
Initially, this only supports:

`stx_requestAccounts`, a method that requests
public key information from the wallet.

chore: tidy test app, adding request accounts

chore: move request account logic to new file
  • Loading branch information
kyranjamie committed Jul 15, 2022
1 parent 6a049e7 commit a0edbf1
Show file tree
Hide file tree
Showing 42 changed files with 3,187 additions and 377 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@
"@reach/rect": "0.15.3",
"@reach/utils": "0.15.3",
"@reach/visually-hidden": "0.15.2",
"@redux-devtools/extension": "3.2.2",
"@reduxjs/toolkit": "1.7.1",
"@rehooks/document-title": "1.0.2",
"@segment/analytics-next": "1.31.1",
Expand Down Expand Up @@ -183,6 +184,7 @@
"@emotion/babel-preset-css-prop": "11.2.0",
"@emotion/cache": "11.7.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.4",
"@redux-devtools/cli": "1.0.7",
"@schemastore/web-manifest": "0.0.5",
"@stacks/connect-react": "18.0.0",
"@stacks/eslint-config": "1.0.10",
Expand Down Expand Up @@ -271,6 +273,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": "7.1.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 @@ -32,7 +32,7 @@ export function useSaveAuthRequest() {
appURL: new URL(origin),
});

navigate(RouteUrls.ChooseAccount);
navigate(RouteUrls.AccountAuthentication);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
Expand Down
9 changes: 5 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 @@ -43,14 +43,15 @@ export function useKeyActions() {
},

async signOut() {
sendMessage({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
void analytics.track('sign_out');
dispatch(keyActions.signOut());
sendMessageToBackground({ method: InternalMethods.RemoveInMemoryKeys, payload: undefined });
clearSessionLocalData();
void analytics.track('sign_out');
},

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,13 @@
import { 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 { 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 { AccountAvatarWithName } from '@app/components/account-avatar/account-avatar';
Expand All @@ -19,38 +20,14 @@ 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 { useWalletType } from '@app/common/use-wallet-type';

import { AccountWithAddress } from '@app/store/accounts/account.models';
import { useNavigate } from 'react-router-dom';
import { RouteUrls } from '@shared/route-urls';
import { AccountTitle, AccountTitlePlaceholder } from '@app/components/account/account-title';
import { useWalletType } from '@app/common/use-wallet-type';

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

interface AccountTitlePlaceholderProps extends BoxProps {
account: AccountWithAddress;
}
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: AccountWithAddress;
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 @@ -87,11 +64,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 @@ -134,26 +107,33 @@ const AddAccountAction = memo(() => {
);
});

export const Accounts = memo(() => {
const { finishSignIn } = useWallet();
interface AccountPickerProps {
selectedAccountIndex: number | null;
onAccountSelected(index: number): void;
}
export function AccountPicker(props: AccountPickerProps) {
const { onAccountSelected, selectedAccountIndex } = props;
const { whenWallet } = useWalletType();

const accounts = useAccounts();
const navigate = useNavigate();
const [selectedAccount, setSelectedAccount] = useState<number | null>(null);

const signIntoAccount = async (index: number) => {
setSelectedAccount(index);
await whenWallet({
async software() {
await finishSignIn(index);
},
async ledger() {
navigate(RouteUrls.ConnectLedger, { state: { index } });
},
})();
};

if (!accounts) return null;
// const { finishSignIn } = useWallet();
// const accounts = useAccounts();
// const navigate = useNavigate();

// const signIntoAccount = async (index: number) => {
// setSelectedAccount(index);
// await whenWallet({
// async software() {
// await finishSignIn(index);
// },
// async ledger() {
// navigate(RouteUrls.ConnectLedger, { state: { index } });
// },
// })();
// };

// if (!accounts) return null;

return (
<Box mt="loose" width="100%">
Expand All @@ -165,11 +145,11 @@ export const Accounts = memo(() => {
itemContent={(index, account) => (
<AccountItem
account={account}
isLoading={whenWallet({ software: selectedAccount === index, ledger: false })}
onSelectAccount={signIntoAccount}
isLoading={whenWallet({ software: selectedAccountIndex === index, ledger: false })}
onSelectAccount={onAccountSelected}
/>
)}
/>
</Box>
);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,34 @@ import ExploreAppsFull from '@assets/images/onboarding/steps/explore-apps-light.
import ExploreAppsFullDone from '@assets/images/onboarding/steps/explore-apps-light-done.png';
import ExploreAppsPopup from '@assets/images/onboarding/steps/explore-apps-light-sm.png';
import ExploreAppsPopupDone from '@assets/images/onboarding/steps/explore-apps-light-done-sm.png';
<<<<<<< HEAD
import { SuggestedFirstSteps } from '@shared/models/onboarding-types';

import { SuggestedFirstStep } from './suggested-first-step';
=======
import { SuggestedFirstSteps, SuggestedFirstStepStatus } from '@shared/models/onboarding-types';
import { onboardingActions } from '@app/store/onboarding/onboarding.actions';

import { SuggestedFirstStep } from './suggested-first-step';

const exploreAppsExternalRoute = 'https://www.stacks.co/explore/discover-apps#apps';

export function ExploreAppsStep() {
const analytics = useAnalytics();
const dispatch = useAppDispatch();
const suggestedFirstStepsStatus = useSuggestedFirstStepsStatus();

const onSelectStep = useCallback(() => {
void analytics.track('select_next_step', { step: SuggestedFirstSteps.ExploreApps });
dispatch(
onboardingActions.updateSuggestedFirstStepsStatus({
...suggestedFirstStepsStatus,
[SuggestedFirstSteps.ExploreApps]: SuggestedFirstStepStatus.Done,
})
);
openInNewTab(exploreAppsExternalRoute);
}, [analytics, dispatch, suggestedFirstStepsStatus]);
>>>>>>> 5cf51a947 (feat: request accounts)

interface ExploreAppsStepProps {
isComplete: boolean;
Expand Down
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>
);
});
Loading

0 comments on commit a0edbf1

Please sign in to comment.