[LWM] feat(lazy-onboarding): add portfolio device banner - #20290
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces a new cross-platform “lazy onboarding” device-discovery banner flow and wires it into Ledger Wallet Mobile’s Portfolio banners, backed by a feature flag that includes a validated Ledger Shop link and session-only dismissal state.
Changes:
- Added
lazyOnboardingBannerfeature-flag params validation, including an allowlisted Ledger Shop HTTPS link with a default fallback. - Created
@features/flow-lazy-onboarding-bannerfor eligibility decisioning, session dismissal state, and Shop URL construction with UTM attribution. - Implemented the Mobile MVVM banner (Lumen
MediaBanner) and made it highest-priority in the Portfolio banners stack, with unit + integration tests.
Reviewed changes
Copilot reviewed 36 out of 38 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| shared/feature-flags/src/lazyOnboardingBanner.ts | Adds default Shop link constant + allowlist validator helper. |
| shared/feature-flags/src/index.ts | Re-exports the new lazy onboarding banner helpers. |
| shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts | Extends flag schema with validated link param + defaults. |
| shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.test.ts | Adds coverage for defaulting + unsafe link rejection. |
| pnpm-lock.yaml | Locks the new workspace package dependency graph. |
| knip.json | Registers the new flow package entry for knip. |
| features/flow/lazy-onboarding-banner/tsconfig.json | TypeScript config for the new flow package. |
| features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.ts | Builds safe Shop URL and applies UTM parameters. |
| features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.test.ts | Tests attribution behavior and fallback for unsafe links. |
| features/flow/lazy-onboarding-banner/src/types.ts | Defines shared types for normalized user/context state and decisions. |
| features/flow/lazy-onboarding-banner/src/testing.ts | Exposes a test-only reset hook for the session singleton. |
| features/flow/lazy-onboarding-banner/src/state/lazyOnboardingBannerSession.ts | Implements in-memory session dismissal store with subscription. |
| features/flow/lazy-onboarding-banner/src/state/lazyOnboardingBannerSession.test.ts | Tests dismissal persistence + subscriber notifications. |
| features/flow/lazy-onboarding-banner/src/react/useLazyOnboardingBannerSession.ts | React hook wrapper over the session store (useSyncExternalStore). |
| features/flow/lazy-onboarding-banner/src/index.ts | Public exports for decisioning, session hook, link builder, and types. |
| features/flow/lazy-onboarding-banner/src/decision/getLazyOnboardingBannerDecision.ts | Central eligibility decision function with explicit hidden reasons. |
| features/flow/lazy-onboarding-banner/src/decision/getLazyOnboardingBannerDecision.test.ts | Exhaustive decision table tests for all hidden reasons. |
| features/flow/lazy-onboarding-banner/README.md | Documents package purpose, exports, and validation commands. |
| features/flow/lazy-onboarding-banner/project.json | Nx project registration. |
| features/flow/lazy-onboarding-banner/package.json | New workspace package definition + scripts + exports. |
| features/flow/lazy-onboarding-banner/jest.config.js | Jest configuration for the new flow package. |
| features/flow/lazy-onboarding-banner/CHANGELOG.md | Initializes package changelog file. |
| CODEOWNERS | Adds ownership entries for the new package and related new files. |
| apps/ledger-live-mobile/src/mvvm/features/Portfolio/components/PortfolioBannersSection/usePortfolioBannersSectionViewModel.ts | Injects lazy onboarding banner VM into the portfolio banner VM result. |
| apps/ledger-live-mobile/src/mvvm/features/Portfolio/components/PortfolioBannersSection/index.tsx | Renders lazy onboarding banner with priority over other banners. |
| apps/ledger-live-mobile/src/mvvm/features/Portfolio/components/PortfolioBannersSection/tests/PortfolioBannersSection.test.tsx | Verifies banner priority behavior in unit tests. |
| apps/ledger-live-mobile/src/mvvm/features/Portfolio/integrations/readOnlyPortfolio.integration.test.tsx | Adds integration coverage for eligible read-only portfolio users. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/index.ts | Exposes the Mobile feature entrypoints (component/view/vm). |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/hooks/useLazyOnboardingBannerState.ts | Bridges Redux/flags state into flow decision + session dismissal. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/components/LazyOnboardingBanner/useLazyOnboardingBannerViewModel.ts | Creates banner props + builds attributed Shop URL + opens via Linking. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/components/LazyOnboardingBanner/types.ts | Defines view props for the banner view component. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/components/LazyOnboardingBanner/LazyOnboardingBannerView.tsx | Implements Lumen MediaBanner view rendering. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/components/LazyOnboardingBanner/index.tsx | Container component wiring ViewModel → View. |
| apps/ledger-live-mobile/src/mvvm/features/LazyOnboardingBanner/integrations/LazyOnboardingBanner.integration.test.tsx | End-to-end MVVM integration tests for openURL + dismissal + eligibility. |
| apps/ledger-live-mobile/src/locales/en/common.json | Adds English strings for the new banner. |
| apps/ledger-live-mobile/package.json | Adds dependency on the new flow package. |
| .changeset/lazy-banner-modes.md | Changeset for shared flag + mobile + new flow package. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
aac2566 to
880296d
Compare
880296d to
063c1d4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts:10
- The
linkvalidation is overly strict:startsWith(DEFAULT_LINK)rejects valid Shop URLs likehttps://shop.ledger.com(no trailing slash). This can cause a valid remote-config value to be rejected even thoughbuildLazyOnboardingBannerLinktreats it as safe.
shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.test.ts:29 - Link validation currently has no coverage for the common
https://shop.ledger.comvariant (no trailing slash). Adding a test for this prevents regressions and matches the intended “safe fallback + validated URL” behavior.
CODEOWNERS:385 - This CODEOWNERS entry points to
shared/feature-flags/src/lazyOnboardingBanner.ts, but that file doesn’t exist. The actual flag lives undershared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts, so ownership won’t be applied as intended.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
2173d8b to
a73498b
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.ts:6
buildLazyOnboardingBannerLinkis exported from a cross-platform flow package, but the UTM attribution is hard-coded (utm_medium: "llm",utm_campaign: "upsell_large_screen"). This makes it difficult to reuse on Desktop (and is inconsistent with the existing platform-aware UTM builder infeatures/flow/large-screen-upsell/src/utils/upsellCta.ts). Consider taking aplatform: "mobile" | "desktop"parameter (or injecting attribution) so the caller can produce correct platform-specific UTMs.
CODEOWNERS:385- This CODEOWNERS entry points to a non-existent path (
shared/feature-flags/src/lazyOnboardingBanner.ts). The flag lives undershared/feature-flags/src/flags/team-engagement/, so ownership for this file won’t be applied as intended.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts:10
- The link validation uses
startsWith(DEFAULT_LINK), which is stricter than needed (e.g. it rejects valid Shop URLs likehttps://shop.ledger.com?x=1) and duplicates the safer origin-based validation used elsewhere in this PR. Parsing the URL and checkingoriginis both clearer and avoids false negatives.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.ts:18
new URL(link.trim() || DEFAULT_LINK)will throw for any non-empty invalid string and will also accept non-HTTPS schemes (e.g.javascript:) if this utility is ever called with untrusted input. Since this link is ultimately opened by the app, it should defensively fall back to the default Shop URL and enforcehttps:.
CODEOWNERS:385- The CODEOWNERS entry points to a non-existent path (
shared/feature-flags/src/lazyOnboardingBanner.ts). The actual flag file lives undershared/feature-flags/src/flags/team-engagement/, so this rule won’t match and ownership won’t be applied.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
a73498b to
96a091e
Compare
|
Web Tools Build Status
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 34 out of 36 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (2)
features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.ts:18
new URL(link.trim() || DEFAULT_LINK)can throw on malformed inputs and also accepts non-HTTPS schemes (e.g.http:/javascript:) when the remote config is misconfigured. SincefetchRemoteFlags()JSON-parses values without Zod validation, this should defensively validate and fall back to the default HTTPS Shop URL to avoid crashes and unsafe navigation.
CODEOWNERS:388- This CODEOWNERS entry points to
shared/feature-flags/src/lazyOnboardingBanner.ts, but that file does not exist (the flag lives undershared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts). As written, ownership won't be applied to changes to the actual flag.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
Resolve leftover merge conflict markers and regenerate the lockfile so frozen CI installs succeed. Co-authored-by: Cursor <cursoragent@cursor.com>
bb4f84a to
1d11367
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 38 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (4)
apps/ledger-live-mobile/src/mvvm/features/Portfolio/components/PortfolioBannersSection/tests/PortfolioBannersSection.test.tsx:106
- This assertion is checking
mock-lns-banner, but the mocked LN banner renders with testIDmock-ln-banner(see beforeEach). As written, the test will pass even if the LN banner is mistakenly rendered.
expect(screen.getByText("Discover Ledger devices")).toBeVisible();
expect(screen.queryByTestId("mock-lns-banner")).toBeNull();
expect(screen.queryByTestId("mock-recover-banner")).toBeNull();
expect(screen.queryByTestId("mock-onboarding-widget")).toBeNull();
CODEOWNERS:401
- CODEOWNERS entry points to a non-existent path (
shared/feature-flags/src/lazyOnboardingBanner.ts). The lazy onboarding banner flag lives undershared/feature-flags/src/flags/team-engagement/, so ownership won’t apply as intended.
# Engagement team
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
apps/ledger-live-mobile/src/mvvm/features/Portfolio/components/PortfolioBannersSection/tests/PortfolioBannersSection.test.tsx:32
imageUrlisn’t part of theLazyOnboardingBannerViewPropsAPI (the native view owns the asset internally). Keeping it in this mocked view model is misleading and can hide API drift.
This issue also appears on line 103 of the same file.
imageUrl: "lazy-onboarding-banner.webp",
features/flow/lazy-onboarding-banner/src/utils/buildLazyOnboardingBannerLink.ts:19
new URL(...)will throw on malformed inputs (e.g., a bad remote-config value), which can crash rendering when the banner view model is created. If this helper is meant to provide a safe fallback, it should catch parsing errors and fall back to the default URL.
Correct the LN upsell mock testID and drop stale imageUrl from the view props mock. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 38 changed files in this pull request and generated 2 comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
CODEOWNERS:401
- The CODEOWNERS entry points to
shared/feature-flags/src/lazyOnboardingBanner.ts, but the flag lives atshared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts(no file exists at the current path), so ownership won’t apply.
# Engagement team
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 38 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
CODEOWNERS:400
- This CODEOWNERS entry points to a non-existent path. The flag lives under shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts (and is already covered by the existing **/team-engagement/ rule later in this file), so this line won’t have the intended effect.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
Fall back to the default HTTPS Shop URL for malformed or unsafe links. Coerce remote flag link values to strings. Co-authored-by: Cursor <cursoragent@cursor.com>
4d16156 to
845efd8
Compare
…VE-34516-lazy-onboarding-banner Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # apps/ledger-live-mobile/package.json
05c8258
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 35 out of 38 changed files in this pull request and generated no new comments.
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (1)
CODEOWNERS:406
- This CODEOWNERS entry points to
shared/feature-flags/src/lazyOnboardingBanner.ts, but that path does not exist in the repo (the flag lives undershared/feature-flags/src/flags/team-engagement/). As-is, changes to the flag won’t be owned by @ledgerhq/engagement.
shared/feature-flags/src/lazyOnboardingBanner.ts @ledgerhq/engagement
|



✅ Checklist
npx changesetwas attached.📝 Description
This PR adds the lazy onboarding device-discovery banner to the Mobile Portfolio for eligible read-only/reborn users who completed onboarding without a Ledger device and have never connected one.
The implementation:
lazyOnboardingBannerwith a validated Ledger Shop HTTPSlinkparameter and safe default.@features/flow-lazy-onboarding-bannerpackage for eligibility decisions, session dismissal, and Shop URL construction.MediaBanner, with priority over existing Portfolio banners.shop_directandfeature_intromodes.Uploading LIVE-34516-lazy-onboarding-banner.mp4…
❓ Context
🧐 Checklist for the PR Reviewers