Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
110 changes: 110 additions & 0 deletions src/__tests__/util/ReferralHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}
): 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')
})
})
6 changes: 5 additions & 1 deletion src/util/ReferralHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
Loading