diff --git a/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue b/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue index 988de67509..e2f84e996f 100644 --- a/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue +++ b/frontend/src/components/user/profile/ProfileBalanceNotifyCard.vue @@ -203,6 +203,7 @@ const verifyingSaved = ref(false) const sendingSavedCode = ref(false) const verifyCountdown = ref(0) let verifyTimer: ReturnType | null = null +let disposed = false const canAddMore = computed(() => { return emailEntries.value.length + pendingEmails.value.length < maxTotalEmails @@ -220,6 +221,7 @@ onMounted(() => { }) onUnmounted(() => { + disposed = true for (const pe of pendingEmails.value) { if (pe.timer) clearInterval(pe.timer) } @@ -281,6 +283,7 @@ async function sendCodeFor(idx: number) { pe.sending = true try { await userAPI.sendNotifyEmailCode(pe.email) + if (disposed || !pendingEmails.value.includes(pe)) return pe.codeSent = true pe.countdown = 60 pe.timer = setInterval(() => { @@ -334,6 +337,7 @@ async function sendCodeForSaved(email: string) { sendingSavedCode.value = true try { await userAPI.sendNotifyEmailCode(email) + if (disposed) return verifyingEmail.value = email verifyCode.value = '' verifyCountdown.value = 60 diff --git a/frontend/src/components/user/profile/__tests__/ProfileBalanceNotifyCard.spec.ts b/frontend/src/components/user/profile/__tests__/ProfileBalanceNotifyCard.spec.ts index 395d8ae23a..03bb8cffeb 100644 --- a/frontend/src/components/user/profile/__tests__/ProfileBalanceNotifyCard.spec.ts +++ b/frontend/src/components/user/profile/__tests__/ProfileBalanceNotifyCard.spec.ts @@ -44,6 +44,39 @@ describe('ProfileBalanceNotifyCard', () => { vi.useRealTimers() }) + it.each(['remove', 'unmount'])('does not start a pending email timer after %s', async (action) => { + const request = deferred() + sendNotifyEmailCode.mockReturnValueOnce(request.promise) + const wrapper = mount(ProfileBalanceNotifyCard, { + props: { enabled: true, threshold: null, extraEmails: [], systemDefaultThreshold: 5, userEmail: '' } + }) + await wrapper.get('input[type="email"]').setValue('new@example.com') + await button(wrapper, 'common.add').trigger('click') + await button(wrapper, 'profile.balanceNotify.sendCode').trigger('click') + if (action === 'remove') await button(wrapper, 'profile.balanceNotify.removeEmail').trigger('click') + else wrapper.unmount() + + request.resolve() + await flushPromises() + expect(vi.getTimerCount()).toBe(0) + }) + + it('does not start a saved email timer after unmount', async () => { + const request = deferred() + sendNotifyEmailCode.mockReturnValueOnce(request.promise) + const wrapper = mount(ProfileBalanceNotifyCard, { + props: { + enabled: true, threshold: null, systemDefaultThreshold: 5, userEmail: '', + extraEmails: [{ email: 'saved@example.com', disabled: false, verified: false }] + } + }) + await button(wrapper, 'profile.balanceNotify.verify').trigger('click') + wrapper.unmount() + request.resolve() + await flushPromises() + expect(vi.getTimerCount()).toBe(0) + }) + it.each([0, 1])('removes only verified emails when request %i finishes first', async (first) => { const emails = ['first@example.com', 'second@example.com', 'third@example.com'] const requests = [deferred(), deferred()]