Skip to content

Commit 70249dc

Browse files
Merge pull request #14897 from LedgerHQ/feat/live-27020/x-ledger-client-v4-ux
feat(live-27020): add wallet40 feature flag into x-ledger-client-v4-ux swap header
2 parents d3b0289 + 3338a91 commit 70249dc

File tree

10 files changed

+184
-4
lines changed

10 files changed

+184
-4
lines changed

.changeset/wise-crabs-know.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+
"live-mobile": minor
4+
"@ledgerhq/live-common": minor
5+
---
6+
7+
swap: add wallet40 feature flag into x-ledger-client-v4-ux swap header

apps/ledger-live-desktop/src/renderer/components/WebPTXPlayer/CustomHandlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,14 @@ import { objectToURLSearchParams } from "@ledgerhq/live-common/wallet-api/helper
3636
import { useRemoteLiveAppContext } from "@ledgerhq/live-common/platform/providers/RemoteLiveAppProvider/index";
3737
import { useLocalLiveAppContext } from "@ledgerhq/live-common/wallet-api/LocalLiveAppProvider/index";
3838
import { usesEncodedAccountIdFormat } from "@ledgerhq/live-common/wallet-api/utils/deriveAccountIdForManifest";
39+
import { useWalletFeaturesConfig } from "@ledgerhq/live-common/featureFlags/index";
3940

4041
export function usePTXCustomHandlers(manifest: WebviewProps["manifest"], accounts: AccountLike[]) {
4142
const dispatch = useDispatch();
4243
const { setDrawer } = React.useContext(context);
4344
const { getRouteToPlatformApp } = useStake();
4445
const navigate = useNavigate();
46+
const { isEnabled } = useWalletFeaturesConfig("desktop");
4547
const walletState = useSelector(walletSelector);
4648
const { state: liveAppRegistryState } = useRemoteLiveAppContext();
4749
const { state: localLiveAppState } = useLocalLiveAppContext();
@@ -84,6 +86,7 @@ export function usePTXCustomHandlers(manifest: WebviewProps["manifest"], account
8486
),
8587
[],
8688
);
89+
const flags = useMemo(() => ({ wallet40Ux: isEnabled }), [isEnabled]);
8790

8891
const getAccount = useCallback(
8992
async (accountId: string): Promise<AccountLike | null> => {
@@ -159,6 +162,7 @@ export function usePTXCustomHandlers(manifest: WebviewProps["manifest"], account
159162
accounts,
160163
tracking,
161164
manifest,
165+
flags,
162166
uiHooks: {
163167
"custom.exchange.start": ({ exchangeParams, onSuccess, onCancel }) => {
164168
dispatch(
@@ -359,6 +363,7 @@ export function usePTXCustomHandlers(manifest: WebviewProps["manifest"], account
359363
accounts,
360364
tracking,
361365
manifest,
366+
flags,
362367
dispatch,
363368
setDrawer,
364369
navigate,

apps/ledger-live-mobile/src/components/WebPTXPlayer/CustomHandlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { usesEncodedAccountIdFormat } from "@ledgerhq/live-common/wallet-api/uti
4141
import { updateAccountWithUpdater } from "~/actions/accounts";
4242
import { useDispatch } from "~/context/hooks";
4343
import { ExchangeSwap } from "@ledgerhq/live-common/exchange/swap/types";
44+
import { useWalletFeaturesConfig } from "@ledgerhq/live-common/featureFlags/index";
4445

4546
const DrawerClosedError = createCustomErrorClass("DrawerClosedError");
4647
const drawerClosedError = new DrawerClosedError("User closed the drawer");
@@ -69,6 +70,8 @@ export function useCustomExchangeHandlers({
6970
const deviceRef = useRef<Device | undefined>(undefined);
7071
const syncAccountById = useSyncAccountById();
7172
const dispatch = useDispatch();
73+
const { isEnabled } = useWalletFeaturesConfig("mobile");
74+
const flags = useMemo(() => ({ wallet40Ux: isEnabled }), [isEnabled]);
7275
const { state: liveAppRegistryState } = useRemoteLiveAppContext();
7376
const { state: localLiveAppState } = useLocalLiveAppContext();
7477

@@ -291,6 +294,7 @@ export function useCustomExchangeHandlers({
291294
accounts,
292295
tracking,
293296
manifest,
297+
flags,
294298
uiHooks: {
295299
"custom.exchange.start": ({ exchangeParams, onSuccess, onCancel }) => {
296300
const promiseId = `start-${Date.now()}`;
@@ -475,6 +479,7 @@ export function useCustomExchangeHandlers({
475479
onCompleteError,
476480
onCompleteResult,
477481
handleLoaderDrawer,
482+
flags,
478483
sendAppReady,
479484
syncAccountById,
480485
tracking,
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import axios from "axios";
2+
import BigNumber from "bignumber.js";
3+
import { retrieveSwapPayload } from "../actions";
4+
5+
jest.mock("axios");
6+
jest.mock("../../..", () => ({
7+
getSwapAPIBaseURL: jest.fn(() => "https://swap.ledger.com/v5"),
8+
}));
9+
10+
describe("retrieveSwapPayload", () => {
11+
const post = jest.fn();
12+
const mockedAxios = jest.mocked(axios);
13+
14+
const payloadData = {
15+
provider: "changelly",
16+
deviceTransactionId: "tx-id",
17+
fromAccountAddress: "from-address",
18+
toAccountAddress: "to-address",
19+
fromAccountCurrency: "bitcoin",
20+
toAccountCurrency: "ethereum",
21+
amount: "1",
22+
amountInAtomicUnit: new BigNumber("100000000"),
23+
};
24+
25+
beforeEach(() => {
26+
jest.clearAllMocks();
27+
mockedAxios.create.mockReturnValue({ post } as never);
28+
post.mockResolvedValue({
29+
data: {
30+
binaryPayload: "binary",
31+
signature: "signature",
32+
payinAddress: "payin",
33+
swapId: "swap-id",
34+
},
35+
});
36+
});
37+
38+
it("sends x-ledger-client-v4-ux header when wallet40Ux flag is true", async () => {
39+
await retrieveSwapPayload({ ...payloadData, flags: { wallet40Ux: true } });
40+
41+
expect(post).toHaveBeenCalledWith(
42+
"https://swap.ledger.com/v5/swap",
43+
expect.any(Object),
44+
expect.objectContaining({
45+
headers: {
46+
"x-ledger-client-v4-ux": "true",
47+
},
48+
}),
49+
);
50+
});
51+
52+
it("does not send request headers when wallet40Ux flag is missing", async () => {
53+
await retrieveSwapPayload(payloadData);
54+
55+
expect(post).toHaveBeenCalledWith(
56+
"https://swap.ledger.com/v5/swap",
57+
expect.any(Object),
58+
undefined,
59+
);
60+
});
61+
62+
it("does not send request headers when wallet40Ux flag is false", async () => {
63+
await retrieveSwapPayload({ ...payloadData, flags: { wallet40Ux: false } });
64+
65+
expect(post).toHaveBeenCalledWith(
66+
"https://swap.ledger.com/v5/swap",
67+
expect.any(Object),
68+
undefined,
69+
);
70+
});
71+
});

libs/ledger-live-common/src/exchange/swap/api/v5/actions.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ export async function retrieveSwapPayload(
2323
rateId: data.quoteId,
2424
};
2525

26-
const res = await swapAxiosClient.post(`${SWAP_API_BASE}/swap`, request);
26+
const requestConfig = data.flags?.wallet40Ux
27+
? { headers: { "x-ledger-client-v4-ux": "true" } }
28+
: undefined;
29+
const res = await swapAxiosClient.post(`${SWAP_API_BASE}/swap`, request, requestConfig);
2730

2831
return {
2932
binaryPayload: res.data?.binaryPayload,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import network from "@ledgerhq/live-network";
2+
import { postSwapAccepted, postSwapCancelled } from "./postSwapState";
3+
4+
jest.mock("@ledgerhq/live-network", () => ({
5+
__esModule: true,
6+
default: jest.fn().mockResolvedValue({}),
7+
}));
8+
9+
jest.mock("./utils/isIntegrationTestEnv", () => ({
10+
isIntegrationTestEnv: () => false,
11+
}));
12+
13+
const mockedNetwork = jest.mocked(network);
14+
15+
describe("postSwapState wallet40 header", () => {
16+
beforeEach(() => {
17+
jest.clearAllMocks();
18+
});
19+
20+
it("adds x-ledger-client-v4-ux=true when wallet40Ux flag is true", async () => {
21+
await postSwapAccepted({
22+
provider: "changelly",
23+
swapId: "swap-id",
24+
transactionId: "tx-id",
25+
flags: { wallet40Ux: true },
26+
});
27+
28+
const request = mockedNetwork.mock.calls[0][0] as { headers?: Record<string, string> };
29+
expect(request.headers?.["x-ledger-client-v4-ux"]).toBe("true");
30+
});
31+
32+
it("adds x-ledger-client-v4-ux=true for cancelled call when wallet40Ux flag is true", async () => {
33+
await postSwapCancelled({
34+
provider: "changelly",
35+
swapId: "swap-id",
36+
swapAppVersion: "4.0.0",
37+
flags: { wallet40Ux: true },
38+
});
39+
40+
const request = mockedNetwork.mock.calls[0][0] as { headers?: Record<string, string> };
41+
expect(request.headers?.["x-ledger-client-v4-ux"]).toBe("true");
42+
expect(request.headers?.["x-swap-app-version"]).toBe("4.0.0");
43+
});
44+
45+
it("does not add x-ledger-client-v4-ux when wallet40Ux flag is missing", async () => {
46+
await postSwapAccepted({
47+
provider: "changelly",
48+
swapId: "swap-id",
49+
transactionId: "tx-id",
50+
});
51+
52+
const request = mockedNetwork.mock.calls[0][0] as { headers?: Record<string, string> };
53+
expect(request.headers?.["x-ledger-client-v4-ux"]).toBeUndefined();
54+
});
55+
56+
it("does not add x-ledger-client-v4-ux when wallet40Ux flag is false", async () => {
57+
await postSwapAccepted({
58+
provider: "changelly",
59+
swapId: "swap-id",
60+
transactionId: "tx-id",
61+
flags: { wallet40Ux: false },
62+
});
63+
64+
const request = mockedNetwork.mock.calls[0][0] as { headers?: Record<string, string> };
65+
expect(request.headers?.["x-ledger-client-v4-ux"]).toBeUndefined();
66+
});
67+
});

libs/ledger-live-common/src/exchange/swap/postSwapState.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import crypto from "crypto";
22
import network from "@ledgerhq/live-network";
33
import { mockPostSwapAccepted, mockPostSwapCancelled } from "./mock";
4-
import type { PostSwapAccepted, PostSwapCancelled } from "./types";
4+
import type { PostSwapAccepted, PostSwapCancelled, FeatureFlags } from "./types";
55
import { isIntegrationTestEnv } from "./utils/isIntegrationTestEnv";
66
import { getSwapAPIBaseURL, getSwapUserIP } from ".";
77

@@ -47,6 +47,9 @@ function createSwapIntentHashes({
4747
return { swapIntentWithProvider, swapIntentWithoutProvider };
4848
}
4949

50+
const getWallet40Header = (flags?: FeatureFlags): Record<string, string> =>
51+
flags?.wallet40Ux ? { "x-ledger-client-v4-ux": "true" } : {};
52+
5053
export const postSwapAccepted: PostSwapAccepted = async ({
5154
provider,
5255
swapId = "",
@@ -55,6 +58,7 @@ export const postSwapAccepted: PostSwapAccepted = async ({
5558
fromAccountAddress,
5659
toAccountAddress,
5760
fromAmount,
61+
flags,
5862
...rest
5963
}) => {
6064
if (isIntegrationTestEnv())
@@ -80,6 +84,7 @@ export const postSwapAccepted: PostSwapAccepted = async ({
8084
const headers = {
8185
...(ipHeader || {}),
8286
...(swapAppVersion ? { "x-swap-app-version": swapAppVersion } : {}),
87+
...getWallet40Header(flags),
8388
};
8489

8590
await network({
@@ -107,6 +112,7 @@ export const postSwapCancelled: PostSwapCancelled = async ({
107112
refundAddress,
108113
payoutAddress,
109114
data,
115+
flags,
110116
...rest
111117
}) => {
112118
if (isIntegrationTestEnv()) return mockPostSwapCancelled({ provider, swapId, ...rest });
@@ -135,6 +141,7 @@ export const postSwapCancelled: PostSwapCancelled = async ({
135141
const headers = {
136142
...(ipHeader || {}),
137143
...(swapAppVersion ? { "x-swap-app-version": swapAppVersion } : {}),
144+
...getWallet40Header(flags),
138145
};
139146

140147
const requestData = {

libs/ledger-live-common/src/exchange/swap/setBroadcastTransaction.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { getEnv } from "@ledgerhq/live-env";
22
import { Operation } from "@ledgerhq/types-live";
33
import { postSwapAccepted, postSwapCancelled } from "./index";
44
import { DeviceModelId } from "@ledgerhq/devices";
5-
import { TradeMethod } from "./types";
5+
import { FeatureFlags, TradeMethod } from "./types";
66

77
export const setBroadcastTransaction = ({
88
result,
@@ -15,6 +15,7 @@ export const setBroadcastTransaction = ({
1515
fromAccountAddress,
1616
toAccountAddress,
1717
fromAmount,
18+
flags,
1819
}: {
1920
result: { operation: Operation | string; swapId: string };
2021
provider: string;
@@ -26,6 +27,7 @@ export const setBroadcastTransaction = ({
2627
fromAccountAddress?: string;
2728
toAccountAddress?: string;
2829
fromAmount?: string;
30+
flags?: FeatureFlags;
2931
}) => {
3032
const { operation, swapId } = result;
3133

@@ -48,6 +50,7 @@ export const setBroadcastTransaction = ({
4850
fromAccountAddress,
4951
toAccountAddress,
5052
fromAmount,
53+
flags,
5154
});
5255
} else {
5356
postSwapAccepted({
@@ -62,6 +65,7 @@ export const setBroadcastTransaction = ({
6265
fromAccountAddress,
6366
toAccountAddress,
6467
fromAmount,
68+
flags,
6569
});
6670
}
6771
};

libs/ledger-live-common/src/exchange/swap/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ export type SwapStatus = {
183183
// -----
184184
// Related to Swap state API call (accepted or cancelled)
185185

186+
export type FeatureFlags = {
187+
wallet40Ux?: boolean;
188+
};
189+
186190
type SwapStateRequest = {
187191
provider: string;
188192
swapId: string;
@@ -203,6 +207,7 @@ type SwapStateRequest = {
203207
refundAddress?: string;
204208
payoutAddress?: string;
205209
sponsored?: boolean;
210+
flags?: FeatureFlags;
206211
}>;
207212

208213
export type SwapStateAcceptedRequest = SwapStateRequest & {
@@ -326,6 +331,7 @@ export type SwapPayloadRequestData = {
326331
amountInAtomicUnit: BigNumber;
327332
quoteId?: string;
328333
toNewTokenId?: string;
334+
flags?: FeatureFlags;
329335
};
330336
export type SwapPayloadResponse = {
331337
binaryPayload: string;

libs/ledger-live-common/src/wallet-api/Exchange/server.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { BigNumber } from "bignumber.js";
3333
import { getAccountBridge } from "../../bridge";
3434
import { retrieveSwapPayload } from "../../exchange/swap/api/v5/actions";
3535
import { transactionStrategy } from "../../exchange/swap/transactionStrategies";
36-
import { ExchangeSwap } from "../../exchange/swap/types";
36+
import { ExchangeSwap, FeatureFlags } from "../../exchange/swap/types";
3737
import { Exchange } from "../../exchange/types";
3838
import { Transaction } from "../../generated/types";
3939
import {
@@ -146,6 +146,7 @@ export const handlers = ({
146146
accounts,
147147
tracking,
148148
manifest,
149+
flags,
149150
uiHooks: {
150151
"custom.exchange.start": uiExchangeStart,
151152
"custom.exchange.complete": uiExchangeComplete,
@@ -157,6 +158,7 @@ export const handlers = ({
157158
accounts: AccountLike[];
158159
tracking: TrackingAPI;
159160
manifest: AppManifest;
161+
flags?: FeatureFlags;
160162
uiHooks: ExchangeUiHooks;
161163
}) =>
162164
({
@@ -496,6 +498,7 @@ export const handlers = ({
496498
amountInAtomicUnit: fromAmountAtomic,
497499
quoteId,
498500
toNewTokenId,
501+
flags,
499502
}).catch((error: Error) => {
500503
const wrappedError = createStepError({
501504
error: get(error, "response.data.error", error),
@@ -621,6 +624,7 @@ export const handlers = ({
621624
fromAccountAddress,
622625
toAccountAddress,
623626
fromAmount,
627+
flags,
624628
});
625629

626630
resolve({ operationHash, swapId });
@@ -661,6 +665,7 @@ export const handlers = ({
661665
data: (transaction as EvmTransaction).data
662666
? `0x${padHexString((transaction as EvmTransaction).data?.toString("hex") || "")}`
663667
: "0x",
668+
flags,
664669
});
665670

666671
reject(completeExchangeError);

0 commit comments

Comments
 (0)