diff --git a/.changeset/quiet-transport-wire.md b/.changeset/quiet-transport-wire.md new file mode 100644 index 000000000000..2db06da2b8f8 --- /dev/null +++ b/.changeset/quiet-transport-wire.md @@ -0,0 +1,6 @@ +--- +"ledger-live-desktop": minor +"live-mobile": minor +--- + +Add debug WebSocket transport to DevTools relay diff --git a/apps/ledger-live-desktop/package.json b/apps/ledger-live-desktop/package.json index 91208da88d72..2810b068fde0 100644 --- a/apps/ledger-live-desktop/package.json +++ b/apps/ledger-live-desktop/package.json @@ -74,6 +74,8 @@ "@domain/entity-large-screen-upsell-modal": "workspace:*", "@devtools/shell": "workspace:^", "@devtools/bindings": "workspace:*", + "@devtools/transport-panel": "workspace:*", + "@devtools/wire": "workspace:*", "@electron/fuses": "2.0.0", "@features/flow-card": "workspace:^", "@features/flow-contacts": "workspace:^", diff --git a/apps/ledger-live-desktop/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx b/apps/ledger-live-desktop/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx index d937a0bb0907..8d0a0df91148 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx @@ -7,6 +7,9 @@ jest.mock("react-router", () => ({ ...jest.requireActual("react-router"), useNavigate: () => mockNavigate, })); +jest.mock("@devtools/transport-panel", () => ({ + TransportPanel: () => null, +})); const devToolsSpy = jest.fn(); jest.mock("@devtools/shell", () => ({ @@ -24,6 +27,20 @@ jest.mock("@devtools/bindings", () => ({ useFeatureFlagsToolProps: () => ({ marker: "ff-props" }), })); +jest.mock("@devtools/wire", () => { + const wireState = { hubUrl: "ws://127.0.0.1:9090", role: "host" }; + return { + buildTransport: () => ({ + transport: {}, + subscribe: () => () => {}, + getState: () => wireState, + setHubUrl: jest.fn(), + }), + buildCopyStoreProtocol: () => ({}), + combineProtocols: (...args: unknown[]) => args[0], + }; +}); + describe("DevToolsScreen", () => { beforeEach(() => { mockNavigate.mockClear(); diff --git a/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx index 98e62f3dd62e..1fe332bbdb20 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx +++ b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx @@ -1,16 +1,23 @@ import React from "react"; import { DevTools } from "@devtools/shell"; +import { TransportPanel } from "@devtools/transport-panel"; import { useDevToolsScreenViewModel } from "./useDevToolsScreenViewModel"; export default function DevToolsScreen() { - const { config, onClose } = useDevToolsScreenViewModel(); + const { config, onClose, transport, hubUrl, setHubUrl, role } = useDevToolsScreenViewModel(); return (
- + + } + />
); } diff --git a/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts new file mode 100644 index 000000000000..22ba9e554800 --- /dev/null +++ b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts @@ -0,0 +1,23 @@ +import { useSyncExternalStore } from "react"; +import { buildTransport, buildCopyStoreProtocol, combineProtocols } from "@devtools/wire"; +import { sleepingListener } from "~/state-manager/sleepingListener"; +import { useStore } from "LLD/hooks/redux"; + +const HUB_URL = "ws://127.0.0.1:9090"; +const ROLE = "host" as const; + +function buildRelay(store: ReturnType) { + return buildTransport( + { hubUrl: HUB_URL, role: ROLE, id: "lld" }, + combineProtocols(buildCopyStoreProtocol(store, sleepingListener, ROLE)), + ); +} + +let relay: ReturnType | undefined; + +export function useDevToolsRelay() { + const store = useStore(); + if (!relay) relay = buildRelay(store); + const wireState = useSyncExternalStore(relay.subscribe, relay.getState, relay.getState); + return { wire: relay, wireState }; +} diff --git a/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts index ddc6a94da281..8fc88d95609e 100644 --- a/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts +++ b/apps/ledger-live-desktop/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts @@ -2,13 +2,12 @@ import { useCallback, useMemo } from "react"; import { useNavigate } from "react-router"; import { useFeatureFlagsToolProps } from "@devtools/bindings"; import type { DevToolsConfig } from "@devtools/shell"; +import { useDevToolsRelay } from "./useDevToolsRelay"; -export function useDevToolsScreenViewModel(): { - config: DevToolsConfig; - onClose: () => void; -} { +export function useDevToolsScreenViewModel() { const navigate = useNavigate(); const featureFlagsToolProps = useFeatureFlagsToolProps(); + const { wire, wireState } = useDevToolsRelay(); const config: DevToolsConfig = useMemo( () => [{ id: "feature-flags", config: featureFlagsToolProps }], @@ -17,5 +16,12 @@ export function useDevToolsScreenViewModel(): { const onClose = useCallback(() => navigate(-1), [navigate]); - return { config, onClose }; + return { + config, + onClose, + transport: wire.transport, + hubUrl: wireState.hubUrl, + setHubUrl: wire.setHubUrl, + role: wireState.role, + }; } diff --git a/apps/ledger-live-desktop/src/state-manager/configureStore.ts b/apps/ledger-live-desktop/src/state-manager/configureStore.ts index 5973d52bdbc9..2cbb12fc11f8 100644 --- a/apps/ledger-live-desktop/src/state-manager/configureStore.ts +++ b/apps/ledger-live-desktop/src/state-manager/configureStore.ts @@ -21,6 +21,7 @@ import { createIdentitiesSyncMiddleware, pushDevicesApiExtra } from "@domain/api import { canPushDeviceIdsSelector, languageSelector } from "~/renderer/reducers/settings"; import { createFeatureFlagsMiddleware, type PartialFeatures } from "@shared/feature-flags"; import { fetchRemoteFlags as defaultFetchRemoteFlags } from "~/firebase/remoteConfig"; +import { sleepingListener } from "./sleepingListener"; type Props = { state?: State; dbMiddleware?: Middleware; @@ -116,7 +117,8 @@ const customCreateStore = ({ fetchRemoteFlags: fetchRemoteFlags ?? undefined, getAppLanguage: languageSelector, }), - ), + ) + .concat(sleepingListener.middleware), devTools: __DEV__, }); diff --git a/apps/ledger-live-desktop/src/state-manager/sleepingListener.ts b/apps/ledger-live-desktop/src/state-manager/sleepingListener.ts new file mode 100644 index 000000000000..bf5387b00138 --- /dev/null +++ b/apps/ledger-live-desktop/src/state-manager/sleepingListener.ts @@ -0,0 +1,3 @@ +import { createListenerMiddleware } from "@reduxjs/toolkit"; + +export const sleepingListener = createListenerMiddleware(); diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index d0ba2fa94e1a..8027af93885e 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -87,6 +87,8 @@ "@datadog/mobile-react-navigation": "2.8.2", "@devtools/bindings": "workspace:^", "@devtools/shell": "workspace:^", + "@devtools/transport-panel": "workspace:*", + "@devtools/wire": "workspace:*", "@domain/api-altcoins-sentiment": "workspace:^", "@domain/api-currency-fiat": "workspace:^", "@domain/api-currency-token": "workspace:^", diff --git a/apps/ledger-live-mobile/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx b/apps/ledger-live-mobile/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx index e66558741f42..85618c1f3bad 100644 --- a/apps/ledger-live-mobile/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx +++ b/apps/ledger-live-mobile/src/mvvm/features/DevTools/__integrations__/DevToolsScreen.integration.test.tsx @@ -19,6 +19,24 @@ jest.mock( () => ({ useFeatureFlagsToolProps: () => ({ marker: "ff-props" }) }), { virtual: true }, ); +jest.mock("@devtools/transport-panel", () => ({ TransportPanel: () => null }), { virtual: true }); +jest.mock( + "@devtools/wire", + () => { + const wireState = { hubUrl: "ws://127.0.0.1:9090", role: "tool" }; + return { + buildTransport: () => ({ + transport: {}, + subscribe: () => () => {}, + getState: () => wireState, + setHubUrl: jest.fn(), + }), + buildCopyStoreProtocol: () => ({}), + combineProtocols: (...args: unknown[]) => args[0], + }; + }, + { virtual: true }, +); function withBottomInset(children: React.ReactNode) { return ( diff --git a/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx index a88e08195382..b672b8e1acca 100644 --- a/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx +++ b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/index.tsx @@ -1,9 +1,19 @@ import React from "react"; import { DevTools } from "@devtools/shell"; +import { TransportPanel } from "@devtools/transport-panel"; import { useDevToolsScreenViewModel } from "./useDevToolsScreenViewModel"; export default function DevToolsScreen() { - const { config, screenOptions } = useDevToolsScreenViewModel(); + const { config, screenOptions, transport, hubUrl, setHubUrl, role } = + useDevToolsScreenViewModel(); - return ; + return ( + + } + /> + ); } diff --git a/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts new file mode 100644 index 000000000000..36d1c8fc0d6e --- /dev/null +++ b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsRelay.ts @@ -0,0 +1,22 @@ +import { useSyncExternalStore } from "react"; +import { buildTransport, buildCopyStoreProtocol, combineProtocols } from "@devtools/wire"; +import { store } from "~/state-manager/configureStore"; +import { sleepingListener } from "~/state-manager/sleepingListener"; + +const HUB_URL = "ws://127.0.0.1:9090"; +const ROLE = "host" as const; + +function buildRelay() { + return buildTransport( + { hubUrl: HUB_URL, role: ROLE, id: "lwm" }, + combineProtocols(buildCopyStoreProtocol(store, sleepingListener, ROLE)), + ); +} + +let relay: ReturnType | undefined; + +export function useDevToolsRelay() { + if (!relay) relay = buildRelay(); + const wireState = useSyncExternalStore(relay.subscribe, relay.getState, relay.getState); + return { wire: relay, wireState }; +} diff --git a/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts index 66856abe984d..fdda2a92f5d2 100644 --- a/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts +++ b/apps/ledger-live-mobile/src/mvvm/features/DevTools/screens/DevToolsScreen/useDevToolsScreenViewModel.ts @@ -5,22 +5,19 @@ import { useTheme } from "@ledgerhq/lumen-ui-rnative/styles"; import { getStackNavigationConfigV4 } from "LLM/components/Navigation"; import { useFeatureFlagsToolProps } from "@devtools/bindings"; import type { DevToolsConfig } from "@devtools/shell"; +import { useDevToolsRelay } from "./useDevToolsRelay"; -export function useDevToolsScreenViewModel(): { - config: DevToolsConfig; - screenOptions: NativeStackNavigationOptions; -} { +export function useDevToolsScreenViewModel() { const featureFlagsProps = useFeatureFlagsToolProps(); const { theme } = useTheme(); const { bottom } = useSafeAreaInsets(); + const { wire, wireState } = useDevToolsRelay(); const config: DevToolsConfig = useMemo( () => [{ id: "feature-flags", config: featureFlagsProps }], [featureFlagsProps], ); - // DevTools renders its own stack; reuse the app's config and add the bottom - // safe-area inset since DevTools only provides body content. const screenOptions: NativeStackNavigationOptions = useMemo(() => { const navConfig = getStackNavigationConfigV4(theme); return { @@ -29,5 +26,12 @@ export function useDevToolsScreenViewModel(): { }; }, [theme, bottom]); - return { config, screenOptions }; + return { + config, + screenOptions, + transport: wire.transport, + hubUrl: wireState.hubUrl, + setHubUrl: wire.setHubUrl, + role: wireState.role, + }; } diff --git a/apps/ledger-live-mobile/src/state-manager/configureStore.ts b/apps/ledger-live-mobile/src/state-manager/configureStore.ts index bac667603812..b33c19188811 100644 --- a/apps/ledger-live-mobile/src/state-manager/configureStore.ts +++ b/apps/ledger-live-mobile/src/state-manager/configureStore.ts @@ -24,6 +24,7 @@ import { altcoinsSentimentApiExtra } from "@domain/api-altcoins-sentiment"; import { payCardApiExtra } from "@domain/api-pay-card"; import { createFeatureFlagsMiddleware, type PartialFeatures } from "@shared/feature-flags"; import { fetchRemoteFlags } from "~/firebase/remoteConfig"; +import { sleepingListener } from "./sleepingListener"; import { createPkcePairWithExpoCrypto } from "~/helpers/pkce"; // This listenerMiddleware is cross-scope as it is preferable to have one instance per store @@ -102,7 +103,8 @@ export const store = configureStore({ fetchRemoteFlags, getAppLanguage: languageSelector, }), - ), + ) + .concat(sleepingListener.middleware), enhancers: getDefaultEnhancers => { const enhancers = getDefaultEnhancers(); diff --git a/apps/ledger-live-mobile/src/state-manager/sleepingListener.ts b/apps/ledger-live-mobile/src/state-manager/sleepingListener.ts new file mode 100644 index 000000000000..bf5387b00138 --- /dev/null +++ b/apps/ledger-live-mobile/src/state-manager/sleepingListener.ts @@ -0,0 +1,3 @@ +import { createListenerMiddleware } from "@reduxjs/toolkit"; + +export const sleepingListener = createListenerMiddleware(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad1197f12144..5d4667077db3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -824,6 +824,12 @@ importers: '@devtools/shell': specifier: workspace:^ version: link:../../devtools/shell + '@devtools/transport-panel': + specifier: workspace:* + version: link:../../devtools/transport-panel + '@devtools/wire': + specifier: workspace:* + version: link:../../devtools/wire '@domain/api-altcoins-sentiment': specifier: workspace:^ version: link:../../domain/api/altcoins-sentiment @@ -1555,6 +1561,12 @@ importers: '@devtools/shell': specifier: workspace:^ version: link:../../devtools/shell + '@devtools/transport-panel': + specifier: workspace:* + version: link:../../devtools/transport-panel + '@devtools/wire': + specifier: workspace:* + version: link:../../devtools/wire '@domain/api-altcoins-sentiment': specifier: workspace:^ version: link:../../domain/api/altcoins-sentiment @@ -34363,7 +34375,6 @@ packages: engines: {node: '>=0.6.0', teleport: '>=0.2.0'} deprecated: |- You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) qrcode-terminal@0.11.0: @@ -37251,10 +37262,6 @@ packages: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} - undici@6.19.2: - resolution: {integrity: sha512-JfjKqIauur3Q6biAtHJ564e3bWa8VvT+7cSiOJHFbX4Erv6CLGDpg8z+Fmg/1OI/47RA+GI2QZaF48SSaLvyBA==} - engines: {node: '>=18.17'} - undici@6.21.1: resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==} engines: {node: '>=18.17'} @@ -43743,7 +43750,7 @@ snapshots: structured-headers: 0.4.1 tar: 7.4.3 terminal-link: 2.1.1 - undici: 6.19.2 + undici: 6.21.1 wrap-ansi: 7.0.0 ws: 8.21.0 transitivePeerDependencies: @@ -72587,8 +72594,6 @@ snapshots: dependencies: '@fastify/busboy': 2.1.1 - undici@6.19.2: {} - undici@6.21.1: {} undici@7.18.2: {}