diff --git a/src/hooks/useProactiveAppReview.ts b/src/hooks/useProactiveAppReview.ts index a9b4af24becd..9ce0cb2ebbd5 100644 --- a/src/hooks/useProactiveAppReview.ts +++ b/src/hooks/useProactiveAppReview.ts @@ -1,12 +1,19 @@ +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Session} from '@src/types/onyx'; import type ProactiveAppReview from '@src/types/onyx/AppReview'; -import {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; + +import {isActingAsDelegateSelector} from '@selectors/Account'; +import {useState} from 'react'; import useOnyx from './useOnyx'; const THIRTY_DAYS_IN_MS = 30 * 24 * 60 * 60 * 1000; +const authTokenTypeSelector = (session: OnyxEntry) => session?.authTokenType; + type UseProactiveAppReviewReturn = { /** Whether the modal should be shown */ shouldShowModal: boolean; @@ -20,35 +27,30 @@ type UseProactiveAppReviewReturn = { */ function useProactiveAppReview(): UseProactiveAppReviewReturn { const [proactiveAppReview] = useOnyx(ONYXKEYS.NVP_APP_REVIEW); - - const shouldShowModal = useMemo(() => { - if (!proactiveAppReview) { - return false; - } - - // Don't show if trigger is not set - if (!proactiveAppReview.trigger) { - return false; - } - + const [authTokenType] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); + const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector}); + + // Capture once so render stays pure (Date.now is impure). Fine for a 30-day cool-down gate. + const [timeAtMount] = useState(Date.now); + + let shouldShowModal = true; + if (authTokenType === CONST.AUTH_TOKEN_TYPES.SUPPORT || isActingAsDelegate) { + // Supportal agents and copilots should not leave reviews on behalf of another account. + shouldShowModal = false; + } else if (!proactiveAppReview?.trigger) { + // Don't show if the trigger is not set + shouldShowModal = false; + } else if (proactiveAppReview?.response === 'positive') { // Don't show again after user gave a positive response - if (proactiveAppReview.response && proactiveAppReview.response === 'positive') { - return false; + shouldShowModal = false; + } else if (proactiveAppReview?.lastPrompt) { + // Don't show again within 30 days of the last prompt + const lastPromptTime = new Date(proactiveAppReview.lastPrompt).getTime(); + const msSinceLastPrompt = timeAtMount - lastPromptTime; + if (msSinceLastPrompt < THIRTY_DAYS_IN_MS) { + shouldShowModal = false; } - - // Check if lastPrompt is missing or older than 30 days - if (proactiveAppReview.lastPrompt) { - const lastPromptTime = new Date(proactiveAppReview.lastPrompt).getTime(); - const now = Date.now(); - const daysSinceLastPrompt = (now - lastPromptTime) / THIRTY_DAYS_IN_MS; - - if (daysSinceLastPrompt < 1) { - return false; - } - } - - return true; - }, [proactiveAppReview]); + } return { shouldShowModal, diff --git a/tests/unit/useProactiveAppReviewTest.ts b/tests/unit/useProactiveAppReviewTest.ts new file mode 100644 index 000000000000..c571fcee3e95 --- /dev/null +++ b/tests/unit/useProactiveAppReviewTest.ts @@ -0,0 +1,56 @@ +import {renderHook, waitFor} from '@testing-library/react-native'; + +import CONST from '@src/CONST'; +import useProactiveAppReview from '@src/hooks/useProactiveAppReview'; +import ONYXKEYS from '@src/ONYXKEYS'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +describe('useProactiveAppReview', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + return waitForBatchedUpdates(); + }); + + beforeEach(() => { + Onyx.clear(); + return waitForBatchedUpdates(); + }); + + it('returns shouldShowModal true when a trigger is set for a normal session', async () => { + await Onyx.merge(ONYXKEYS.NVP_APP_REVIEW, {trigger: 'submit'}); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useProactiveAppReview()); + + await waitFor(() => { + expect(result.current.shouldShowModal).toBe(true); + }); + }); + + it('returns shouldShowModal false during a supportal session even when a trigger is set', async () => { + await Onyx.merge(ONYXKEYS.SESSION, {authTokenType: CONST.AUTH_TOKEN_TYPES.SUPPORT}); + await Onyx.merge(ONYXKEYS.NVP_APP_REVIEW, {trigger: 'submit'}); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useProactiveAppReview()); + + await waitFor(() => { + expect(result.current.shouldShowModal).toBe(false); + }); + }); + + it('returns shouldShowModal false when acting as a copilot even when a trigger is set', async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {delegatedAccess: {delegate: 'copilot@expensify.com'}}); + await Onyx.merge(ONYXKEYS.NVP_APP_REVIEW, {trigger: 'submit'}); + await waitForBatchedUpdates(); + + const {result} = renderHook(() => useProactiveAppReview()); + + await waitFor(() => { + expect(result.current.shouldShowModal).toBe(false); + }); + }); +});