diff --git a/apps/sample/src/components/Device/index.tsx b/apps/sample/src/components/Device/index.tsx index e231fd5eb..61fbba86f 100644 --- a/apps/sample/src/components/Device/index.tsx +++ b/apps/sample/src/components/Device/index.tsx @@ -16,6 +16,10 @@ 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"}`}; + cursor: ${({ active }: { active: boolean }) => + active ? "normal" : "pointer"}; `; const IconContainer = styled(Flex).attrs({ p: 4, mr: 3, borderRadius: 100 })` @@ -42,6 +46,7 @@ type DeviceProps = { sessionId: DeviceSessionId; model: DeviceModelId; onDisconnect: () => Promise; + onSelect: () => void; }; function getIconComponent(model: DeviceModelId) { @@ -60,16 +65,17 @@ export const Device: React.FC = ({ type, model, onDisconnect, + onSelect, sessionId, }) => { const sessionState = useDeviceSessionState(sessionId); const { - state: { deviceById, selectedId }, - dispatch, + state: { selectedId }, } = useDeviceSessionsContext(); const IconComponent = getIconComponent(model); + const isActive = selectedId === sessionId; return ( - + @@ -98,17 +104,6 @@ export const Device: React.FC = ({
- {Object.values(deviceById).length > 1 && selectedId !== sessionId && ( - - dispatch({ type: "select_session", payload: { sessionId } }) - } - > - - Select - - - )} Disconnect diff --git a/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx b/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx index 23dfa742e..7f86c9757 100644 --- a/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx +++ b/apps/sample/src/components/DeviceActionsView/AllDeviceActions.tsx @@ -38,10 +38,6 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({ const deviceModelId = sdk.getConnectedDevice({ sessionId, }).modelId; - console.log( - "sdk get connected device::", - sdk.getConnectedDevice({ sessionId }), - ); // eslint-disable-next-line @typescript-eslint/no-explicit-any const deviceActions: DeviceActionProps[] = useMemo( @@ -155,7 +151,7 @@ export const AllDeviceActions: React.FC<{ sessionId: string }> = ({ ListAppsWithMetadataDAIntermediateValue >, ], - [], + [sessionId, deviceModelId], ); return ( diff --git a/apps/sample/src/components/Header/index.tsx b/apps/sample/src/components/Header/index.tsx index 8265b538a..a8941ca9e 100644 --- a/apps/sample/src/components/Header/index.tsx +++ b/apps/sample/src/components/Header/index.tsx @@ -1,14 +1,14 @@ import React, { useCallback, useState } from "react"; import { Button, - Dropdown, DropdownGeneric, Flex, Icons, Input, + Switch, } from "@ledgerhq/react-ui"; import styled, { DefaultTheme } from "styled-components"; -import { useSdkConfigContext } from "../../providers/SdkConfig"; +import { useSdkConfigContext } from "@/providers/SdkConfig"; import { BuiltinTransports } from "@ledgerhq/device-management-kit"; const Root = styled(Flex).attrs({ py: 3, px: 10, gridGap: 8 })` @@ -33,50 +33,25 @@ const UrlInput = styled(Input)` align-items: center; `; -type DropdownOption = { - label: string; - value: BuiltinTransports; -}; - -const DropdownValues: DropdownOption[] = [ - { - label: "USB", - value: BuiltinTransports.USB, - }, - { - label: "BLE", - value: BuiltinTransports.BLE, - }, - { - label: "Mock server", - value: BuiltinTransports.MOCK_SERVER, - }, -]; - export const Header = () => { const { dispatch, state: { transport, mockServerUrl }, } = useSdkConfigContext(); - const onChangeTransport = useCallback( - (selectedValue: DropdownOption | null) => { - if (selectedValue) { - dispatch({ - type: "set_transport", - payload: { transport: selectedValue.value }, - }); - } - }, - [], - ); + const onToggleMockServer = useCallback(() => { + dispatch({ + type: "set_transport", + payload: { + transport: + transport === BuiltinTransports.MOCK_SERVER + ? BuiltinTransports.USB + : BuiltinTransports.MOCK_SERVER, + }, + }); + }, [transport]); const [mockServerStateUrl, setMockServerStateUrl] = useState(mockServerUrl); - - const getDropdownValue = useCallback( - (transport: BuiltinTransports): DropdownOption | undefined => - DropdownValues.find((option) => option.value === transport), - [], - ); + const mockServerEnabled = transport === BuiltinTransports.MOCK_SERVER; const validateServerUrl = useCallback( () => @@ -98,48 +73,29 @@ export const Header = () => {
- - - {transport === BuiltinTransports.MOCK_SERVER && ( - setMockServerStateUrl(url)} - renderRight={() => ( - - - - )} + +
+ - )} +
+ {mockServerEnabled && ( + setMockServerStateUrl(url)} + renderRight={() => ( + + + + )} + /> + )}
diff --git a/apps/sample/src/components/MainView/ConnectDeviceActions.tsx b/apps/sample/src/components/MainView/ConnectDeviceActions.tsx new file mode 100644 index 000000000..7f899d6a0 --- /dev/null +++ b/apps/sample/src/components/MainView/ConnectDeviceActions.tsx @@ -0,0 +1,91 @@ +import { Button, Flex } from "@ledgerhq/react-ui"; +import { BuiltinTransports, SdkError } from "@ledgerhq/device-management-kit"; +import React, { useCallback } from "react"; +import { useSdkConfigContext } from "@/providers/SdkConfig"; +import { useSdk } from "@/providers/DeviceSdkProvider"; +import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider"; + +type ConnectDeviceActionsProps = { + onError: (error: SdkError | null) => void; +}; + +export const ConnectDeviceActions = ({ + onError, +}: ConnectDeviceActionsProps) => { + const { + dispatch: dispatchSdkConfig, + state: { transport }, + } = useSdkConfigContext(); + const { dispatch: dispatchDeviceSession } = useDeviceSessionsContext(); + const sdk = useSdk(); + + const onSelectDeviceClicked = useCallback( + (selectedTransport: BuiltinTransports) => { + onError(null); + dispatchSdkConfig({ + type: "set_transport", + payload: { transport: selectedTransport }, + }); + sdk.startDiscovering({ transport: selectedTransport }).subscribe({ + next: (device) => { + sdk + .connect({ device }) + .then((sessionId) => { + console.log( + `🦖 Response from connect: ${JSON.stringify(sessionId)} 🎉`, + ); + dispatchDeviceSession({ + type: "add_session", + payload: { + sessionId, + connectedDevice: sdk.getConnectedDevice({ sessionId }), + }, + }); + }) + .catch((error) => { + onError(error); + console.error(`Error from connection or get-version`, error); + }); + }, + error: (error) => { + console.error(error); + }, + }); + }, + [sdk, transport], + ); + + return transport === BuiltinTransports.MOCK_SERVER ? ( + + ) : ( + + + + + ); +}; diff --git a/apps/sample/src/components/MainView/index.tsx b/apps/sample/src/components/MainView/index.tsx index 9207b85c5..166326acf 100644 --- a/apps/sample/src/components/MainView/index.tsx +++ b/apps/sample/src/components/MainView/index.tsx @@ -1,10 +1,10 @@ -import React, { useCallback } from "react"; -import { Button, Flex, Text } from "@ledgerhq/react-ui"; +import React, { useEffect, useState } from "react"; +import { Badge, Flex, Icon, Text, Notification } from "@ledgerhq/react-ui"; import Image from "next/image"; import styled, { DefaultTheme } from "styled-components"; -import { useSdk } from "@/providers/DeviceSdkProvider"; -import { useDeviceSessionsContext } from "@/providers/DeviceSessionsProvider"; +import { SdkError } from "@ledgerhq/device-management-kit"; +import { ConnectDeviceActions } from "./ConnectDeviceActions"; const Root = styled(Flex)` flex: 1; @@ -12,6 +12,11 @@ const Root = styled(Flex)` 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}; @@ -22,36 +27,21 @@ const NanoLogo = styled(Image).attrs({ mb: 8 })` `; export const MainView: React.FC = () => { - const sdk = useSdk(); - const { dispatch } = useDeviceSessionsContext(); + const [connectionError, setConnectionError] = useState(null); - // Example starting the discovery on a user action - const onSelectDeviceClicked = useCallback(() => { - sdk.startDiscovering({}).subscribe({ - next: (device) => { - sdk - .connect({ device }) - .then((sessionId) => { - console.log( - `🦖 Response from connect: ${JSON.stringify(sessionId)} 🎉`, - ); - dispatch({ - type: "add_session", - payload: { - sessionId, - connectedDevice: sdk.getConnectedDevice({ sessionId }), - }, - }); - }) - .catch((error) => { - console.error(`Error from connection or get-version`, error); - }); - }, - error: (error) => { - console.error(error); - }, - }); - }, [sdk]); + useEffect(() => { + let timeoutId: NodeJS.Timeout; + if (connectionError) { + timeoutId = setTimeout(() => { + setConnectionError(null); + }, 3000); + } + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [connectionError]); return ( @@ -68,15 +58,24 @@ export const MainView: React.FC = () => { Use this application to test Ledger hardware device features. - + + {connectionError && ( + } + /> + } + hasBackground + title="Error" + description={ + connectionError.message || + (connectionError.originalError as Error | undefined)?.message + } + /> + )} ); }; diff --git a/apps/sample/src/components/SessionIdWrapper.tsx b/apps/sample/src/components/SessionIdWrapper.tsx index 282e682fd..81a6e4fa3 100644 --- a/apps/sample/src/components/SessionIdWrapper.tsx +++ b/apps/sample/src/components/SessionIdWrapper.tsx @@ -39,5 +39,5 @@ export const SessionIdWrapper: React.FC<{ ); } - return ; + return ; }; diff --git a/apps/sample/src/components/Sidebar/index.tsx b/apps/sample/src/components/Sidebar/index.tsx index 6404da674..91b659d6c 100644 --- a/apps/sample/src/components/Sidebar/index.tsx +++ b/apps/sample/src/components/Sidebar/index.tsx @@ -109,7 +109,10 @@ export const Sidebar: React.FC = () => { name={device.name} model={device.modelId} type={device.type} - onDisconnect={async () => onDeviceDisconnect(sessionId)} + onSelect={() => + dispatch({ type: "select_session", payload: { sessionId } }) + } + onDisconnect={() => onDeviceDisconnect(sessionId)} /> ))}
diff --git a/apps/sample/src/hooks/usePrevious.ts b/apps/sample/src/hooks/usePrevious.ts new file mode 100644 index 000000000..d6d1f2155 --- /dev/null +++ b/apps/sample/src/hooks/usePrevious.ts @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "react"; + +export function usePrevious(value: T) { + const ref = useRef(); + useEffect(() => { + ref.current = value; //assign the value of ref to the argument + }, [value]); //this code will run when the value of 'value' changes + return ref.current; //in the end, return the current ref value. +} diff --git a/apps/sample/src/providers/DeviceSdkProvider/index.tsx b/apps/sample/src/providers/DeviceSdkProvider/index.tsx index 4d6e35775..22ff0d05c 100644 --- a/apps/sample/src/providers/DeviceSdkProvider/index.tsx +++ b/apps/sample/src/providers/DeviceSdkProvider/index.tsx @@ -6,10 +6,12 @@ import { DeviceSdkBuilder, } from "@ledgerhq/device-management-kit"; import { useSdkConfigContext } from "../SdkConfig"; +import { usePrevious } from "@/hooks/usePrevious"; const defaultSdk = new DeviceSdkBuilder() .addLogger(new ConsoleLogger()) .addTransport(BuiltinTransports.BLE) + .addTransport(BuiltinTransports.USB) .build(); const SdkContext = createContext(defaultSdk); @@ -22,6 +24,7 @@ export const SdkProvider: React.FC = ({ children }) => { const { state: { transport, mockServerUrl }, } = useSdkConfigContext(); + const previousTransport = usePrevious(transport); const [sdk, setSdk] = useState(defaultSdk); useEffect(() => { if (transport === BuiltinTransports.MOCK_SERVER) { @@ -33,16 +36,17 @@ export const SdkProvider: React.FC = ({ children }) => { .addConfig({ mockUrl: mockServerUrl }) .build(), ); - } else { + } else if (previousTransport === BuiltinTransports.MOCK_SERVER) { sdk.close(); setSdk( new DeviceSdkBuilder() .addLogger(new ConsoleLogger()) - .addTransport(transport) + .addTransport(BuiltinTransports.BLE) + .addTransport(BuiltinTransports.USB) .build(), ); } - }, [transport, mockServerUrl]); + }, [transport, mockServerUrl, previousTransport]); if (sdk) { return {children};