From d661a7f9f04551823395a85c08ef9b3e58050b52 Mon Sep 17 00:00:00 2001 From: peachbits Date: Wed, 23 Sep 2026 23:12:01 -0700 Subject: [PATCH] Recognize exchange-only promotions in the swap settings The swap settings switch to a promotion-controlled mode when a promotion is choosing the preferred exchange: they say so, replace the tappable list with the promoted provider, and offer to remove the promotion. The gate that decides this asked whether the promotion set a preferred *fiat* plugin, so a promotion that only set an exchange preference never triggered it. The settings kept showing the normal list, a tap on another provider was saved but silently overridden for the promotion's whole window, and the only way out was the promotion settings screen. Test the swap preference instead. The second half of the condition is still needed: with no exchange preference anywhere, undefined equals undefined, and a buy/sell-only promotion would otherwise claim the source. bestOfPlugins had no tests, so add the cases that pin this. --- CHANGELOG.md | 1 + src/__tests__/util/ReferralHelpers.test.ts | 110 +++++++++++++++++++++ src/util/ReferralHelpers.ts | 6 +- 3 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/util/ReferralHelpers.test.ts 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 } }