diff --git a/CHANGELOG.md b/CHANGELOG.md index 769c99a972b..9ea69c499ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - fixed: Staked "locked" balance in the wallet view no longer gets cut off. The crypto amount is truncated to an exchange-rate-appropriate number of decimals, and the text is no longer clamped to a fraction of the card width. - fixed: Improve the unstake error experience by replacing the popup alert and generic "unknown error occurred" with the real error in the scene's error field, and showing a clear message when the wallet lacks the balance to cover the unstaking network fee. - fixed: Tapping Max on the Sell scene no longer briefly shows the entered fiat amount in the crypto field while the max is being calculated. +- fixed: Banxa Google Pay purchases now use Banxa's current Google Pay processor instead of the legacy one it replaced. Banxa keeps both live during a migration, and Edge was picking whichever processor had the lower id, which was always the legacy one. - fixed: An info card no longer disappears into an empty gap when the carousel's card list shrinks. A card's position comes entirely from an animated transform keyed on its index, and that transform is not re-applied when a surviving card shifts slots, so dropping a card left the ones after it parked a full card-width off-screen. The carousel now remounts a card whose slot changes. Reproduces wherever the list shrinks after mount - most visibly when a `noBalance` card is filtered out as balances finish loading. ## 4.50.2 (2026-08-06) diff --git a/src/__tests__/plugins/ramps/banxa/pickPreferredPayment.test.ts b/src/__tests__/plugins/ramps/banxa/pickPreferredPayment.test.ts new file mode 100644 index 00000000000..9810bcd6f52 --- /dev/null +++ b/src/__tests__/plugins/ramps/banxa/pickPreferredPayment.test.ts @@ -0,0 +1,54 @@ +import { describe, expect, it } from '@jest/globals' + +import type { BanxaPaymentIdLimit } from '../../../../plugins/ramps/banxa/banxaRampPlugin' +import { pickPreferredPayment } from '../../../../plugins/ramps/banxa/banxaRampPlugin' + +// Banxa's live USD Google Pay methods: four legacy WorldPay PSPs that all sort +// below the consolidated PRIMERGP PSP that replaced them. +const worldpayGoogleUsd: BanxaPaymentIdLimit = { + id: 6036, + paymentType: 'WORLDPAYGOOGLE', + type: 'googlepay', + min: '20', + max: '15000' +} +const primerGooglePay: BanxaPaymentIdLimit = { + id: 6142, + paymentType: 'PRIMERGP', + type: 'googlepay', + min: '20', + max: '15000' +} +const primerCredit: BanxaPaymentIdLimit = { + id: 6098, + paymentType: 'PRIMERCC', + type: 'credit', + min: '20', + max: '15000' +} + +describe('pickPreferredPayment', function () { + it('prefers the current PSP over the deprecated one it replaced', function () { + const payments = [worldpayGoogleUsd, primerGooglePay] + + expect(pickPreferredPayment(payments, 'googlepay')).toBe(primerGooglePay) + }) + + it('ignores the order Banxa payment ids happen to iterate in', function () { + const payments = [primerGooglePay, worldpayGoogleUsd] + + expect(pickPreferredPayment(payments, 'googlepay')).toBe(primerGooglePay) + }) + + it('falls back to the deprecated PSP when it is the only candidate', function () { + const payments = [worldpayGoogleUsd, primerCredit] + + expect(pickPreferredPayment(payments, 'googlepay')).toBe(worldpayGoogleUsd) + }) + + it('returns undefined when no payment method matches the type', function () { + const payments = [worldpayGoogleUsd, primerGooglePay] + + expect(pickPreferredPayment(payments, 'ach')).toBeUndefined() + }) +}) diff --git a/src/plugins/gui/providers/banxaProvider.ts b/src/plugins/gui/providers/banxaProvider.ts index 8b1d74e1a39..8d97b6c9615 100644 --- a/src/plugins/gui/providers/banxaProvider.ts +++ b/src/plugins/gui/providers/banxaProvider.ts @@ -289,6 +289,7 @@ const asBanxaStates = asObject({ interface BanxaPaymentIdLimit { id: number + paymentType: BanxaPaymentType type: FiatPaymentType min: string max: string @@ -1107,6 +1108,19 @@ const typeMap: Record = { ZHACHSELL: 'ach' } +/** + * Banxa PSPs that have been superseded by a newer PSP serving the same + * `FiatPaymentType`. They remain mapped because they still cover fiats and coins + * the replacement PSP does not, but they are only selected when no current PSP + * is available for the requested fiat/coin pair. + */ +const deprecatedPaymentTypes = new Set([ + // Superseded by PRIMERGP + 'WORLDPAYGOOGLE', + // Superseded by BRDGACHSELL + 'ZHACHSELL' +]) + // While this could use Array.find(), this is an inner loop routine used hundreds of times interating over // hundreds entries, so I'm opting for a more optimal for loop const findLimit = ( @@ -1148,6 +1162,7 @@ const buildPaymentsMap = ( // There shouldn't be an existing payment for this fiat/coin combo const newMap: BanxaPaymentIdLimit = { id: pm.id, + paymentType, min: limit.min, max: limit.max, type: pt @@ -1179,13 +1194,30 @@ const getPaymentIdLimit = ( ): BanxaPaymentIdLimit | undefined => { try { const payments = banxaPaymentsMap[direction][fiat][banxaCoin] - const paymentId = Object.values(payments).find(p => p.type === type) - return paymentId + return pickPreferredPayment(Object.values(payments), type) } catch (e) { return undefined } } +/** + * Banxa often exposes several payment methods for the same `FiatPaymentType`, + * because a replacement PSP runs alongside the PSP it supersedes until the old + * one is switched off. Always pick a current PSP, and only fall back to a + * deprecated one when it is the sole option for the fiat/coin pair. + */ +const pickPreferredPayment = ( + payments: BanxaPaymentIdLimit[], + type: FiatPaymentType +): BanxaPaymentIdLimit | undefined => { + const candidates = payments.filter(payment => payment.type === type) + return ( + candidates.find( + payment => !deprecatedPaymentTypes.has(payment.paymentType) + ) ?? candidates[0] + ) +} + // Takes an EdgeAsset and returns the corresponding Banxa chain code and coin code const edgeToBanxaCrypto = ( pluginId: string, diff --git a/src/plugins/ramps/banxa/banxaRampPlugin.ts b/src/plugins/ramps/banxa/banxaRampPlugin.ts index 2eeb886718a..0374ec05638 100644 --- a/src/plugins/ramps/banxa/banxaRampPlugin.ts +++ b/src/plugins/ramps/banxa/banxaRampPlugin.ts @@ -280,8 +280,9 @@ const ensureIsoPrefix = (currencyCode: string): string => { return currencyCode.startsWith('iso:') ? currencyCode : `iso:${currencyCode}` } -interface BanxaPaymentIdLimit { +export interface BanxaPaymentIdLimit { id: number + paymentType: BanxaPaymentType type: FiatPaymentType min: string max: string @@ -358,6 +359,19 @@ const typeMap: Record = { ZHACHSELL: 'ach' } +/** + * Banxa PSPs that have been superseded by a newer PSP serving the same + * `FiatPaymentType`. They remain mapped because they still cover fiats and coins + * the replacement PSP does not, but they are only selected when no current PSP + * is available for the requested fiat/coin pair. + */ +const deprecatedPaymentTypes = new Set([ + // Superseded by PRIMERGP + 'WORLDPAYGOOGLE', + // Superseded by BRDGACHSELL + 'ZHACHSELL' +]) + // Provider configuration cache interface ProviderConfigCache { data: { @@ -530,6 +544,7 @@ const buildPaymentsMap = ( } else { const newMap: BanxaPaymentIdLimit = { id: pm.id, + paymentType, min: limit.min, max: limit.max, type: pt @@ -562,11 +577,28 @@ const getPaymentIdLimit = ( ): BanxaPaymentIdLimit | undefined => { try { const payments = banxaPaymentsMap[direction][fiat][banxaCoin] - const paymentId = Object.values(payments).find(p => p.type === type) - return paymentId + return pickPreferredPayment(Object.values(payments), type) } catch (e) {} } +/** + * Banxa often exposes several payment methods for the same `FiatPaymentType`, + * because a replacement PSP runs alongside the PSP it supersedes until the old + * one is switched off. Always pick a current PSP, and only fall back to a + * deprecated one when it is the sole option for the fiat/coin pair. + */ +export const pickPreferredPayment = ( + payments: BanxaPaymentIdLimit[], + type: FiatPaymentType +): BanxaPaymentIdLimit | undefined => { + const candidates = payments.filter(payment => payment.type === type) + return ( + candidates.find( + payment => !deprecatedPaymentTypes.has(payment.paymentType) + ) ?? candidates[0] + ) +} + // Takes an EdgeAsset and returns the corresponding Banxa chain code and coin code const edgeToBanxaCrypto = ( pluginId: string,