From e59151b30c7a8621d46587a49eb6576fd54ae475 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 21 Apr 2026 16:22:54 +0530 Subject: [PATCH 1/5] Add helper to detect stale classicRedirect nudge --- src/libs/TryNewDotUtils.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libs/TryNewDotUtils.ts b/src/libs/TryNewDotUtils.ts index 10e692afa63f..96bc97990b7e 100644 --- a/src/libs/TryNewDotUtils.ts +++ b/src/libs/TryNewDotUtils.ts @@ -1,11 +1,24 @@ +import {differenceInDays} from 'date-fns'; import type {OnyxEntry} from 'react-native-onyx'; import type {TryNewDot} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +const CLASSIC_REDIRECT_STALE_THRESHOLD_IN_DAYS = 30; + function isLockedToNewApp(tryNewDot: OnyxEntry): boolean { return tryNewDot?.isLockedToNewApp === true; } +function isClassicRedirectStale(tryNewDot: OnyxEntry): boolean { + const classicRedirect = tryNewDot?.classicRedirect; + if (!classicRedirect || classicRedirect.dismissed !== false || !classicRedirect.timestamp) { + return false; + } + + const daysSinceNudge = differenceInDays(new Date(), new Date(classicRedirect.timestamp)); + return daysSinceNudge >= CLASSIC_REDIRECT_STALE_THRESHOLD_IN_DAYS; +} + function shouldBlockOldAppExit(tryNewDot: OnyxEntry, isLoadingTryNewDot: boolean, shouldSetNVP: boolean): boolean { if (isLockedToNewApp(tryNewDot)) { return true; @@ -34,4 +47,4 @@ function shouldUseOldApp(tryNewDot: TryNewDot): boolean | undefined { return tryNewDot.classicRedirect.dismissed; } -export {isLockedToNewApp, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp}; +export {isClassicRedirectStale, isLockedToNewApp, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp}; From 67b5b8c0b2edd872173cde088179ce27fcb59a70 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 21 Apr 2026 16:23:26 +0530 Subject: [PATCH 2/5] Hide Switch to Classic button when classicRedirect nudge is stale --- src/libs/TryNewDotUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/TryNewDotUtils.ts b/src/libs/TryNewDotUtils.ts index 96bc97990b7e..b1657a0b1efa 100644 --- a/src/libs/TryNewDotUtils.ts +++ b/src/libs/TryNewDotUtils.ts @@ -28,7 +28,7 @@ function shouldBlockOldAppExit(tryNewDot: OnyxEntry, isLoadingTryNewD } function isOldAppRedirectBlocked(tryNewDot: OnyxEntry, shouldRespectMobileLock: boolean): boolean { - return tryNewDot?.classicRedirect?.isLockedToNewDot === true || (shouldRespectMobileLock && isLockedToNewApp(tryNewDot)); + return tryNewDot?.classicRedirect?.isLockedToNewDot === true || isClassicRedirectStale(tryNewDot) || (shouldRespectMobileLock && isLockedToNewApp(tryNewDot)); } function shouldHideOldAppRedirect(tryNewDot: OnyxEntry, isLoadingTryNewDot: boolean, shouldRespectMobileLock: boolean): boolean { From 4fd0188582796b0f5353308a0145a8d5bb27035e Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 21 Apr 2026 16:24:20 +0530 Subject: [PATCH 3/5] Test isClassicRedirectStale helper --- tests/unit/TryNewDotUtilsTest.ts | 46 +++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/tests/unit/TryNewDotUtilsTest.ts b/tests/unit/TryNewDotUtilsTest.ts index 76b7e20678d7..a5754066ac9c 100644 --- a/tests/unit/TryNewDotUtilsTest.ts +++ b/tests/unit/TryNewDotUtilsTest.ts @@ -1,5 +1,6 @@ +import {subDays} from 'date-fns'; import Onyx from 'react-native-onyx'; -import {isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp} from '@src/libs/TryNewDotUtils'; +import {isClassicRedirectStale, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp} from '@src/libs/TryNewDotUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {TryNewDot} from '@src/types/onyx'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -68,6 +69,49 @@ describe('TryNewDotUtils', () => { expect(shouldBlockOldAppExit({isLockedToNewApp: true} as TryNewDot, false, false)).toBe(true); }); + it('treats classicRedirect as stale when the user has not dismissed the nudge for over a month', () => { + const tryNewDot = { + classicRedirect: { + dismissed: false, + timestamp: subDays(new Date(), 31).toISOString(), + }, + } as unknown as TryNewDot; + + expect(isClassicRedirectStale(tryNewDot)).toBe(true); + }); + + it('does not treat classicRedirect as stale when the nudge is less than a month old', () => { + const tryNewDot = { + classicRedirect: { + dismissed: false, + timestamp: subDays(new Date(), 10).toISOString(), + }, + } as unknown as TryNewDot; + + expect(isClassicRedirectStale(tryNewDot)).toBe(false); + }); + + it('does not treat classicRedirect as stale once the user has dismissed the nudge', () => { + const tryNewDot = { + classicRedirect: { + dismissed: true, + timestamp: subDays(new Date(), 60).toISOString(), + }, + } as unknown as TryNewDot; + + expect(isClassicRedirectStale(tryNewDot)).toBe(false); + }); + + it('does not treat classicRedirect as stale when no timestamp is set', () => { + const tryNewDot = { + classicRedirect: { + dismissed: false, + }, + } as unknown as TryNewDot; + + expect(isClassicRedirectStale(tryNewDot)).toBe(false); + }); + it('preserves isLockedToNewApp when nvp_tryNewDot is merged', async () => { await Onyx.set(ONYXKEYS.NVP_TRY_NEW_DOT, { isLockedToNewApp: true, From e09b1a49bbac55f1d5c8ed31b0728283c07d5406 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Tue, 21 Apr 2026 16:24:56 +0530 Subject: [PATCH 4/5] Test isOldAppRedirectBlocked hides button for stale nudge --- tests/unit/TryNewDotUtilsTest.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/TryNewDotUtilsTest.ts b/tests/unit/TryNewDotUtilsTest.ts index a5754066ac9c..508e89e0b2ec 100644 --- a/tests/unit/TryNewDotUtilsTest.ts +++ b/tests/unit/TryNewDotUtilsTest.ts @@ -69,6 +69,28 @@ describe('TryNewDotUtils', () => { expect(shouldBlockOldAppExit({isLockedToNewApp: true} as TryNewDot, false, false)).toBe(true); }); + it('blocks the OldDot redirect when the classicRedirect nudge has gone stale', () => { + const tryNewDot = { + classicRedirect: { + dismissed: false, + timestamp: subDays(new Date(), 31).toISOString(), + }, + } as unknown as TryNewDot; + + expect(isOldAppRedirectBlocked(tryNewDot, false)).toBe(true); + }); + + it('still shows the OldDot redirect when the classicRedirect nudge is fresh', () => { + const tryNewDot = { + classicRedirect: { + dismissed: false, + timestamp: subDays(new Date(), 5).toISOString(), + }, + } as unknown as TryNewDot; + + expect(isOldAppRedirectBlocked(tryNewDot, false)).toBe(false); + }); + it('treats classicRedirect as stale when the user has not dismissed the nudge for over a month', () => { const tryNewDot = { classicRedirect: { From 2336591a04859ef84eceae0f805b1e3d560e5317 Mon Sep 17 00:00:00 2001 From: allgandaf Date: Wed, 22 Apr 2026 00:24:27 +0530 Subject: [PATCH 5/5] Rename helper to hasBeenInNewDot30Days --- src/libs/TryNewDotUtils.ts | 10 +++++----- tests/unit/TryNewDotUtilsTest.ts | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libs/TryNewDotUtils.ts b/src/libs/TryNewDotUtils.ts index b1657a0b1efa..6cfa5a30f26b 100644 --- a/src/libs/TryNewDotUtils.ts +++ b/src/libs/TryNewDotUtils.ts @@ -3,20 +3,20 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {TryNewDot} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const CLASSIC_REDIRECT_STALE_THRESHOLD_IN_DAYS = 30; +const NEW_DOT_MIN_DAYS_BEFORE_HIDING_CLASSIC_REDIRECT = 30; function isLockedToNewApp(tryNewDot: OnyxEntry): boolean { return tryNewDot?.isLockedToNewApp === true; } -function isClassicRedirectStale(tryNewDot: OnyxEntry): boolean { +function hasBeenInNewDot30Days(tryNewDot: OnyxEntry): boolean { const classicRedirect = tryNewDot?.classicRedirect; if (!classicRedirect || classicRedirect.dismissed !== false || !classicRedirect.timestamp) { return false; } const daysSinceNudge = differenceInDays(new Date(), new Date(classicRedirect.timestamp)); - return daysSinceNudge >= CLASSIC_REDIRECT_STALE_THRESHOLD_IN_DAYS; + return daysSinceNudge >= NEW_DOT_MIN_DAYS_BEFORE_HIDING_CLASSIC_REDIRECT; } function shouldBlockOldAppExit(tryNewDot: OnyxEntry, isLoadingTryNewDot: boolean, shouldSetNVP: boolean): boolean { @@ -28,7 +28,7 @@ function shouldBlockOldAppExit(tryNewDot: OnyxEntry, isLoadingTryNewD } function isOldAppRedirectBlocked(tryNewDot: OnyxEntry, shouldRespectMobileLock: boolean): boolean { - return tryNewDot?.classicRedirect?.isLockedToNewDot === true || isClassicRedirectStale(tryNewDot) || (shouldRespectMobileLock && isLockedToNewApp(tryNewDot)); + return tryNewDot?.classicRedirect?.isLockedToNewDot === true || hasBeenInNewDot30Days(tryNewDot) || (shouldRespectMobileLock && isLockedToNewApp(tryNewDot)); } function shouldHideOldAppRedirect(tryNewDot: OnyxEntry, isLoadingTryNewDot: boolean, shouldRespectMobileLock: boolean): boolean { @@ -47,4 +47,4 @@ function shouldUseOldApp(tryNewDot: TryNewDot): boolean | undefined { return tryNewDot.classicRedirect.dismissed; } -export {isClassicRedirectStale, isLockedToNewApp, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp}; +export {hasBeenInNewDot30Days, isLockedToNewApp, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp}; diff --git a/tests/unit/TryNewDotUtilsTest.ts b/tests/unit/TryNewDotUtilsTest.ts index 508e89e0b2ec..a2f4244aed33 100644 --- a/tests/unit/TryNewDotUtilsTest.ts +++ b/tests/unit/TryNewDotUtilsTest.ts @@ -1,6 +1,6 @@ import {subDays} from 'date-fns'; import Onyx from 'react-native-onyx'; -import {isClassicRedirectStale, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp} from '@src/libs/TryNewDotUtils'; +import {hasBeenInNewDot30Days, isOldAppRedirectBlocked, shouldBlockOldAppExit, shouldHideOldAppRedirect, shouldUseOldApp} from '@src/libs/TryNewDotUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import type {TryNewDot} from '@src/types/onyx'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; @@ -91,7 +91,7 @@ describe('TryNewDotUtils', () => { expect(isOldAppRedirectBlocked(tryNewDot, false)).toBe(false); }); - it('treats classicRedirect as stale when the user has not dismissed the nudge for over a month', () => { + it('reports that a user has been in NewDot 30 days when the nudge is over a month old and not dismissed', () => { const tryNewDot = { classicRedirect: { dismissed: false, @@ -99,10 +99,10 @@ describe('TryNewDotUtils', () => { }, } as unknown as TryNewDot; - expect(isClassicRedirectStale(tryNewDot)).toBe(true); + expect(hasBeenInNewDot30Days(tryNewDot)).toBe(true); }); - it('does not treat classicRedirect as stale when the nudge is less than a month old', () => { + it('does not report 30 days in NewDot when the nudge is less than a month old', () => { const tryNewDot = { classicRedirect: { dismissed: false, @@ -110,10 +110,10 @@ describe('TryNewDotUtils', () => { }, } as unknown as TryNewDot; - expect(isClassicRedirectStale(tryNewDot)).toBe(false); + expect(hasBeenInNewDot30Days(tryNewDot)).toBe(false); }); - it('does not treat classicRedirect as stale once the user has dismissed the nudge', () => { + it('does not report 30 days in NewDot once the user has dismissed the nudge', () => { const tryNewDot = { classicRedirect: { dismissed: true, @@ -121,17 +121,17 @@ describe('TryNewDotUtils', () => { }, } as unknown as TryNewDot; - expect(isClassicRedirectStale(tryNewDot)).toBe(false); + expect(hasBeenInNewDot30Days(tryNewDot)).toBe(false); }); - it('does not treat classicRedirect as stale when no timestamp is set', () => { + it('does not report 30 days in NewDot when no timestamp is set', () => { const tryNewDot = { classicRedirect: { dismissed: false, }, } as unknown as TryNewDot; - expect(isClassicRedirectStale(tryNewDot)).toBe(false); + expect(hasBeenInNewDot30Days(tryNewDot)).toBe(false); }); it('preserves isLockedToNewApp when nvp_tryNewDot is merged', async () => {