Skip to content

Commit 34e916a

Browse files
authored
Merge pull request #14107 from LedgerHQ/feat/concordium-onboarding
feat(concordium): desktop onboarding ui
2 parents 21ad873 + 9c93e99 commit 34e916a

File tree

66 files changed

+4573
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+4573
-2
lines changed

.changeset/kind-hotels-attack.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"ledger-live-desktop": minor
3+
"@ledgerhq/live-common": minor
4+
"@ledgerhq/live-countervalues": minor
5+
---
6+
7+
Concordium integration with account onboarding UI

apps/ledger-live-desktop/jest.polyfills.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,20 @@ Object.defineProperties(globalThis, {
4040
MessagePort: { value: MessagePort },
4141
BroadcastChannel: {
4242
value: class {
43-
postMessage() {}
43+
_listeners = {};
44+
postMessage(data) {
45+
const event = { data };
46+
(this._listeners["message"] || []).forEach(fn => fn(event));
47+
}
4448
close() {}
49+
addEventListener(type, fn) {
50+
if (!this._listeners[type]) this._listeners[type] = [];
51+
this._listeners[type].push(fn);
52+
}
53+
removeEventListener(type, fn) {
54+
if (!this._listeners[type]) return;
55+
this._listeners[type] = this._listeners[type].filter(l => l !== fn);
56+
}
4557
onmessage = null;
4658
onmessageerror = null;
4759
},

apps/ledger-live-desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"@ledgerhq/client-ids": "workspace:*",
6363
"@ledgerhq/coin-bitcoin": "workspace:^",
6464
"@ledgerhq/coin-canton": "workspace:^",
65+
"@ledgerhq/coin-concordium": "workspace:^",
6566
"@ledgerhq/coin-cosmos": "workspace:^",
6667
"@ledgerhq/coin-evm": "workspace:^",
6768
"@ledgerhq/coin-filecoin": "workspace:^",

apps/ledger-live-desktop/src/config/urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,11 @@ export const urls = {
223223
aleo: {
224224
viewKeyLearnMore: "", // TODO: waiting for https://ledgerhq.atlassian.net/browse/LIVE-26269
225225
},
226+
concordium: {
227+
learnMore: "https://support.ledger.com/article/Concordium-CCD",
228+
appStore: "https://apps.apple.com/app/concordium-id/id6746754485",
229+
playStore: "https://play.google.com/store/apps/details?id=com.idwallet.app",
230+
},
226231
};
227232

228233
export const vaultSigner = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import { ConcordiumAccount } from "@ledgerhq/coin-concordium/types";
2+
import { createEmptyHistoryCache } from "@ledgerhq/coin-framework/account/balanceHistoryCache";
3+
import {
4+
getDerivationModesForCurrency,
5+
getDerivationScheme,
6+
runDerivationScheme,
7+
} from "@ledgerhq/coin-framework/derivation";
8+
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
9+
import { Account } from "@ledgerhq/types-live";
10+
import BigNumber from "bignumber.js";
11+
import { renderHook } from "tests/testSetup";
12+
import { useConcordiumCreatableAccounts } from "./useConcordiumCreatableAccounts";
13+
14+
const createMockAccount = (
15+
id: string,
16+
currencyId: string,
17+
used: boolean = false,
18+
isOnboarded: boolean = false,
19+
): Account => {
20+
const currency = getCryptoCurrencyById(currencyId);
21+
const derivationMode = getDerivationModesForCurrency(currency)[0];
22+
const scheme = getDerivationScheme({ derivationMode, currency });
23+
const baseAccount: Account = {
24+
id,
25+
type: "Account",
26+
used,
27+
currency,
28+
derivationMode,
29+
index: 0,
30+
freshAddress: "test_address",
31+
freshAddressPath: runDerivationScheme(scheme, currency, {
32+
account: 0,
33+
node: 0,
34+
address: 0,
35+
}),
36+
creationDate: new Date(),
37+
lastSyncDate: new Date(),
38+
balance: new BigNumber(0),
39+
spendableBalance: new BigNumber(0),
40+
seedIdentifier: "test_seed",
41+
blockHeight: 0,
42+
operationsCount: 0,
43+
operations: [],
44+
pendingOperations: [],
45+
balanceHistoryCache: createEmptyHistoryCache(),
46+
swapHistory: [],
47+
subAccounts: [],
48+
};
49+
50+
if (currency.family === "concordium") {
51+
return {
52+
...baseAccount,
53+
concordiumResources: {
54+
isOnboarded,
55+
credId: "test_cred_id",
56+
publicKey: "test_public_key",
57+
identityIndex: 0,
58+
credNumber: 0,
59+
ipIdentity: 0,
60+
},
61+
} as ConcordiumAccount;
62+
}
63+
64+
return baseAccount;
65+
};
66+
67+
describe("useConcordiumCreatableAccounts", () => {
68+
it("should return false when no Concordium accounts are present", () => {
69+
const scannedAccounts: Account[] = [createMockAccount("1", "bitcoin", false)];
70+
const selectedIds = ["1"];
71+
72+
const { result } = renderHook(() =>
73+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
74+
);
75+
76+
expect(result.current.hasConcordiumCreatableAccounts).toBe(false);
77+
expect(result.current.selectedConcordiumAccounts).toEqual([]);
78+
});
79+
80+
it("should return false when Concordium accounts are onboarded", () => {
81+
const scannedAccounts: Account[] = [createMockAccount("1", "concordium", false, true)];
82+
const selectedIds = ["1"];
83+
84+
const { result } = renderHook(() =>
85+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
86+
);
87+
88+
expect(result.current.hasConcordiumCreatableAccounts).toBe(false);
89+
expect(result.current.selectedConcordiumAccounts).toHaveLength(1);
90+
});
91+
92+
it("should return false when Concordium accounts are not selected", () => {
93+
const scannedAccounts: Account[] = [createMockAccount("1", "concordium", false)];
94+
const selectedIds: string[] = [];
95+
96+
const { result } = renderHook(() =>
97+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
98+
);
99+
100+
expect(result.current.hasConcordiumCreatableAccounts).toBe(false);
101+
expect(result.current.selectedConcordiumAccounts).toEqual([]);
102+
});
103+
104+
it("should return true when Concordium creatable accounts are selected", () => {
105+
const scannedAccounts: Account[] = [createMockAccount("1", "concordium", false, false)];
106+
const selectedIds = ["1"];
107+
108+
const { result } = renderHook(() =>
109+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
110+
);
111+
112+
expect(result.current.hasConcordiumCreatableAccounts).toBe(true);
113+
expect(result.current.selectedConcordiumAccounts).toHaveLength(1);
114+
expect(result.current.selectedConcordiumAccounts.map(a => a.id)).toEqual(["1"]);
115+
});
116+
117+
it("should handle empty scanned accounts array", () => {
118+
const scannedAccounts: Account[] = [];
119+
const selectedIds: string[] = [];
120+
121+
const { result } = renderHook(() =>
122+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
123+
);
124+
125+
expect(result.current.hasConcordiumCreatableAccounts).toBe(false);
126+
expect(result.current.selectedConcordiumAccounts).toEqual([]);
127+
});
128+
129+
it("should update results when props change", () => {
130+
const initialScannedAccounts: Account[] = [createMockAccount("1", "concordium", false, false)];
131+
const initialSelectedIds = ["1"];
132+
133+
const { result, rerender } = renderHook(
134+
({ scannedAccounts, selectedIds }) =>
135+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
136+
{
137+
initialProps: {
138+
scannedAccounts: initialScannedAccounts,
139+
selectedIds: initialSelectedIds,
140+
},
141+
},
142+
);
143+
144+
expect(result.current.hasConcordiumCreatableAccounts).toBe(true);
145+
146+
// Update to deselect the account
147+
rerender({
148+
scannedAccounts: initialScannedAccounts,
149+
selectedIds: [],
150+
});
151+
152+
expect(result.current.hasConcordiumCreatableAccounts).toBe(false);
153+
expect(result.current.selectedConcordiumAccounts).toEqual([]);
154+
});
155+
156+
it("should include both onboarded and creatable accounts in selectedConcordiumAccounts", () => {
157+
const scannedAccounts: Account[] = [
158+
createMockAccount("1", "concordium", false, true), // onboarded
159+
createMockAccount("2", "concordium", false, false), // creatable
160+
];
161+
const selectedIds = ["1", "2"];
162+
163+
const { result } = renderHook(() =>
164+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
165+
);
166+
167+
expect(result.current.hasConcordiumCreatableAccounts).toBe(true);
168+
expect(result.current.selectedConcordiumAccounts).toHaveLength(2);
169+
});
170+
171+
it("should filter out non-Concordium accounts from selectedConcordiumAccounts", () => {
172+
const scannedAccounts: Account[] = [
173+
createMockAccount("1", "concordium", false, false),
174+
createMockAccount("2", "bitcoin", false),
175+
];
176+
const selectedIds = ["1", "2"];
177+
178+
const { result } = renderHook(() =>
179+
useConcordiumCreatableAccounts({ scannedAccounts, selectedIds }),
180+
);
181+
182+
expect(result.current.hasConcordiumCreatableAccounts).toBe(true);
183+
expect(result.current.selectedConcordiumAccounts).toHaveLength(1);
184+
expect(result.current.selectedConcordiumAccounts[0].id).toBe("1");
185+
});
186+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { isConcordiumAccount } from "@ledgerhq/coin-concordium/bridge/serialization";
2+
import { Account } from "@ledgerhq/types-live";
3+
import { useMemo } from "react";
4+
5+
export interface UseConcordiumCreatableAccountsProps {
6+
scannedAccounts: Account[];
7+
selectedIds: string[];
8+
}
9+
10+
export interface UseConcordiumCreatableAccountsReturn {
11+
hasConcordiumCreatableAccounts: boolean;
12+
selectedConcordiumAccounts: Account[];
13+
}
14+
15+
/**
16+
* Hook to check if there are any Concordium creatable accounts among the scanned accounts
17+
* and return all selected Concordium accounts (both creatable and importable).
18+
*
19+
* @param scannedAccounts - Array of all scanned accounts
20+
* @param selectedIds - Array of selected account IDs
21+
* @returns Object containing whether there are creatable accounts and all selected Concordium accounts
22+
*/
23+
export function useConcordiumCreatableAccounts({
24+
scannedAccounts,
25+
selectedIds,
26+
}: UseConcordiumCreatableAccountsProps): UseConcordiumCreatableAccountsReturn {
27+
return useMemo(() => {
28+
const selectedIdsSet = new Set(selectedIds);
29+
const selectedConcordiumAccounts: Account[] = [];
30+
let hasCreatableAccounts = false;
31+
32+
for (const account of scannedAccounts) {
33+
if (!isConcordiumAccount(account)) continue;
34+
35+
if (!selectedIdsSet.has(account.id)) continue;
36+
37+
selectedConcordiumAccounts.push(account);
38+
39+
if (account.concordiumResources.isOnboarded) continue;
40+
41+
hasCreatableAccounts = true;
42+
}
43+
44+
return {
45+
hasConcordiumCreatableAccounts: hasCreatableAccounts,
46+
selectedConcordiumAccounts,
47+
};
48+
}, [scannedAccounts, selectedIds]);
49+
}

apps/ledger-live-desktop/src/mvvm/features/AddAccountDrawer/screens/ScanAccounts/useScanAccounts.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
getUnimportedAccounts,
2727
} from "./utils/processAccounts";
2828
import { useCantonCreatableAccounts } from "./hooks/useCantonCreatableAccounts";
29+
import { useConcordiumCreatableAccounts } from "./hooks/concordium/useConcordiumCreatableAccounts";
2930

3031
const selectImportable = (importable: Account[]) => (selected: string[]) => {
3132
const importableIds = importable.map(a => a.id);
@@ -168,6 +169,12 @@ export function useScanAccounts({
168169
selectedIds: filteredSelectedIds,
169170
});
170171

172+
const { hasConcordiumCreatableAccounts, selectedConcordiumAccounts } =
173+
useConcordiumCreatableAccounts({
174+
scannedAccounts,
175+
selectedIds: filteredSelectedIds,
176+
});
177+
171178
const handleConfirm = useCallback(() => {
172179
trackAddAccountEvent(ADD_ACCOUNT_EVENTS_NAME.ADD_ACCOUNT_BUTTON_CLICKED, {
173180
button: "Confirm",
@@ -191,6 +198,20 @@ export function useScanAccounts({
191198
return;
192199
}
193200

201+
if (hasConcordiumCreatableAccounts) {
202+
setDrawer();
203+
204+
dispatch(
205+
openModal("MODAL_CONCORDIUM_ONBOARD_ACCOUNT", {
206+
currency,
207+
selectedAccounts: selectedConcordiumAccounts,
208+
editedNames: {},
209+
}),
210+
);
211+
212+
return;
213+
}
214+
194215
if (accountsToImport.length > 0) {
195216
setHasImportedAccounts(true);
196217
}
@@ -217,6 +238,8 @@ export function useScanAccounts({
217238
device,
218239
hasCantonCreatableAccounts,
219240
selectedCantonCreatableAccounts,
241+
hasConcordiumCreatableAccounts,
242+
selectedConcordiumAccounts,
220243
filteredSelectedIds,
221244
scannedAccounts,
222245
deferAccountAddition,

0 commit comments

Comments
 (0)