From cf59cf5be71ff81bccb1fb7d8e8d7a3259992a80 Mon Sep 17 00:00:00 2001 From: Alejandro Akbal <37181533+AlejandroAkbal@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:38:17 +0700 Subject: [PATCH] fix: show one automatic modal per startup --- app/composables/useDialogManagerState.ts | 27 ++++++-- test/assets/dialog-manager-state.test.ts | 88 ++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 test/assets/dialog-manager-state.test.ts diff --git a/app/composables/useDialogManagerState.ts b/app/composables/useDialogManagerState.ts index 0667e384..104126a4 100644 --- a/app/composables/useDialogManagerState.ts +++ b/app/composables/useDialogManagerState.ts @@ -1,4 +1,17 @@ export type DialogManagerDialog = 'premium' | 'installPwa' | 'feedback' | 'newsletter' | 'review' +type AutomaticDialog = Exclude + +export function resolvePendingDialog( + isPremiumDialogOpen: boolean, + automaticDialog: AutomaticDialog | undefined, + hasConsumedAutomaticDialog: boolean +): DialogManagerDialog | undefined { + if (isPremiumDialogOpen) { + return 'premium' + } + + return hasConsumedAutomaticDialog ? undefined : automaticDialog +} export function useDialogManagerState() { const { timesTheAppHasBeenOpened, promptInstallPwa, promptFeedback, promptNewsletter, promptReview } = @@ -12,11 +25,8 @@ export function useDialogManagerState() { }) } - const pendingDialog = computed(() => { - if (isPremiumDialogOpen.value) { - return 'premium' - } - + const hasConsumedAutomaticDialog = useState('dialog-manager-has-consumed-automatic', () => false) + const automaticDialog = computed(() => { if (timesTheAppHasBeenOpened.value >= 3 && !promptInstallPwa.value && !isStandaloneDisplayMode.value) { return 'installPwa' } @@ -33,6 +43,9 @@ export function useDialogManagerState() { return 'review' } }) + const pendingDialog = computed(() => + resolvePendingDialog(isPremiumDialogOpen.value, automaticDialog.value, hasConsumedAutomaticDialog.value) + ) function closeDialog(dialog = pendingDialog.value) { switch (dialog) { @@ -42,18 +55,22 @@ export function useDialogManagerState() { case 'installPwa': promptInstallPwa.value = true + hasConsumedAutomaticDialog.value = true break case 'feedback': promptFeedback.value = true + hasConsumedAutomaticDialog.value = true break case 'newsletter': promptNewsletter.value = true + hasConsumedAutomaticDialog.value = true break case 'review': promptReview.value = true + hasConsumedAutomaticDialog.value = true break } } diff --git a/test/assets/dialog-manager-state.test.ts b/test/assets/dialog-manager-state.test.ts new file mode 100644 index 00000000..2dffde55 --- /dev/null +++ b/test/assets/dialog-manager-state.test.ts @@ -0,0 +1,88 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { resolvePendingDialog, useDialogManagerState } from '../../app/composables/useDialogManagerState' + +type State = { value: T } + +const states = new Map>() +const statistics = { + timesTheAppHasBeenOpened: { value: 30 }, + promptInstallPwa: { value: false }, + promptFeedback: { value: false }, + promptNewsletter: { value: false }, + promptReview: { value: false } +} +const premium = { value: false } + +beforeEach(() => { + states.clear() + statistics.timesTheAppHasBeenOpened.value = 30 + statistics.promptInstallPwa.value = false + statistics.promptFeedback.value = false + statistics.promptNewsletter.value = false + statistics.promptReview.value = false + premium.value = false + + vi.stubGlobal('computed', (getter: () => T) => ({ + get value() { + return getter() + } + })) + vi.stubGlobal('useState', (key: string, init: () => T) => { + if (!states.has(key)) { + states.set(key, { value: init() }) + } + + return states.get(key) as State + }) + vi.stubGlobal('useAppStatistics', () => statistics) + vi.stubGlobal('usePremiumDialog', () => ({ open: premium })) +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('resolvePendingDialog', () => { + it('does not select another automatic dialog after one has been shown', () => { + expect(resolvePendingDialog(false, 'feedback', true)).toBeUndefined() + }) + + it('allows premium after an automatic dialog has been shown', () => { + expect(resolvePendingDialog(true, 'feedback', true)).toBe('premium') + }) +}) + +describe('useDialogManagerState', () => { + it('does not cascade or reset automatic dialogs when recreated during one startup', () => { + const firstManager = useDialogManagerState() + + expect(firstManager.pendingDialog.value).toBe('installPwa') + firstManager.closeDialog() + expect(firstManager.pendingDialog.value).toBeUndefined() + + const recreatedManager = useDialogManagerState() + + expect(recreatedManager.pendingDialog.value).toBeUndefined() + }) + + it('lets premium preempt and restore an unconsumed automatic dialog', () => { + const manager = useDialogManagerState() + + premium.value = true + expect(manager.pendingDialog.value).toBe('premium') + + manager.closeDialog() + expect(manager.pendingDialog.value).toBe('installPwa') + }) + + it('lets premium open after an automatic dialog has been consumed', () => { + const manager = useDialogManagerState() + + manager.closeDialog() + premium.value = true + expect(manager.pendingDialog.value).toBe('premium') + + manager.closeDialog() + expect(manager.pendingDialog.value).toBeUndefined() + }) +})