Skip to content

Commit

Permalink
💄 (smpl): Select and list device in a drawer
Browse files Browse the repository at this point in the history
  • Loading branch information
jdabbech-ledger committed Oct 4, 2024
1 parent cee6a89 commit 9258f6e
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 80 deletions.
15 changes: 9 additions & 6 deletions apps/sample/src/app/client-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import { Sidebar } from "@/components/Sidebar";
import { SdkProvider } from "@/providers/DeviceSdkProvider";
import { DeviceSessionsProvider } from "@/providers/DeviceSessionsProvider";
import { GlobalStyle } from "@/styles/globalstyles";
import { SdkConfigProvider } from "../providers/SdkConfig";
import { SdkConfigProvider } from "@/providers/SdkConfig";
import { DeviceSelectionProvider } from "@/providers/DeviceSelectionProvider";

type ClientRootLayoutProps = {
children: React.ReactNode;
Expand Down Expand Up @@ -48,11 +49,13 @@ const ClientRootLayout: React.FC<ClientRootLayoutProps> = ({ children }) => {
<body>
<Root>
<DeviceSessionsProvider>
<Sidebar />
<PageContainer>
<Header />
{children}
</PageContainer>
<DeviceSelectionProvider>
<Sidebar />
<PageContainer>
<Header />
{children}
</PageContainer>
</DeviceSelectionProvider>
</DeviceSessionsProvider>
</Root>
</body>
Expand Down
49 changes: 42 additions & 7 deletions apps/sample/src/components/Device/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from "react";
import React, { useCallback } from "react";
import {
ConnectionType,
DeviceModelId,
Expand All @@ -11,13 +11,15 @@ import { useDeviceSessionState } from "@/hooks/useDeviceSessionState";

import { StatusText } from "./StatusText";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
import { useSdk } from "@/providers/DeviceSdkProvider";
import { useDeviceSelectionContext } from "@/providers/DeviceSelectionProvider";

const Root = styled(Flex).attrs({ p: 5, mb: 8, borderRadius: 2 })`
background: ${({ theme }: { theme: DefaultTheme }) =>
theme.colors.neutral.c30};
align-items: center;
border: ${({ active, theme }: { theme: DefaultTheme; active: boolean }) =>
`1px solid ${active ? theme.colors.success.c40 : "transparent"}`};
`1px solid ${active ? theme.colors.opacityDefault.c40 : "transparent"}`};
cursor: ${({ active }: { active: boolean }) =>
active ? "normal" : "pointer"};
`;
Expand Down Expand Up @@ -45,8 +47,8 @@ type DeviceProps = {
type: ConnectionType;
sessionId: DeviceSessionId;
model: DeviceModelId;
onDisconnect: () => Promise<void>;
onSelect: () => void;
showActiveIndicator?: boolean;
showSelectDeviceAction?: boolean;
};

function getIconComponent(model: DeviceModelId) {
Expand All @@ -64,18 +66,40 @@ export const Device: React.FC<DeviceProps> = ({
name,
type,
model,
onDisconnect,
onSelect,
sessionId,
showActiveIndicator,
showSelectDeviceAction,
}) => {
const sessionState = useDeviceSessionState(sessionId);
const {
state: { selectedId },
dispatch,
} = useDeviceSessionsContext();
const { setVisibility: setDeviceSelectionVisibility } =
useDeviceSelectionContext();
const sdk = useSdk();
const onDisconnect = useCallback(async () => {
try {
await sdk.disconnect({ sessionId });
dispatch({ type: "remove_session", payload: { sessionId } });
} catch (e) {
console.error(e);
}
}, [dispatch, sdk, sessionId]);
const onSelect = useCallback(() => {
dispatch({
type: "select_session",
payload: { sessionId },
});
}, [sessionId]);
const IconComponent = getIconComponent(model);
const isActive = selectedId === sessionId;

return (
<Root active={isActive} onClick={isActive ? undefined : onSelect}>
<Root
active={showActiveIndicator && isActive}
onClick={isActive ? undefined : onSelect}
>
<IconContainer>
<IconComponent size="S" />
</IconContainer>
Expand Down Expand Up @@ -104,6 +128,17 @@ export const Device: React.FC<DeviceProps> = ({
</Box>
<div data-testid="dropdown_device-option">
<DropdownGeneric closeOnClickOutside label="" placement="bottom">
{showSelectDeviceAction && (
<ActionRow
data-testid="CTA_change-device"
onClick={() => setDeviceSelectionVisibility(true)}
>
<Text variant="paragraph" color="neutral.c80">
Change device
</Text>
<Icons.ChevronRight size="S" />
</ActionRow>
)}
<ActionRow data-testid="CTA_disconnect-device" onClick={onDisconnect}>
<Text variant="paragraph" color="neutral.c80">
Disconnect
Expand Down
9 changes: 4 additions & 5 deletions apps/sample/src/components/MainView/ConnectDeviceActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,17 @@ export const ConnectDeviceActions = ({
size="large"
data-testid="CTA_select-device"
>
Select a device
With Mockserver
</Button>
) : (
<Flex>
<Flex alignItems="center" flexWrap="wrap">
<Button
mx={3}
onClick={() => onSelectDeviceClicked(BuiltinTransports.USB)}
variant="main"
backgroundColor="main"
size="large"
>
Select a USB device
With USB
</Button>
<Button
mx={3}
Expand All @@ -84,7 +83,7 @@ export const ConnectDeviceActions = ({
backgroundColor="main"
size="large"
>
Select a BLE device
With Bluetooth
</Button>
</Flex>
);
Expand Down
45 changes: 45 additions & 0 deletions apps/sample/src/components/MainView/DeviceSelectionDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { StyledDrawer } from "@/components/StyledDrawer";
import React from "react";
import { Text } from "@ledgerhq/react-ui";
import { ConnectDeviceActions } from "@/components/MainView/ConnectDeviceActions";
import { SdkError } from "@ledgerhq/device-management-kit";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
import { Device } from "@/components/Device";

export const DeviceSelectionDrawer: React.FC<{
isOpen: boolean;
onClose: () => void;
onError: (error: SdkError | null) => void;
}> = ({ isOpen, onClose, onError }) => {
const {
state: { deviceById },
} = useDeviceSessionsContext();
return (
<StyledDrawer isOpen={isOpen} onClose={onClose} big title="Select a device">
<Text variant="body" fontWeight="regular" color="opacityDefault.c60">
Connect another device
</Text>
<ConnectDeviceActions onError={onError} />
<Text
variant="body"
fontWeight="regular"
color="opacityDefault.c60"
mt={5}
>
Available devices
</Text>
<div data-testid="container_devices">
{Object.entries(deviceById).map(([sessionId, device]) => (
<Device
key={sessionId}
sessionId={sessionId}
name={device.name}
model={device.modelId}
type={device.type}
showActiveIndicator
/>
))}
</div>
</StyledDrawer>
);
};
40 changes: 14 additions & 26 deletions apps/sample/src/components/MainView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import React, { useEffect, useState } from "react";
import { Badge, Flex, Icon, Text, Notification } from "@ledgerhq/react-ui";
import { Flex, Text, Button } from "@ledgerhq/react-ui";
import Image from "next/image";
import styled, { DefaultTheme } from "styled-components";

import { SdkError } from "@ledgerhq/device-management-kit";
import { ConnectDeviceActions } from "./ConnectDeviceActions";
import { useDeviceSelectionContext } from "@/providers/DeviceSelectionProvider";

const Root = styled(Flex)`
flex: 1;
justify-content: center;
align-items: center;
flex-direction: column;
`;
const ErrorNotification = styled(Notification)`
position: absolute;
bottom: 10px;
width: 70%;
`;

const Description = styled(Text).attrs({ my: 6 })`
color: ${({ theme }: { theme: DefaultTheme }) => theme.colors.neutral.c70};
Expand All @@ -28,6 +23,8 @@ const NanoLogo = styled(Image).attrs({ mb: 8 })`

export const MainView: React.FC = () => {
const [connectionError, setConnectionError] = useState<SdkError | null>(null);
const { setVisibility: setDeviceSelectionVisibility } =
useDeviceSelectionContext();

useEffect(() => {
let timeoutId: NodeJS.Timeout;
Expand Down Expand Up @@ -57,25 +54,16 @@ export const MainView: React.FC = () => {
<Description variant={"body"}>
Use this application to test Ledger hardware device features.
</Description>

<ConnectDeviceActions onError={setConnectionError} />
{connectionError && (
<ErrorNotification
badge={
<Badge
backgroundColor="error.c10"
color="error.c50"
icon={<Icon name="Warning" size={24} />}
/>
}
hasBackground
title="Error"
description={
connectionError.message ||
(connectionError.originalError as Error | undefined)?.message
}
/>
)}
<Button
mx={3}
variant="main"
backgroundColor="main"
size="large"
data-testid="CTA_open-select-device-drawer"
onClick={() => setDeviceSelectionVisibility(true)}
>
Select a device
</Button>
</Root>
);
};
54 changes: 27 additions & 27 deletions apps/sample/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useCallback, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import { Box, Flex, Icons, Link, Text } from "@ledgerhq/react-ui";
import { useRouter } from "next/navigation";
import styled, { DefaultTheme } from "styled-components";
Expand All @@ -8,7 +8,7 @@ import { Device } from "@/components/Device";
import { Menu } from "@/components/Menu";
import { useSdk } from "@/providers/DeviceSdkProvider";
import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider";
import { useSdkConfigContext } from "../../providers/SdkConfig";
import { useSdkConfigContext } from "@/providers/SdkConfig";
import { BuiltinTransports } from "@ledgerhq/device-management-kit";

const Root = styled(Flex).attrs({ py: 8, px: 6 })`
Expand All @@ -26,6 +26,14 @@ const Root = styled(Flex).attrs({ py: 8, px: 6 })`
: theme.colors.background.drawer};
`;

const NoDeviceContainer = styled(Flex).attrs({
backgroundColor: "opacityDefault.c10",
mb: 8,
borderRadius: 2,
})`
height: 66px;
`;

const Subtitle = styled(Text).attrs({ mb: 5 })``;

const MenuContainer = styled(Box)`
Expand Down Expand Up @@ -55,7 +63,6 @@ export const Sidebar: React.FC = () => {
const sdk = useSdk();
const {
state: { deviceById, selectedId },
dispatch,
} = useDeviceSessionsContext();
const {
state: { transport },
Expand All @@ -70,21 +77,13 @@ export const Sidebar: React.FC = () => {
setVersion("");
});
}, [sdk]);
const onDeviceDisconnect = useCallback(
async (sessionId: string) => {
try {
await sdk.disconnect({ sessionId });
dispatch({ type: "remove_session", payload: { sessionId } });
} catch (e) {
console.error(e);
}
},
[dispatch, sdk],
);

const router = useRouter();
return (
<Root mockServerEnabled={transport === BuiltinTransports.MOCK_SERVER}>
<Root
mockServerEnabled={transport === BuiltinTransports.MOCK_SERVER}
data-testid="container_sidebar-view"
>
<Link
onClick={() => router.push("/")}
mb={8}
Expand All @@ -101,20 +100,21 @@ export const Sidebar: React.FC = () => {
</Subtitle>

<Subtitle variant={"tiny"}>Device</Subtitle>
<div data-testid="container_devices">
{Object.entries(deviceById).map(([sessionId, device]) => (
<div data-testid="container_main-device">
{selectedId ? (
<Device
key={sessionId}
sessionId={sessionId}
name={device.name}
model={device.modelId}
type={device.type}
onSelect={() =>
dispatch({ type: "select_session", payload: { sessionId } })
}
onDisconnect={() => onDeviceDisconnect(sessionId)}
key={selectedId}
sessionId={selectedId}
name={deviceById[selectedId].name}
model={deviceById[selectedId].modelId}
type={deviceById[selectedId].type}
showSelectDeviceAction
/>
))}
) : (
<NoDeviceContainer alignItems="center" justifyContent="center">
No device connected
</NoDeviceContainer>
)}
</div>
<MenuContainer active={!!selectedId}>
<Subtitle variant={"tiny"}>Menu</Subtitle>
Expand Down
Loading

0 comments on commit 9258f6e

Please sign in to comment.