Skip to content

[LWM] feat(lazy-onboarding): add portfolio device banner - #20290

Merged
sarneijim merged 9 commits into
developfrom
feat/engagement-LIVE-34516-lazy-onboarding-banner
Aug 7, 2026
Merged

[LWM] feat(lazy-onboarding): add portfolio device banner#20290
sarneijim merged 9 commits into
developfrom
feat/engagement-LIVE-34516-lazy-onboarding-banner

Conversation

@sarneijim

@sarneijim sarneijim commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

✅ Checklist

  • npx changeset was attached.
  • Covered by automatic tests.
  • Impact of the changes:
    • Eligibility for completed lazy-onboarding users who have never connected a device
    • Portfolio banner priority, rendering, and session-only dismissal
    • Direct Ledger Shop navigation for both feature-flag modes
    • Ledger Shop link validation, safe fallback, and UTM attribution

📝 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:

  • Extends lazyOnboardingBanner with a validated Ledger Shop HTTPS link parameter and safe default.
  • Adds the DDD @features/flow-lazy-onboarding-banner package for eligibility decisions, session dismissal, and Shop URL construction.
  • Adds the LWM MVVM banner implementation using Lumen MediaBanner, with priority over existing Portfolio banners.
  • Opens the Shop directly for both shop_direct and feature_intro modes.
  • Adds no discount or tracking; tracking will be adjusted separately.

Uploading LIVE-34516-lazy-onboarding-banner.mp4…

❓ Context


🧐 Checklist for the PR Reviewers

  • The code aligns with the requirements described in the linked JIRA or GitHub issue.
  • The PR description clearly documents the changes made and explains any technical trade-offs or design decisions.
  • There are no undocumented trade-offs, technical debt, or maintainability issues.
  • The PR has been tested thoroughly, and any potential edge cases have been considered and handled.
  • Any new dependencies have been justified and documented.
  • Performance considerations have been taken into account. (changes have been profiled or benchmarked if necessary)
image

Copilot AI lite review requested due to automatic review settings July 31, 2026 14:30
@live-github-bot live-github-bot Bot added mobile Has changes in LLM translations Translation files have been touched labels Jul 31, 2026
@live-github-bot live-github-bot Bot changed the title feat(lazy-onboarding): add portfolio device banner (LIVE-34516) [LWM] feat(lazy-onboarding): add portfolio device banner (LIVE-34516) Jul 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 lazyOnboardingBanner feature-flag params validation, including an allowlisted Ledger Shop HTTPS link with a default fallback.
  • Created @features/flow-lazy-onboarding-banner for 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

Comment thread shared/feature-flags/src/lazyOnboardingBanner.ts Outdated
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch from aac2566 to 880296d Compare July 31, 2026 19:24
Copilot AI review requested due to automatic review settings July 31, 2026 19:24
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch from 880296d to 063c1d4 Compare July 31, 2026 19:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 link validation is overly strict: startsWith(DEFAULT_LINK) rejects valid Shop URLs like https://shop.ledger.com (no trailing slash). This can cause a valid remote-config value to be rejected even though buildLazyOnboardingBannerLink treats 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.com variant (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 under shared/feature-flags/src/flags/team-engagement/lazyOnboardingBanner.ts, so ownership won’t be applied as intended.
shared/feature-flags/src/lazyOnboardingBanner.ts                                              @ledgerhq/engagement

Copilot AI review requested due to automatic review settings July 31, 2026 19:30
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch 2 times, most recently from 2173d8b to a73498b Compare July 31, 2026 19:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

  • buildLazyOnboardingBannerLink is 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 in features/flow/large-screen-upsell/src/utils/upsellCta.ts). Consider taking a platform: "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 under shared/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 like https://shop.ledger.com?x=1) and duplicates the safer origin-based validation used elsewhere in this PR. Parsing the URL and checking origin is both clearer and avoids false negatives.

Copilot AI review requested due to automatic review settings July 31, 2026 19:38
@sarneijim
sarneijim marked this pull request as ready for review July 31, 2026 19:43
@sarneijim
sarneijim requested review from a team as code owners July 31, 2026 19:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 enforce https:.
    CODEOWNERS:385
  • The CODEOWNERS entry points to a non-existent path (shared/feature-flags/src/lazyOnboardingBanner.ts). The actual flag file lives under shared/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

Copilot AI review requested due to automatic review settings July 31, 2026 19:54
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch from a73498b to 96a091e Compare July 31, 2026 19:54
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ E2E tests are required

Changes detected require e2e testing before merge (even before asking for any review).

🖥️ Desktop

-> Run Desktop E2E

  • Select "Run workflow"
  • Branch: feat/engagement-LIVE-34516-lazy-onboarding-banner
  • Device: nanoSP or stax

📱 Mobile

-> Run Mobile E2E

  • Select "Run workflow"
  • Branch: feat/engagement-LIVE-34516-lazy-onboarding-banner
  • Device: nanoX

@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Web Tools Build Status

Build Status Deployment
Web Tools Build ✅ Deployed https://web-tools-q8q1l4dvy-ledger-hq-prd.vercel.app
Native Storybook Build ✅ Deployed https://native-ui-storybook-4cdqsway2-ledger-hq-prd.vercel.app
React Storybook Build ✅ Deployed https://react-ui-storybook-9tgut1f49-ledger-hq-prd.vercel.app

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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. Since fetchRemoteFlags() 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 under shared/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>
Copilot AI review requested due to automatic review settings August 5, 2026 14:39
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch from bb4f84a to 1d11367 Compare August 5, 2026 14:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 testID mock-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 under shared/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

  • imageUrl isn’t part of the LazyOnboardingBannerViewProps API (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>
Copilot AI review requested due to automatic review settings August 5, 2026 14:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 at shared/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

Copilot AI review requested due to automatic review settings August 5, 2026 15:02

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@sarneijim
sarneijim force-pushed the feat/engagement-LIVE-34516-lazy-onboarding-banner branch from 4d16156 to 845efd8 Compare August 5, 2026 19:54
jiyuzhuang
jiyuzhuang previously approved these changes Aug 6, 2026
gre-ledger
gre-ledger previously approved these changes Aug 6, 2026
…VE-34516-lazy-onboarding-banner

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	apps/ledger-live-mobile/package.json
Copilot AI review requested due to automatic review settings August 7, 2026 09:43

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 under shared/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

@sonarqubecloud

sonarqubecloud Bot commented Aug 7, 2026

Copy link
Copy Markdown

@sarneijim
sarneijim merged commit b70b4e9 into develop Aug 7, 2026
83 checks passed
@sarneijim
sarneijim deleted the feat/engagement-LIVE-34516-lazy-onboarding-banner branch August 7, 2026 10:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

mobile Has changes in LLM translations Translation files have been touched

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants