diff --git a/CHANGELOG.md b/CHANGELOG.md index a23a01d2a62..59fc852d6bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - fixed: Password reminder no longer stops reappearing after unlocking Account Settings - fixed: Show the password recovery reminder at every balance milestone, including for funds that arrived while the app was closed or before the exchange rates loaded. - fixed: Hide the send scene's MAX button once a send has more than one recipient, and hide "Add another address" once MAX has been applied, so the two can no longer combine into an insufficient-funds transaction. +- fixed: Recognize promotions that only set a preferred exchange in the exchange settings, which now say the promotion is choosing it and offer to remove it, instead of showing a list whose taps were saved but silently overridden for the promotion's whole window ## 4.51.1 (2026-09-23) diff --git a/src/__tests__/util/ReferralHelpers.test.ts b/src/__tests__/util/ReferralHelpers.test.ts new file mode 100644 index 00000000000..c83764a156e --- /dev/null +++ b/src/__tests__/util/ReferralHelpers.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, it } from '@jest/globals' + +import type { AccountReferral, Promotion } from '../../types/ReferralTypes' +import type { PluginTweak } from '../../types/TweakTypes' +import { bestOfPlugins } from '../../util/ReferralHelpers' + +const now = new Date('2026-09-24T12:00:00Z') + +const tweak = ( + pluginId: string, + extra: Partial = {} +): PluginTweak => ({ + pluginId, + disabled: false, + startDate: new Date('2026-09-20T00:00:00Z'), + durationDays: 14, + ...extra +}) + +const promo = (installerId: string, plugins: PluginTweak[]): Promotion => ({ + installerId, + hiddenMessages: {}, + messages: [], + plugins +}) + +const referral = (promotions: Promotion[]): AccountReferral => ({ + promotions, + ignoreAccountSwap: false, + hiddenAccountMessages: {}, + activePromotions: [] +}) + +describe('bestOfPlugins', () => { + it('lets a promotion outrank the preferred exchange from the settings', () => { + const out = bestOfPlugins( + [], + referral([ + promo('nexchange_test', [tweak('nexchange', { preferredSwap: true })]) + ]), + 'changenow', + now + ) + expect(out.preferredSwapPluginId).toBe('nexchange') + }) + + it('reports the promotion as the source when it only sets an exchange preference', () => { + const out = bestOfPlugins( + [], + referral([ + promo('nexchange_test', [tweak('nexchange', { preferredSwap: true })]) + ]), + 'changenow', + now + ) + expect(out.swapSource).toEqual({ + type: 'promotion', + installerId: 'nexchange_test' + }) + }) + + it('still reports the promotion when it sets both preferences', () => { + const out = bestOfPlugins( + [], + referral([ + promo('nexchange_test', [ + tweak('nexchange', { preferredSwap: true, preferredFiat: true }) + ]) + ]), + undefined, + now + ) + expect(out.preferredSwapPluginId).toBe('nexchange') + expect(out.swapSource).toEqual({ + type: 'promotion', + installerId: 'nexchange_test' + }) + }) + + it('does not blame a promotion that sets no exchange preference', () => { + // A buy/sell-only promotion, with nothing else preferring an exchange: + // undefined equals undefined, so the gate needs its second test. + const out = bestOfPlugins( + [], + referral([ + promo('moonpay_promo', [tweak('moonpay', { preferredFiat: true })]) + ]), + undefined, + now + ) + expect(out.preferredSwapPluginId).toBeUndefined() + expect(out.swapSource.type).not.toBe('promotion') + }) + + it('ignores an expired promotion entirely', () => { + const stale = tweak('nexchange', { + preferredSwap: true, + startDate: new Date('2026-08-01T00:00:00Z'), + durationDays: 7 + }) + const out = bestOfPlugins( + [], + referral([promo('old_promo', [stale])]), + 'changenow', + now + ) + expect(out.preferredSwapPluginId).toBe('changenow') + expect(out.swapSource.type).not.toBe('promotion') + }) +}) diff --git a/src/util/ReferralHelpers.ts b/src/util/ReferralHelpers.ts index 188052f506c..a5a8c5c79c4 100644 --- a/src/util/ReferralHelpers.ts +++ b/src/util/ReferralHelpers.ts @@ -99,9 +99,13 @@ export function bestOfPlugins( for (const promo of accountReferral.promotions) { const fromPromo = summarizePlugins(promo.plugins, now) out = mergePluginSummaries(out, fromPromo) + // This promotion is the source when it names the swap plugin that won. + // The second test keeps a promotion with no swap preference from claiming + // the source when nothing else has one either, since undefined equals + // undefined: if ( out.preferredSwapPluginId === fromPromo.preferredSwapPluginId && - fromPromo.preferredFiatPluginId != null + fromPromo.preferredSwapPluginId != null ) { swapSource = { type: 'promotion', installerId: promo.installerId } }