Skip to content

Commit 6309480

Browse files
authored
Merge pull request #14363 from LedgerHQ/feat/support-lottie
Feat: support dotlottie
2 parents 74bafc5 + 8d855dd commit 6309480

File tree

14 files changed

+245
-10279
lines changed

14 files changed

+245
-10279
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"live-mobile": minor
3+
---
4+
5+
feat(mobile): Lottie splash screen support (.lottie only, no JSON fallback)
6+
7+
- Add LottieLauncher component and .lottie format (Splashscreen.lottie) for the splash screen
8+
- Use lottie-react-native for animated splash
9+
- Splash screen load time improved from ~4s to ~3.4s

apps/ledger-live-desktop/src/mvvm/components/LoadingOverlay/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export const LoadingOverlay = ({ theme }: { theme: "light" | "dark" }) => {
1313
<Box
1414
position="absolute"
1515
zIndex={1}
16-
height={"100%"}
16+
height="100%"
1717
width="100%"
1818
style={{
1919
backgroundImage: `linear-gradient(180deg, ${backgroundColor}, 80%, rgba(0,0,0,0))`,
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Global Jest mock for .lottie (dotLottie) files.
2+
// See: https://github.com/lottie-react-native/lottie-react-native#setup-jest-for-dotlottie-files
3+
module.exports = "lottie-test-file-stub";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
// Mirror runtime: env vars are strings when set. DETOX undefined = disabled (use "1" in Detox test env).
12
export default {
23
DEVICE_PROXY_URL: null,
34
MOCK: true,
5+
DETOX: undefined,
46
};

apps/ledger-live-mobile/__tests__/jest-setup.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,31 @@ jest.mock("react-native-share", () => ({
7171
default: jest.fn(),
7272
}));
7373

74+
// Global mocks for Lottie and env config (used by LaunchScreen and other components)
75+
jest.mock("lottie-react-native", () => {
76+
const React = require("react");
77+
const { View, Text } = require("react-native");
78+
const MockLottie = ({ source }) =>
79+
React.createElement(
80+
View,
81+
{ testID: "lottie-mock" },
82+
React.createElement(Text, { testID: "lottie-source" }, JSON.stringify(source)),
83+
);
84+
return MockLottie;
85+
});
86+
87+
// Mirror runtime: react-native-config exposes env as strings (e.g. "1"/"true" for DETOX).
88+
// Use undefined so (1) DETOX_ENABLED stays false and (2) truthiness checks (Config.DETOX) are falsy in unit tests.
89+
jest.mock("react-native-config", () => {
90+
const config = { DETOX: undefined };
91+
return {
92+
__esModule: true,
93+
get default() {
94+
return config;
95+
},
96+
};
97+
});
98+
7499
export const mockSimulateBarcodeScanned = jest.fn();
75100
export const mockGetCameraPermissionStatus = jest.fn(() => "granted");
76101

apps/ledger-live-mobile/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ declare module "*.png";
77
declare module "*.jpg";
88
declare module "*.jpeg";
99
declare module "*.webp";
10+
11+
// For Lottie/DotLottie asset (Metro resolves to asset id number)
12+
declare module "*.lottie" {
13+
const value: number;
14+
export default value;
15+
}

apps/ledger-live-mobile/jest.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,7 @@ module.exports = {
109109
"<rootDir>/../../node_modules/.pnpm/@tanstack+react-query@5.28.9_react@19.0.0/node_modules/@tanstack/react-query",
110110
// Redirect to mock for pre-compiled dependencies (like @ledgerhq/native-ui)
111111
"^react-native-worklets$": "<rootDir>/__mocks__/react-native-worklets.js",
112+
// Global mock for .lottie (dotLottie) files
113+
"\\.(lottie)$": "<rootDir>/__mocks__/lottieMock.js",
112114
},
113115
};

apps/ledger-live-mobile/metro.config.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const forcedDependencies = [
3131
const { getDefaultConfig, mergeConfig } = require("@react-native/metro-config");
3232
const removeStarPath = moduleName => moduleName.replace("/*", "");
3333

34+
const defaultConfig = getDefaultConfig(__dirname);
35+
3436
const buildTsAlias = (conf = {}) =>
3537
Object.keys(conf).reduce(
3638
(acc, moduleName) => ({
@@ -115,6 +117,9 @@ const metroConfig = {
115117
unstable_conditionNames: ["require", "react-native", "browser"],
116118
nodeModulesPaths,
117119
resolverMainFields: ["react-native", "browser", "main"],
120+
assetExts: [
121+
...new Set([...(defaultConfig.resolver?.assetExts ?? []), "lottie"]),
122+
],
118123
extraNodeModules: {
119124
...require("node-libs-react-native"),
120125
fs: require.resolve("react-native-level-fs"),
@@ -141,7 +146,7 @@ const metroConfig = {
141146
},
142147
};
143148

144-
module.exports = withRozenite(mergeConfig(getDefaultConfig(__dirname), metroConfig), {
149+
module.exports = withRozenite(mergeConfig(defaultConfig, metroConfig), {
145150
enabled: process.env.WITH_ROZENITE === "true",
146151
include: [
147152
"@rozenite/network-activity-plugin",
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import React from "react";
2+
import { render, screen } from "@tests/test-renderer";
3+
import { Lottie } from "./index";
4+
5+
describe("Lottie", () => {
6+
beforeEach(() => {
7+
const Config = require("react-native-config").default;
8+
Config.DETOX = undefined;
9+
});
10+
11+
it("renders LottieView when source has uri", () => {
12+
render(<Lottie source={{ uri: "file:///test.lottie" }} />);
13+
expect(screen.getByTestId("lottie-source")).toBeOnTheScreen();
14+
expect(screen.getByTestId("lottie-mock")).toBeOnTheScreen();
15+
expect(screen.getByTestId("lottie-source").props.children).toContain("file:///test.lottie");
16+
});
17+
18+
it("renders empty view when source is null", () => {
19+
render(<Lottie source={null} />);
20+
expect(screen.queryByTestId("lottie-mock")).toBeNull();
21+
});
22+
23+
it("renders empty view when source has no uri", () => {
24+
render(<Lottie source={{ width: 1, height: 1 } as unknown as { uri: string }} />);
25+
expect(screen.queryByTestId("lottie-mock")).toBeNull();
26+
});
27+
28+
it("uses default testID for detox view", () => {
29+
const Config = require("react-native-config").default;
30+
Config.DETOX = "1";
31+
render(<Lottie source={{ uri: "file:///test.lottie" }} />);
32+
expect(screen.getByTestId("lottie-detox")).toBeOnTheScreen();
33+
expect(screen.queryByTestId("lottie-mock")).toBeNull();
34+
Config.DETOX = undefined;
35+
});
36+
37+
it("uses custom testID for detox view when provided", () => {
38+
const Config = require("react-native-config").default;
39+
Config.DETOX = "1";
40+
render(<Lottie source={{ uri: "file:///test.lottie" }} testID="splash-lottie" />);
41+
expect(screen.getByTestId("splash-lottie-detox")).toBeOnTheScreen();
42+
expect(screen.queryByTestId("lottie-mock")).toBeNull();
43+
Config.DETOX = undefined;
44+
});
45+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React from "react";
2+
import { View, ViewStyle } from "react-native";
3+
import LottieView from "lottie-react-native";
4+
import Config from "react-native-config";
5+
6+
export type LottieSource = { uri: string } | null;
7+
8+
type LottieProps = {
9+
source: LottieSource;
10+
style?: ViewStyle;
11+
loop?: boolean;
12+
autoPlay?: boolean;
13+
speed?: number;
14+
testID?: string;
15+
};
16+
17+
/**
18+
* Shared Lottie wrapper that handles Detox bypass (empty view in E2E)
19+
* and invalid source. Use this instead of lottie-react-native directly
20+
* so Detox logic is consistent across the app.
21+
*/
22+
export function Lottie({
23+
source,
24+
style,
25+
loop = false,
26+
autoPlay = true,
27+
speed = 1,
28+
testID = "lottie",
29+
}: LottieProps) {
30+
if (Config.DETOX) {
31+
return <View testID={`${testID}-detox`} style={style} />;
32+
}
33+
if (!source?.uri) {
34+
return <View style={style} />;
35+
}
36+
return (
37+
<LottieView
38+
testID={testID}
39+
source={source}
40+
style={style}
41+
loop={loop}
42+
autoPlay={autoPlay}
43+
speed={speed}
44+
/>
45+
);
46+
}

0 commit comments

Comments
 (0)