Skip to content

Commit

Permalink
feat(devwallet): change profile (#2651)
Browse files Browse the repository at this point in the history
  • Loading branch information
sstraatemans authored Nov 8, 2024
1 parent b28d165 commit 65c30e4
Show file tree
Hide file tree
Showing 13 changed files with 467 additions and 228 deletions.
2 changes: 2 additions & 0 deletions .changeset/bright-falcons-exercise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
3 changes: 2 additions & 1 deletion packages/apps/dev-wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,16 @@
"@tauri-apps/api": "^1.4.0",
"@vanilla-extract/css": "1.14.2",
"@vanilla-extract/dynamic": "^2.1.2",
"@vanilla-extract/recipes": "0.5.1",
"asn1js": "^3.0.5",
"cbor-web": "^9.0.2",
"chrome-types": "^0.1.248",
"classnames": "^2.3.1",
"createRuntimeFn": "link:@vanilla-extract/recipes/createRuntimeFn",
"ed25519-keygen": "0.4.8",
"elliptic": "^6.5.5",
"js-yaml": "~4.1.0",
"react": "^18.2.0",
"react-aria": "^3.31.1",
"react-console-emulator": "^5.0.2",
"react-dom": "^18.2.0",
"react-hook-form": "^7.45.4",
Expand Down
62 changes: 49 additions & 13 deletions packages/apps/dev-wallet/src/App/Layout/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,30 @@ import {
import { NetworkSelector } from '@/Components/NetworkSelector/NetworkSelector';

import { useWallet } from '@/modules/wallet/wallet.hook';
import { Button, Themes, useTheme } from '@kadena/kode-ui';
import { unlockWithWebAuthn } from '@/utils/unlockWithWebAuthn';
import {
Button,
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
Themes,
useTheme,
} from '@kadena/kode-ui';
import {
SideBarItem,
SideBarItemsInline,
SideBar as SideBarUI,
useLayout,
} from '@kadena/kode-ui/patterns';
import { FC } from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { KLogo } from './KLogo';

export const SideBar: FC = () => {
const { theme, setTheme } = useTheme();
const { isExpanded } = useLayout();
const { lockProfile } = useWallet();
const { lockProfile, profileList, unlockProfile } = useWallet();
const navigate = useNavigate();

const toggleTheme = (): void => {
const newTheme = theme === Themes.dark ? Themes.light : Themes.dark;
Expand All @@ -40,9 +49,11 @@ export const SideBar: FC = () => {
return (
<SideBarUI
logo={
<Link to="/">
<KLogo height={40} />
</Link>
<>
<Link to="/">
<KLogo height={40} />
</Link>
</>
}
appContext={
<SideBarItem visual={<MonoNetworkCheck />} label="Select network">
Expand Down Expand Up @@ -108,13 +119,38 @@ export const SideBar: FC = () => {
context={
<>
<SideBarItemsInline>
<SideBarItem visual={<MonoLogout />} label="Logout">
<Button
isCompact
variant="transparent"
onPress={lockProfile}
startVisual={<MonoLogout />}
/>
<SideBarItem visual={<MonoContacts />} label="Profile">
<ContextMenu
trigger={
<Button
isCompact
variant={isExpanded ? 'outlined' : 'transparent'}
endVisual={<MonoContacts />}
>
{isExpanded ? 'Profile' : undefined}
</Button>
}
>
{profileList.map((profile) => (
<ContextMenuItem
key={profile.uuid}
label={profile.name}
onClick={async () => {
if (profile.options.authMode === 'WEB_AUTHN') {
await unlockWithWebAuthn(profile, unlockProfile);
} else {
navigate(`/unlock-profile/${profile.uuid}`);
}
}}
/>
))}
<ContextMenuDivider />
<ContextMenuItem
endVisual={<MonoLogout />}
label="Logout"
onClick={lockProfile}
/>
</ContextMenu>
</SideBarItem>
<SideBarItem
visual={<MonoContrast />}
Expand Down
8 changes: 4 additions & 4 deletions packages/apps/dev-wallet/src/App/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,6 @@ export const Routes: FC = () => {
<Route element={<Redirect if={!isLocked} to={origin} />}>
<Route path="/select-profile" element={<SelectProfile />} />
<Route path="/create-profile/*" element={<CreateProfile />} />
<Route
path="/unlock-profile/:profileId"
element={<UnlockProfile />}
/>
<Route
path="/import-wallet"
element={<ImportWallet setOrigin={setOrigin} />}
Expand Down Expand Up @@ -139,6 +135,10 @@ export const Routes: FC = () => {
</Route>
</Route>
<Route element={<LayoutMini />}>
<Route
path="/unlock-profile/:profileId"
element={<UnlockProfile origin={origin} />}
/>
<Route path="/ready" element={<Ready />} />
<Route path="*" element={<Heading>Not found!</Heading>} />
</Route>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { useWallet } from '@/modules/wallet/wallet.hook';
import { IProfilePicked, unlockWithWebAuthn } from '@/utils/unlockWithWebAuthn';
import { MonoMoreHoriz } from '@kadena/kode-icons/system';
import {
Button,
ContextMenu,
ContextMenuDivider,
ContextMenuItem,
Stack,
} from '@kadena/kode-ui';
import { FC } from 'react';
import { useNavigate } from 'react-router-dom';
import { Profile } from './components/Profile';
import { profileClass, profileListClass } from './components/style.css';

const getInitial = (name: string): string => {
return name.at(0)?.toUpperCase() ?? '-';
};

export const ProfileChanger: FC = () => {
const {
profileList,
profile: currentProfile,
unlockProfile,
lockProfile,
} = useWallet();
const navigate = useNavigate();

const handleSelect = async (profile: IProfilePicked) => {
if (profile.options.authMode === 'WEB_AUTHN') {
await unlockWithWebAuthn(profile, unlockProfile);
} else {
navigate(`/unlock-profile/${profile.uuid}`);
}
};

const filteredProfile = profileList.filter(
(p) => p.uuid !== currentProfile?.uuid,
)[0];

const hasMoreOptions = profileList.length > 2;

return (
<Stack as="section" className={profileListClass}>
{hasMoreOptions && (
<ContextMenu
placement="bottom start"
trigger={
<Button className={profileClass()}>
<MonoMoreHoriz />
</Button>
}
>
{profileList.map((profile) => {
return (
<ContextMenuItem
key={profile.uuid}
label={profile.name}
onClick={() => {
handleSelect(profile);
}}
/>
);
})}
<ContextMenuDivider />
<ContextMenuItem label="logout" onClick={lockProfile} />
</ContextMenu>
)}

{filteredProfile && (
<Profile
key={filteredProfile.uuid}
color={filteredProfile.accentColor}
idx={hasMoreOptions ? 1 : 0}
hasMoreOptions={hasMoreOptions}
isActive={currentProfile?.uuid === filteredProfile.uuid}
onClick={() => handleSelect(filteredProfile)}
>
{getInitial(filteredProfile.name)}
</Profile>
)}

{currentProfile && (
<Profile
key={currentProfile.uuid}
color={currentProfile.accentColor}
idx={hasMoreOptions ? 2 : 0}
hasMoreOptions={hasMoreOptions}
isActive={currentProfile?.uuid === currentProfile.uuid}
onClick={() => handleSelect(currentProfile)}
>
{getInitial(currentProfile.name)}
</Profile>
)}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Stack } from '@kadena/kode-ui';
import { FC, PropsWithChildren } from 'react';
import { profileClass } from './style.css';

interface IProps extends PropsWithChildren {
idx?: number;
color: string;
isActive: boolean;
onClick: () => void;
hasMoreOptions: boolean;
}

export const Profile: FC<IProps> = ({
children,
color,
idx,
isActive,
onClick,
hasMoreOptions,
}) => {
return (
<Stack
as="button"
data-hasMoreOptions={hasMoreOptions}
onClick={() => onClick()}
className={profileClass({ isActive })}
style={{
backgroundColor: color,
zIndex: idx ?? 0 + 1,
transform:
idx && hasMoreOptions
? `translateX(-${90 * idx}%)`
: `translateX(-100%)`,
}}
>
{children}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {
atoms,
globalStyle,
recipe,
style,
token,
} from '@kadena/kode-ui/styles';

export const profileClass = recipe({
base: {
borderRadius: token('radius.round'),
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
fontWeight: token('typography.weight.primaryFont.medium'),

border: 0,
width: '32px',
height: '32px',
aspectRatio: '1/1',
color: token('color.background.base.default'),
willChange: 'transform',
transition: 'transform .4s ease',
selectors: {
'&:hover': {
opacity: '.8',
},
},
},
variants: {
isActive: {
true: {
selectors: {
'&:after': {
content: '',
position: 'absolute',
backgroundColor: token(
'color.background.accent.primary.inverse.default',
),
borderRadius: '50%',
width: '10px',
height: '10px',
bottom: 0,
right: 0,
},
},
},
false: {},
},
},
});

export const profileListClass = style([
atoms({
position: 'relative',
padding: 'no',
margin: 'no',
listStyleType: 'none',
alignItems: 'center',
cursor: 'pointer',
gap: 'xs',
}),
]);

globalStyle(
`${profileListClass}:hover ${profileClass()}[data-hasMoreOptions="true"]`,
{
background: 'green',
transform: 'translateX(0)!important',
},
);
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { useWallet } from '@/modules/wallet/wallet.hook';
import { IProfile } from '@/modules/wallet/wallet.repository';
import { recoverPublicKey, retrieveCredential } from '@/utils/webAuthn';
import { unlockWithWebAuthn } from '@/utils/unlockWithWebAuthn';
import { MonoAdd } from '@kadena/kode-icons';
import { Box, Heading, Stack } from '@kadena/kode-ui';
import { tokens } from '@kadena/kode-ui/styles';
Expand All @@ -20,27 +19,6 @@ export function SelectProfile() {
const { profileList, unlockProfile } = useWallet();
const [params] = useSearchParams();

const unlockWithWebAuthn = async (
profile: Pick<IProfile, 'name' | 'uuid' | 'accentColor' | 'options'>,
) => {
if (profile.options.authMode !== 'WEB_AUTHN') {
throw new Error('Profile does not support WebAuthn');
}
const credentialId = profile.options.webAuthnCredential;
const credential = await retrieveCredential(credentialId);
if (!credential) {
throw new Error('Failed to retrieve credential');
}
const keys = await recoverPublicKey(credential);
for (const key of keys) {
const result = await unlockProfile(profile.uuid, key);
if (result) {
return;
}
}
console.error('Failed to unlock profile');
};

const redirect = params.get('redirect');

return (
Expand All @@ -66,7 +44,7 @@ export function SelectProfile() {
key={profile.uuid}
className={cardClass}
onClick={() => {
unlockWithWebAuthn(profile);
unlockWithWebAuthn(profile, unlockProfile);
}}
>
<Stack alignItems="center" gap="md">
Expand Down
Loading

0 comments on commit 65c30e4

Please sign in to comment.