Skip to content

Commit 59a6c82

Browse files
committed
feat(large-screen-upsell): gate modal by selected variant
1 parent 61cc19c commit 59a6c82

8 files changed

Lines changed: 251 additions & 20 deletions

File tree

.changeset/quiet-screens-toggle.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@features/flow-large-screen-upsell": patch
3+
"@shared/feature-flags": minor
4+
"ledger-live-desktop": minor
5+
"live-mobile": minor
6+
---
7+
8+
Gate the large-screen upsell modal by the enabled state of the selected opt-in variant

apps/ledger-live-desktop/src/mvvm/features/LargeScreenUpsell/__integrations__/LargeScreenUpsellModal.integration.test.tsx

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,15 @@ const NOW = new Date("2026-07-15T12:00:00.000Z");
5656
function eligibleState(settingsOverrides: Partial<State["settings"]> = {}): DeepPartial<State> {
5757
return {
5858
...withFlagOverrides({
59-
largeScreenUpsell: { enabled: true },
59+
largeScreenUpsell: {
60+
enabled: true,
61+
params: {
62+
opted_out: {
63+
enabled: true,
64+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
65+
},
66+
},
67+
},
6068
lwdWallet40: { enabled: true, params: { tour: false } },
6169
}),
6270
settings: {
@@ -223,8 +231,14 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
223231
largeScreenUpsell: {
224232
enabled: true,
225233
params: {
226-
opted_in: { link: "https://shop.ledger.com/pages/opted-in-offer" },
227-
opted_out: { link: "https://shop.ledger.com/pages/opted-out-offer" },
234+
opted_in: {
235+
enabled: true,
236+
link: "https://shop.ledger.com/pages/opted-in-offer",
237+
},
238+
opted_out: {
239+
enabled: true,
240+
link: "https://shop.ledger.com/pages/opted-out-offer",
241+
},
228242
},
229243
},
230244
lwdWallet40: { enabled: true, params: { tour: false } },
@@ -268,8 +282,14 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
268282
largeScreenUpsell: {
269283
enabled: true,
270284
params: {
271-
opted_in: { link: "https://shop.ledger.com/pages/opted-in-offer" },
272-
opted_out: { link: "https://shop.ledger.com/pages/opted-out-offer" },
285+
opted_in: {
286+
enabled: true,
287+
link: "https://shop.ledger.com/pages/opted-in-offer",
288+
},
289+
opted_out: {
290+
enabled: true,
291+
link: "https://shop.ledger.com/pages/opted-out-offer",
292+
},
273293
},
274294
},
275295
lwdWallet40: { enabled: true, params: { tour: false } },
@@ -356,7 +376,7 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
356376
largeScreenUpsell: {
357377
enabled: true,
358378
params: {
359-
opted_out: { link: " " },
379+
opted_out: { enabled: true, link: " " },
360380
},
361381
},
362382
lwdWallet40: { enabled: true, params: { tour: false } },
@@ -404,6 +424,10 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
404424
enabled: true,
405425
params: {
406426
modal: { enabled: true, killThreshold: 3, cadenceDays: 30 },
427+
opted_out: {
428+
enabled: true,
429+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
430+
},
407431
},
408432
},
409433
lwdWallet40: { enabled: true, params: { tour: false } },
@@ -511,6 +535,52 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
511535
await expectModalNotOpen();
512536
});
513537

538+
it("should not open when the opted-out variant is disabled", async () => {
539+
const { store } = renderMount({
540+
...eligibleState(),
541+
...withFlagOverrides({
542+
largeScreenUpsell: {
543+
enabled: true,
544+
params: {
545+
opted_out: {
546+
enabled: false,
547+
link: "https://shop.ledger.com/pages/opted-out-offer",
548+
},
549+
},
550+
},
551+
lwdWallet40: { enabled: true, params: { tour: false } },
552+
}),
553+
});
554+
555+
await expectModalNotOpen();
556+
557+
expect(store.getState().largeScreenUpsellModal.retries).toBe(0);
558+
expect(trackPage).not.toHaveBeenCalled();
559+
});
560+
561+
it("should not open when the opted-in variant is disabled", async () => {
562+
const { store } = renderMount({
563+
...eligibleState({ sharePersonalizedRecommandations: true }),
564+
...withFlagOverrides({
565+
largeScreenUpsell: {
566+
enabled: true,
567+
params: {
568+
opted_in: {
569+
enabled: false,
570+
link: "https://shop.ledger.com/pages/opted-in-offer",
571+
},
572+
},
573+
},
574+
lwdWallet40: { enabled: true, params: { tour: false } },
575+
}),
576+
});
577+
578+
await expectModalNotOpen();
579+
580+
expect(store.getState().largeScreenUpsellModal.retries).toBe(0);
581+
expect(trackPage).not.toHaveBeenCalled();
582+
});
583+
514584
it("should not open when the seen nano model is disabled for the audience", async () => {
515585
renderMount({
516586
...eligibleState(),
@@ -562,6 +632,10 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
562632
enabled: true,
563633
params: {
564634
modal: { enabled: true, killThreshold: 3, cadenceDays: 30 },
635+
opted_out: {
636+
enabled: true,
637+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
638+
},
565639
},
566640
},
567641
lwdWallet40: { enabled: true, params: { tour: false } },
@@ -584,6 +658,10 @@ describe("LargeScreenUpsellModalMount (integration)", () => {
584658
enabled: true,
585659
params: {
586660
modal: { enabled: true, killThreshold: 3, cadenceDays: 30 },
661+
opted_out: {
662+
enabled: true,
663+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
664+
},
587665
},
588666
},
589667
lwdWallet40: { enabled: true, params: { tour: false } },

apps/ledger-live-mobile/src/mvvm/features/LargeScreenUpsell/__integrations__/LargeScreenUpsellModal.integration.test.tsx

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
110110
it("should auto-open the upsell modal when eligible and unblocked", async () => {
111111
const overrideInitialState = withFlagOverrides(
112112
{
113-
largeScreenUpsell: { enabled: true },
113+
largeScreenUpsell: {
114+
enabled: true,
115+
params: {
116+
opted_out: {
117+
enabled: true,
118+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
119+
},
120+
},
121+
},
114122
lwmProductTour: { enabled: false },
115123
lwmGenericAwarenessModal: { enabled: false },
116124
analyticsOptIn: { enabled: false },
@@ -131,7 +139,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
131139
it("should track the modal view with shared analytics properties when it auto-opens", async () => {
132140
const overrideInitialState = withFlagOverrides(
133141
{
134-
largeScreenUpsell: { enabled: true },
142+
largeScreenUpsell: {
143+
enabled: true,
144+
params: {
145+
opted_out: {
146+
enabled: true,
147+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
148+
},
149+
},
150+
},
135151
lwmProductTour: { enabled: false },
136152
lwmGenericAwarenessModal: { enabled: false },
137153
analyticsOptIn: { enabled: false },
@@ -156,7 +172,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
156172
it("should track opted-in offer properties when personalized recommendations are enabled", async () => {
157173
const overrideInitialState = withFlagOverrides(
158174
{
159-
largeScreenUpsell: { enabled: true },
175+
largeScreenUpsell: {
176+
enabled: true,
177+
params: {
178+
opted_in: {
179+
enabled: true,
180+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
181+
},
182+
},
183+
},
160184
lwmProductTour: { enabled: false },
161185
lwmGenericAwarenessModal: { enabled: false },
162186
analyticsOptIn: { enabled: false },
@@ -180,10 +204,76 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
180204
});
181205
});
182206

207+
it("should not open when the opted-out variant is disabled", async () => {
208+
const overrideInitialState = withFlagOverrides(
209+
{
210+
largeScreenUpsell: {
211+
enabled: true,
212+
params: {
213+
opted_out: {
214+
enabled: false,
215+
link: "https://shop.ledger.com/pages/opted-out-offer",
216+
},
217+
},
218+
},
219+
lwmProductTour: { enabled: false },
220+
lwmGenericAwarenessModal: { enabled: false },
221+
analyticsOptIn: { enabled: false },
222+
},
223+
withKnownDeviceModels([DeviceModelId.nanoS]),
224+
);
225+
226+
const { store } = render(<IntegrationNavigator />, { overrideInitialState });
227+
228+
expect(await screen.findByTestId("large-screen-upsell-integration-portfolio")).toBeVisible();
229+
act(() => jest.runOnlyPendingTimers());
230+
expect(screen.queryByTestId("large-screen-upsell-modal-drawer")).toBeNull();
231+
expect(store.getState().largeScreenUpsellModal.retries).toBe(0);
232+
expect(jest.mocked(analyticsScreen)).not.toHaveBeenCalled();
233+
});
234+
235+
it("should not open when the opted-in variant is disabled", async () => {
236+
const overrideInitialState = withFlagOverrides(
237+
{
238+
largeScreenUpsell: {
239+
enabled: true,
240+
params: {
241+
opted_in: {
242+
enabled: false,
243+
link: "https://shop.ledger.com/pages/opted-in-offer",
244+
},
245+
},
246+
},
247+
lwmProductTour: { enabled: false },
248+
lwmGenericAwarenessModal: { enabled: false },
249+
analyticsOptIn: { enabled: false },
250+
},
251+
withKnownDeviceModels([DeviceModelId.nanoS], {
252+
personalizedRecommendationsEnabled: true,
253+
}),
254+
);
255+
256+
const { store } = render(<IntegrationNavigator />, { overrideInitialState });
257+
258+
expect(await screen.findByTestId("large-screen-upsell-integration-portfolio")).toBeVisible();
259+
act(() => jest.runOnlyPendingTimers());
260+
expect(screen.queryByTestId("large-screen-upsell-modal-drawer")).toBeNull();
261+
expect(store.getState().largeScreenUpsellModal.retries).toBe(0);
262+
expect(jest.mocked(analyticsScreen)).not.toHaveBeenCalled();
263+
});
264+
183265
it("should track button_clicked with shared analytics properties when the CTA is pressed", async () => {
184266
const overrideInitialState = withFlagOverrides(
185267
{
186-
largeScreenUpsell: { enabled: true },
268+
largeScreenUpsell: {
269+
enabled: true,
270+
params: {
271+
opted_out: {
272+
enabled: true,
273+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
274+
},
275+
},
276+
},
187277
lwmProductTour: { enabled: false },
188278
lwmGenericAwarenessModal: { enabled: false },
189279
analyticsOptIn: { enabled: false },
@@ -210,7 +300,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
210300
it("should track modal_dismissed with dismissMethod close button exactly once when the header close button is pressed", async () => {
211301
const overrideInitialState = withFlagOverrides(
212302
{
213-
largeScreenUpsell: { enabled: true },
303+
largeScreenUpsell: {
304+
enabled: true,
305+
params: {
306+
opted_out: {
307+
enabled: true,
308+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
309+
},
310+
},
311+
},
214312
lwmProductTour: { enabled: false },
215313
lwmGenericAwarenessModal: { enabled: false },
216314
analyticsOptIn: { enabled: false },
@@ -243,7 +341,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
243341
it("should auto-open the upsell modal when product tour is enabled but onboarding is not completed", async () => {
244342
const overrideInitialState = withFlagOverrides(
245343
{
246-
largeScreenUpsell: { enabled: true },
344+
largeScreenUpsell: {
345+
enabled: true,
346+
params: {
347+
opted_out: {
348+
enabled: true,
349+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
350+
},
351+
},
352+
},
247353
lwmProductTour: { enabled: true },
248354
lwmGenericAwarenessModal: { enabled: false },
249355
analyticsOptIn: { enabled: false },
@@ -267,7 +373,15 @@ describe("LargeScreenUpsellModal on Portfolio (integration)", () => {
267373
it("should not auto-open the upsell modal when a competing app-start modal opens after mount", async () => {
268374
const overrideInitialState = withFlagOverrides(
269375
{
270-
largeScreenUpsell: { enabled: true },
376+
largeScreenUpsell: {
377+
enabled: true,
378+
params: {
379+
opted_out: {
380+
enabled: true,
381+
link: "https://shop.ledger.com/pages/ledger-nano-upgrade-program",
382+
},
383+
},
384+
},
271385
lwmProductTour: { enabled: false },
272386
lwmGenericAwarenessModal: { enabled: false },
273387
analyticsOptIn: { enabled: false },

apps/ledger-live-mobile/src/mvvm/features/LargeScreenUpsell/components/LargeScreenUpsellModalPortfolioMount/useLargeScreenUpsellModalPortfolioMountViewModel.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,12 @@ export function useLargeScreenUpsellModalPortfolioMountViewModel(): LargeScreenU
6565
hasSeenCompetingAppStartModalRef.current = true;
6666
}
6767

68+
const variant: LargeScreenUpsellVariant = personalizedRecommendationsEnabled
69+
? "opted_in"
70+
: "opted_out";
71+
6872
const params = feature?.params;
69-
const hasEnabledFeature = Boolean(feature?.enabled && params);
73+
const hasEnabledFeature = Boolean(feature?.enabled && params?.[variant].enabled);
7074
const lastSeenAtDate = typeof lastSeenAt === "number" ? new Date(lastSeenAt) : null;
7175

7276
const isThrottled =
@@ -88,10 +92,6 @@ export function useLargeScreenUpsellModalPortfolioMountViewModel(): LargeScreenU
8892
const shouldAttemptAutoOpen =
8993
isEligible && !hasSeenCompetingAppStartModalRef.current && !hasAutoOpenedThisSession;
9094

91-
const variant: LargeScreenUpsellVariant = personalizedRecommendationsEnabled
92-
? "opted_in"
93-
: "opted_out";
94-
9595
const sharedAnalyticsProps: LargeScreenUpsellSharedAnalyticsProps | null = useMemo(() => {
9696
if (!("deviceModelId" in eligibility)) {
9797
return null;

features/flow/large-screen-upsell/src/hooks/useLargeScreenUpsellDecision.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {
55
retriesUpsellModalSelector,
66
} from "@domain/entity-large-screen-upsell-modal";
77
import { getLargeScreenUpsellDecision } from "../decision/getLargeScreenUpsellDecision";
8+
import type { LargeScreenUpsellVariant } from "../utils/upsellContent";
89
import type { LargeScreenUpsellDecision, NanoDeviceModelId } from "../types";
910

1011
export type UseLargeScreenUpsellDecisionInput = {
1112
seenNanoModelIds: NanoDeviceModelId[];
1213
hasSeenTouchscreenDevice: boolean;
1314
onboardingDate: Date | null;
15+
variant: LargeScreenUpsellVariant;
1416
now?: Date;
1517
};
1618

@@ -30,7 +32,7 @@ export function useLargeScreenUpsellDecision(
3032
frequency: { retries, lastSeenAt },
3133
},
3234
{
33-
isFeatureEnabled: Boolean(feature?.enabled) && params !== undefined,
35+
isFeatureEnabled: Boolean(feature?.enabled && params?.[input.variant].enabled),
3436
isModalEnabled: Boolean(params?.modal.enabled),
3537
audienceModels: params?.audience.models ?? { nanoS: false, nanoSP: false, nanoX: false },
3638
cooldownDays: params?.cooldownDays ?? { default: Infinity },

features/flow/large-screen-upsell/src/screens/LargeScreenUpsellModal/useLargeScreenUpsellModalViewModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function useLargeScreenUpsellModalViewModel({
5151
seenNanoModelIds,
5252
hasSeenTouchscreenDevice,
5353
onboardingDate,
54+
variant,
5455
now,
5556
});
5657
const [isOpen, setIsOpen] = useState(false);

0 commit comments

Comments
 (0)