Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

πŸ” feat(lld): Track users creating or joining a Ledger keyring on LLD #8283

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/neat-paws-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ledger-live-desktop": minor
---

Track users creating, joining, and leaving Ledger keyrings on LLD
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* @jest-environment jsdom
*/
import { renderHook, waitFor } from "@testing-library/react";
import { TrustchainResultType } from "@ledgerhq/ledger-key-ring-protocol/types";
import { DeviceModelId } from "@ledgerhq/types-devices";
import { Flow, Step } from "~/renderer/reducers/walletSync";
import { useAddMember } from "../hooks/useAddMember";
import {
TrustchainAlreadyInitialized,
TrustchainAlreadyInitializedWithOtherSeed,
} from "@ledgerhq/ledger-key-ring-protocol/errors";

const device = { deviceId: "", modelId: DeviceModelId.stax, wired: true };
const trustchain = {
rootId: "trustchainId",
walletSyncEncryptionKey: "0x123",
applicationPath: "m/0'/16'/1'",
};

const Mocks = {
sdk: { getOrCreateTrustchain: jest.fn() },
memberCredentialsSelector: jest.fn(),
setTrustchain: jest.fn(),
setFlow: jest.fn(),
track: jest.fn(),
};

describe("useAddMember", () => {
it("should create a new trustchain", async () => {
Mocks.sdk.getOrCreateTrustchain.mockResolvedValue({
trustchain,
type: TrustchainResultType.created,
});
const { result } = renderHook(() => useAddMember({ device }));

await waitFor(() => expect(Mocks.sdk.getOrCreateTrustchain).toHaveBeenCalled());

expect(result.current.error).toBeNull();
expect(Mocks.setTrustchain).toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).toHaveBeenCalledWith({
flow: Flow.Activation,
step: Step.ActivationLoading,
nextStep: Step.ActivationFinal,
hasTrustchainBeenCreated: true,
});
expect(Mocks.track).toHaveBeenCalledTimes(1);
expect(Mocks.track).toHaveBeenCalledWith("ledgersync_activated");
});

it("should get an existing trustchain", async () => {
Mocks.sdk.getOrCreateTrustchain.mockResolvedValue({
trustchain,
type: TrustchainResultType.restored,
});
const { result } = renderHook(() => useAddMember({ device }));

await waitFor(() => expect(Mocks.sdk.getOrCreateTrustchain).toHaveBeenCalled());

expect(result.current.error).toBeNull();
expect(Mocks.setTrustchain).toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).toHaveBeenCalledWith({
flow: Flow.Activation,
step: Step.ActivationLoading,
nextStep: Step.SynchronizationFinal,
hasTrustchainBeenCreated: false,
});
expect(Mocks.track).toHaveBeenCalledTimes(1);
expect(Mocks.track).toHaveBeenCalledWith("ledgersync_activated");
});

it("should handle missing device", async () => {
const { result } = renderHook(() => useAddMember({ device: null }));

expect(result.current.error).toBeNull();
expect(Mocks.setTrustchain).not.toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).toHaveBeenCalledWith({
flow: Flow.Activation,
step: Step.DeviceAction,
});
expect(Mocks.track).not.toHaveBeenCalled();
});

it("should handle already initialized trustchain", async () => {
Mocks.sdk.getOrCreateTrustchain.mockRejectedValue(new TrustchainAlreadyInitialized());
const { result } = renderHook(() => useAddMember({ device }));

await waitFor(() => expect(Mocks.sdk.getOrCreateTrustchain).toHaveBeenCalled());

expect(result.current.error).toBeNull();
expect(Mocks.setTrustchain).not.toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).toHaveBeenCalledWith({
flow: Flow.Synchronize,
step: Step.AlreadySecuredSameSeed,
});
expect(Mocks.track).not.toHaveBeenCalled();
});

it("should handle trustchain initialized with other seed", async () => {
Mocks.sdk.getOrCreateTrustchain.mockRejectedValue(
new TrustchainAlreadyInitializedWithOtherSeed(),
);
const { result } = renderHook(() => useAddMember({ device }));

await waitFor(() => expect(Mocks.sdk.getOrCreateTrustchain).toHaveBeenCalled());

expect(result.current.error).toBeNull();
expect(Mocks.setTrustchain).not.toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).toHaveBeenCalledWith({
flow: Flow.Synchronize,
step: Step.AlreadySecuredOtherSeed,
});
expect(Mocks.track).not.toHaveBeenCalled();
});

it("should handle missing member credentials", async () => {
Mocks.memberCredentialsSelector.mockReturnValue(null);
const { result } = renderHook(() => useAddMember({ device }));

expect(result.current.error).toBeInstanceOf(Error);
expect(Mocks.sdk.getOrCreateTrustchain).not.toHaveBeenCalled();
expect(Mocks.setTrustchain).not.toHaveBeenCalledWith(trustchain);
expect(Mocks.setFlow).not.toHaveBeenCalled();
expect(Mocks.track).not.toHaveBeenCalled();
});

it("should handle other sdk errors", async () => {
Mocks.sdk.getOrCreateTrustchain.mockRejectedValue(new Error("Random error"));
const { result } = renderHook(() => useAddMember({ device }));

await waitFor(() => expect(Mocks.sdk.getOrCreateTrustchain).toHaveBeenCalled());

expect(result.current.error).toBeInstanceOf(Error);
expect((result.current.error as Error).message).toBe("Random error");
expect(Mocks.setTrustchain).not.toHaveBeenCalled();
expect(Mocks.setFlow).not.toHaveBeenCalled();
expect(Mocks.track).not.toHaveBeenCalled();
});

beforeEach(() => {
jest.clearAllMocks();
Mocks.memberCredentialsSelector.mockReturnValue({
pubkey: "pubkey",
privatekey: "privatekey",
});
});
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i would move this bloc to the top before using the variables

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean the beforeEach ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no the variable declarations , mock setups starting from line 134

jest.mock("react-redux", () => {
const dispatch = jest.fn();
return {
useDispatch: () => dispatch,
useSelector: (selector: () => unknown) => selector(),
};
});

jest.mock("@ledgerhq/ledger-key-ring-protocol/store", () => ({
memberCredentialsSelector: () => Mocks.memberCredentialsSelector(),
trustchainSelector: () => null,
setTrustchain: (trustchain: unknown) => Mocks.setTrustchain(trustchain),
}));

jest.mock("~/renderer/actions/walletSync", () => ({
setFlow: (flow: unknown) => Mocks.setFlow(flow),
}));

jest.mock("~/renderer/analytics/segment", () => ({
track: (event: string) => Mocks.track(event),
}));

jest.mock("../hooks/useTrustchainSdk", () => ({
useTrustchainSdk: () => Mocks.sdk,
}));
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
TrustchainAlreadyInitialized,
TrustchainAlreadyInitializedWithOtherSeed,
} from "@ledgerhq/ledger-key-ring-protocol/errors";
import { track } from "~/renderer/analytics/segment";

export function useAddMember({ device }: { device: Device | null }) {
const dispatch = useDispatch();
Expand All @@ -31,6 +32,7 @@ export function useAddMember({ device }: { device: Device | null }) {
const transitionToNextScreen = useCallback(
(trustchainResult: TrustchainResult) => {
dispatch(setTrustchain(trustchainResult.trustchain));
track("ledgersync_activated");
dispatch(
setFlow({
flow: Flow.Activation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Flow, Step } from "~/renderer/reducers/walletSync";
import { QueryKey } from "./type.hooks";
import { useCloudSyncSDK } from "./useWatchWalletSync";
import { walletSyncUpdate } from "@ledgerhq/live-wallet/store";
import { track } from "~/renderer/analytics/segment";

export function useDestroyTrustchain() {
const dispatch = useDispatch();
Expand All @@ -31,6 +32,7 @@ export function useDestroyTrustchain() {
onSuccess: () => {
dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeleted }));
dispatch(resetTrustchainStore());
track("ledgersync_deactivated");
dispatch(walletSyncUpdate(null, 0));
},
onError: () => dispatch(setFlow({ flow: Flow.ManageBackup, step: Step.BackupDeletionError })),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { setTrustchain, resetTrustchainStore } from "@ledgerhq/ledger-key-ring-protocol/store";
import { TrustchainEjected } from "@ledgerhq/ledger-key-ring-protocol/errors";
import { log } from "@ledgerhq/logs";
import { track } from "~/renderer/analytics/segment";

export function useOnTrustchainRefreshNeeded(
trustchainSdk: TrustchainSDK,
Expand All @@ -24,6 +25,7 @@ export function useOnTrustchainRefreshNeeded(
} catch (e) {
if (e instanceof TrustchainEjected) {
dispatch(resetTrustchainStore());
track("ledgersync_deactivated");
}
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { useTrustchainSdk } from "./useTrustchainSdk";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import getWalletSyncEnvironmentParams from "@ledgerhq/live-common/walletSync/getEnvironmentParams";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { track } from "~/renderer/analytics/segment";
import { QueryKey } from "./type.hooks";
import { useInstanceName } from "./useInstanceName";

Expand Down Expand Up @@ -84,6 +85,7 @@ export function useQRCode() {
onSuccess(newTrustchain) {
if (newTrustchain) {
dispatch(setTrustchain(newTrustchain));
if (!trustchain) track("ledgersync_activated");
}
dispatch(
setFlow({
Expand Down
Loading