From 39922f2b6d065be84904daafa28cd930074e4e8c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 15 Jul 2026 04:28:55 +0000 Subject: [PATCH 1/6] fix(ramps): align Transak checkout WebView with pure-black BottomSheet (TMCU-1087) Use getElevatedSurfaceColor for Checkout WebView chrome so pure-black dark mode uses bg-alternative and light mode uses bg-default, matching MMDS BottomSheet elevated surfaces. When pure black is enabled, append Transak widget theme params to the buy URL before fetching the provider widget so the hosted checkout background matches the in-app sheet instead of Transak's default charcoal. Co-authored-by: George Marshall --- .../Views/Checkout/Checkout.styles.ts | 3 +- .../UI/Ramp/Views/Checkout/Checkout.styles.ts | 3 +- .../UI/Ramp/hooks/useContinueWithQuote.ts | 11 ++- .../utils/buildQuoteWithRedirectUrl.test.ts | 96 ++++++++++++++++++- .../Ramp/utils/buildQuoteWithRedirectUrl.ts | 51 ++++++++++ .../UI/Ramp/utils/depositUtils.test.ts | 72 ++++++++++++++ app/components/UI/Ramp/utils/depositUtils.ts | 33 +++++-- 7 files changed, 257 insertions(+), 12 deletions(-) diff --git a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts index df5fea79316..3ef49091603 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts +++ b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts @@ -1,5 +1,6 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../../util/theme/models'; +import { getElevatedSurfaceColor } from '../../../../../../util/theme/themeUtils'; const styleSheet = (params: { theme: Theme }) => StyleSheet.create({ @@ -7,7 +8,7 @@ const styleSheet = (params: { theme: Theme }) => paddingVertical: 0, }, webview: { - backgroundColor: params.theme.colors.background.default, + backgroundColor: getElevatedSurfaceColor(params.theme), }, }); diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts b/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts index f93576d81a8..16e2bcf2737 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts @@ -1,5 +1,6 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../util/theme/models'; +import { getElevatedSurfaceColor } from '../../../../../util/theme/themeUtils'; const styleSheet = (params: { theme: Theme }) => StyleSheet.create({ @@ -7,7 +8,7 @@ const styleSheet = (params: { theme: Theme }) => paddingVertical: 0, }, webview: { - backgroundColor: params.theme.colors.background.default, + backgroundColor: getElevatedSurfaceColor(params.theme), }, }); diff --git a/app/components/UI/Ramp/hooks/useContinueWithQuote.ts b/app/components/UI/Ramp/hooks/useContinueWithQuote.ts index ddf0084b155..f960f8a5bde 100644 --- a/app/components/UI/Ramp/hooks/useContinueWithQuote.ts +++ b/app/components/UI/Ramp/hooks/useContinueWithQuote.ts @@ -9,9 +9,10 @@ import { strings } from '../../../../../locales/i18n'; import { FIAT_ORDER_PROVIDERS } from '../../../../constants/on-ramp'; import { selectHasAgreedTransakNativePolicy } from '../../../../reducers/fiatOrders'; import Device from '../../../../util/device'; +import { useTheme } from '../../../../util/theme'; import { - buildQuoteWithRedirectUrl, + buildQuoteWithWidgetTheme, getCheckoutContext, getWidgetRedirectConfig, } from '../utils/buildQuoteWithRedirectUrl'; @@ -123,6 +124,7 @@ export function useContinueWithQuote( const hasAgreedTransakNativePolicy = useSelector( selectHasAgreedTransakNativePolicy, ); + const theme = useTheme(); const currency = userRegion?.country?.currency || 'USD'; @@ -252,7 +254,11 @@ export function useContinueWithQuote( ); useExternalBrowser = redirectConfig.useExternalBrowser; redirectUrl = redirectConfig.redirectUrl; - const quoteForWidget = buildQuoteWithRedirectUrl(quote, redirectUrl); + const quoteForWidget = buildQuoteWithWidgetTheme( + quote, + redirectUrl, + theme, + ); buyWidget = await getBuyWidgetData(quoteForWidget); } catch (error) { throw new Error( @@ -370,6 +376,7 @@ export function useContinueWithQuote( getBuyWidgetData, addPrecreatedOrder, navigateAfterExternalBrowser, + theme, ], ); diff --git a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts index bd0f2c7fb20..5eb7e494409 100644 --- a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts +++ b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts @@ -1,17 +1,48 @@ import type { Quote } from '@metamask/ramps-controller'; +import { brandColor, darkTheme, lightTheme } from '@metamask/design-tokens'; import { + appendWidgetThemeToBuyUrl, + buildQuoteWithWidgetTheme, getCheckoutContext, getAggregatorRedirectConfig, getWidgetRedirectConfig, } from './buildQuoteWithRedirectUrl'; +import { AppThemeKey, type Theme } from '../../../../util/theme/models'; + +let mockIsPureBlackEnabled = false; jest.mock('./getRampCallbackBaseUrl', () => ({ getRampCallbackBaseUrl: () => 'https://callback.example/base', })); -const makeQuote = (browser?: 'APP_BROWSER' | 'IN_APP_OS_BROWSER'): Quote => +jest.mock('../../../../util/theme/themeUtils', () => ({ + get isPureBlackEnabled() { + return mockIsPureBlackEnabled; + }, +})); + +const createTheme = ( + themeAppearance: AppThemeKey.light | AppThemeKey.dark, +): Theme => { + const base = themeAppearance === AppThemeKey.dark ? darkTheme : lightTheme; + return { + colors: base.colors, + themeAppearance, + typography: base.typography, + shadows: base.shadows, + brandColors: brandColor, + }; +}; + +const makeQuote = ( + browser?: 'APP_BROWSER' | 'IN_APP_OS_BROWSER', + buyURL = 'https://on-ramp.example/providers/transak/buy', +): Quote => ({ - quote: browser ? { buyWidget: { browser } } : {}, + quote: { + ...(browser ? { buyWidget: { browser } } : {}), + buyURL, + }, }) as unknown as Quote; describe('getCheckoutContext', () => { @@ -90,3 +121,64 @@ describe('getWidgetRedirectConfig', () => { expect(result.redirectUrl).toBe('https://callback.example/base'); }); }); + +describe('appendWidgetThemeToBuyUrl', () => { + beforeEach(() => { + mockIsPureBlackEnabled = false; + }); + + it('returns the original buy URL when pure black is disabled', () => { + const buyURL = 'https://on-ramp.example/providers/transak/buy'; + + expect( + appendWidgetThemeToBuyUrl(buyURL, createTheme(AppThemeKey.dark)), + ).toBe(buyURL); + }); + + it('appends widget theme params when pure black is enabled', () => { + mockIsPureBlackEnabled = true; + const theme = createTheme(AppThemeKey.dark); + const result = appendWidgetThemeToBuyUrl( + 'https://on-ramp.example/providers/transak/buy', + theme, + ); + const parsed = new URL(result); + + expect(parsed.searchParams.get('colorMode')).toBe('DARK'); + expect(parsed.searchParams.get('widgetBackgroundFillColor')).toBe( + theme.colors.background.alternative, + ); + expect(parsed.searchParams.get('backgroundColors')).toBe( + [ + theme.colors.background.alternative, + theme.colors.background.alternative, + theme.colors.background.muted, + ].join(','), + ); + }); +}); + +describe('buildQuoteWithWidgetTheme', () => { + beforeEach(() => { + mockIsPureBlackEnabled = false; + }); + + it('rewrites redirect and theme params on the quote buy URL', () => { + mockIsPureBlackEnabled = true; + const theme = createTheme(AppThemeKey.dark); + const quote = makeQuote('APP_BROWSER'); + const result = buildQuoteWithWidgetTheme( + quote, + 'https://callback.example/base', + theme, + ); + const parsed = new URL(result.quote?.buyURL ?? ''); + + expect(parsed.searchParams.get('redirectUrl')).toBe( + 'https://callback.example/base', + ); + expect(parsed.searchParams.get('widgetBackgroundFillColor')).toBe( + theme.colors.background.alternative, + ); + }); +}); diff --git a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts index 32ffe48814d..830a36df28e 100644 --- a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts +++ b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts @@ -1,4 +1,7 @@ import { isExternalBrowserQuote, type Quote } from '@metamask/ramps-controller'; +import type { Theme } from '../../../../util/theme/models'; +import { isPureBlackEnabled } from '../../../../util/theme/themeUtils'; +import { generateThemeParameters } from './depositUtils'; import { getRampCallbackBaseUrl } from './getRampCallbackBaseUrl'; /** @@ -24,6 +27,54 @@ export function buildQuoteWithRedirectUrl( }; } +/** + * Appends Transak widget theme query params to a buy URL when pure black is + * enabled so provider-hosted checkout surfaces match the elevated BottomSheet. + */ +export function appendWidgetThemeToBuyUrl( + buyURL: string, + theme: Theme, +): string { + if (!isPureBlackEnabled) { + return buyURL; + } + + const buyUrl = new URL(buyURL); + const themeParams = generateThemeParameters( + theme.themeAppearance, + theme.colors, + ); + + Object.entries(themeParams).forEach(([key, value]) => { + buyUrl.searchParams.set(key, value); + }); + + return buyUrl.toString(); +} + +/** + * Rewrites a quote's buy URL with redirect + optional pure-black widget theme. + */ +export function buildQuoteWithWidgetTheme( + quote: Quote, + redirectUrl: string, + theme: Theme, +): Quote { + const quoteWithRedirect = buildQuoteWithRedirectUrl(quote, redirectUrl); + const buyURL = quoteWithRedirect.quote?.buyURL; + if (!buyURL) { + return quoteWithRedirect; + } + + return { + ...quoteWithRedirect, + quote: { + ...quoteWithRedirect.quote, + buyURL: appendWidgetThemeToBuyUrl(buyURL, theme), + }, + }; +} + function getProviderDeeplinkRedirectUrl(providerCode: string): string { return `metamask://on-ramp/providers/${providerCode}`; } diff --git a/app/components/UI/Ramp/utils/depositUtils.test.ts b/app/components/UI/Ramp/utils/depositUtils.test.ts index a7c770d9b7a..5c77d9a905e 100644 --- a/app/components/UI/Ramp/utils/depositUtils.test.ts +++ b/app/components/UI/Ramp/utils/depositUtils.test.ts @@ -16,6 +16,14 @@ import { mockTheme } from '../../../../util/theme'; import { AppThemeKey } from '../../../../util/theme/models'; import { MOCK_ETH_TOKEN } from '../testUtils/constants'; +let mockIsPureBlackEnabled = false; + +jest.mock('../../../../util/theme/themeUtils', () => ({ + get isPureBlackEnabled() { + return mockIsPureBlackEnabled; + }, +})); + jest.mock('../../../../../locales/i18n', () => ({ strings: jest.fn(), })); @@ -219,6 +227,10 @@ describe('generateThemeParameters', () => { themeAppearance: AppThemeKey.dark, }; + beforeEach(() => { + mockIsPureBlackEnabled = false; + }); + it('should generate correct theme parameters for light mode', () => { const themeAppearance = AppThemeKey.light; const colors = mockTheme.colors; @@ -274,6 +286,66 @@ describe('generateThemeParameters', () => { surfaceFillColor: colors.background.muted, }); }); + + it('adds elevated widget background for pure black dark mode', () => { + mockIsPureBlackEnabled = true; + const colors = mockDarkTheme.colors; + const result = generateThemeParameters(AppThemeKey.dark, colors); + + expect(result).toEqual({ + themeColor: colors.primary.default, + colorMode: 'DARK', + backgroundColors: [ + colors.background.alternative, + colors.background.alternative, + colors.background.muted, + ].join(','), + textColors: [ + colors.text.default, + colors.text.default, + colors.text.alternative, + ].join(','), + borderColors: [ + colors.border.default, + colors.border.muted, + colors.border.muted, + ].join(','), + primaryButtonFillColor: colors.icon.default, + primaryButtonTextColor: colors.icon.inverse, + surfaceFillColor: colors.background.muted, + widgetBackgroundFillColor: colors.background.alternative, + }); + }); + + it('keeps default widget background for pure black light mode', () => { + mockIsPureBlackEnabled = true; + const colors = mockTheme.colors; + const result = generateThemeParameters(AppThemeKey.light, colors); + + expect(result).toEqual({ + themeColor: colors.primary.default, + colorMode: 'LIGHT', + backgroundColors: [ + colors.background.default, + colors.background.default, + colors.background.muted, + ].join(','), + textColors: [ + colors.text.default, + colors.text.default, + colors.text.alternative, + ].join(','), + borderColors: [ + colors.border.default, + colors.border.muted, + colors.border.muted, + ].join(','), + primaryButtonFillColor: colors.icon.default, + primaryButtonTextColor: colors.icon.inverse, + surfaceFillColor: colors.background.muted, + widgetBackgroundFillColor: colors.background.default, + }); + }); }); describe('timestampToTransakFormat', () => { diff --git a/app/components/UI/Ramp/utils/depositUtils.ts b/app/components/UI/Ramp/utils/depositUtils.ts index bcadaaabdb3..6b6739d9b38 100644 --- a/app/components/UI/Ramp/utils/depositUtils.ts +++ b/app/components/UI/Ramp/utils/depositUtils.ts @@ -4,6 +4,7 @@ import { FIAT_ORDER_STATES } from '../../../../constants/on-ramp'; import { renderNumber } from '../../../../util/number'; import { strings } from '../../../../../locales/i18n'; import { AppThemeKey, Colors } from '../../../../util/theme/models'; +import { isPureBlackEnabled } from '../../../../util/theme/themeUtils'; const emailRegex = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; @@ -105,11 +106,22 @@ export const generateThemeParameters = ( themeAppearance: AppThemeKey, colors: Colors, ) => { - const backgroundColors = [ - colors.background.default, - colors.background.default, - colors.background.alternative, - ].join(','); + const widgetBackgroundFillColor = + isPureBlackEnabled && themeAppearance === AppThemeKey.dark + ? colors.background.alternative + : colors.background.default; + + const backgroundColors = isPureBlackEnabled + ? [ + widgetBackgroundFillColor, + widgetBackgroundFillColor, + colors.background.muted, + ].join(',') + : [ + colors.background.default, + colors.background.default, + colors.background.alternative, + ].join(','); const textColors = [ colors.text.default, @@ -123,7 +135,7 @@ export const generateThemeParameters = ( colors.border.muted, ].join(','); - return { + const themeParameters = { themeColor: colors.primary.default, colorMode: themeAppearance === AppThemeKey.light ? 'LIGHT' : 'DARK', backgroundColors, @@ -133,6 +145,15 @@ export const generateThemeParameters = ( primaryButtonTextColor: colors.icon.inverse, surfaceFillColor: colors.background.muted, }; + + if (!isPureBlackEnabled) { + return themeParameters; + } + + return { + ...themeParameters, + widgetBackgroundFillColor, + }; }; /** From abb86f3282bcbe3b9442956340a1c716814dcd05 Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Wed, 15 Jul 2026 14:40:58 -0700 Subject: [PATCH 2/6] fix(ramps): match Transak WebView background to theme colors (TMCU-1087) Set BottomSheet and WebView background to Transak-specific colors (#1a1a1a dark / #ffffff light) so the sheet matches the iframe content and eliminates the visible color mismatch on open. --- .../UI/Ramp/Views/Checkout/Checkout.styles.ts | 14 +++++++++----- app/components/UI/Ramp/Views/Checkout/Checkout.tsx | 9 +++++++++ app/styles/common.ts | 2 ++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts b/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts index 16e2bcf2737..bcb7acf465a 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts @@ -1,15 +1,19 @@ import { StyleSheet } from 'react-native'; -import { Theme } from '../../../../../util/theme/models'; -import { getElevatedSurfaceColor } from '../../../../../util/theme/themeUtils'; +import { Theme, AppThemeKey } from '../../../../../util/theme/models'; +import { colors } from '../../../../../styles/common'; -const styleSheet = (params: { theme: Theme }) => - StyleSheet.create({ +const styleSheet = (params: { theme: Theme }) => { + const isDark = params.theme.themeAppearance === AppThemeKey.dark; + return StyleSheet.create({ headerWithoutPadding: { paddingVertical: 0, }, webview: { - backgroundColor: getElevatedSurfaceColor(params.theme), + backgroundColor: isDark + ? colors.transakBackgroundDark + : colors.transakBackgroundLight, }, }); +}; export default styleSheet; diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx index 36112af360f..a44254cac29 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx @@ -44,6 +44,9 @@ import { } from '../../headless/headlessEntryNavigation'; import { useStyles } from '../../../../hooks/useStyles'; import styleSheet from './Checkout.styles'; +import { useTheme } from '../../../../../util/theme'; +import { AppThemeKey } from '../../../../../util/theme/models'; +import { colors } from '../../../../../styles/common'; import Device from '../../../../../util/device'; import { shouldStartLoadWithRequest } from '../../../../../util/browser'; import { CHECKOUT_TEST_IDS } from './Checkout.testIds'; @@ -99,6 +102,11 @@ const Checkout = () => { const navigation = useNavigation(); const params = useParams(); const { styles } = useStyles(styleSheet, {}); + const { themeAppearance } = useTheme(); + const transakBgClassName = + themeAppearance === AppThemeKey.dark + ? `bg-[${colors.transakBackgroundDark}]` + : `bg-[${colors.transakBackgroundLight}]`; const { addOrder, addPrecreatedOrder, getOrderFromCallback } = useRampsOrders(); const { trackEvent, createEventBuilder } = useAnalytics(); @@ -695,6 +703,7 @@ const Checkout = () => { isFullscreen isInteractable={!Device.isAndroid()} keyboardAvoidingViewEnabled={false} + twClassName={transakBgClassName} > {sharedHeader} Date: Wed, 15 Jul 2026 14:47:07 -0700 Subject: [PATCH 3/6] revert: remove unrelated ramp utils/hooks changes --- .../Views/Checkout/Checkout.styles.ts | 3 +- .../UI/Ramp/hooks/useContinueWithQuote.ts | 11 +-- .../utils/buildQuoteWithRedirectUrl.test.ts | 96 +------------------ .../Ramp/utils/buildQuoteWithRedirectUrl.ts | 51 ---------- .../UI/Ramp/utils/depositUtils.test.ts | 72 -------------- app/components/UI/Ramp/utils/depositUtils.ts | 33 ++----- 6 files changed, 11 insertions(+), 255 deletions(-) diff --git a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts index 3ef49091603..df5fea79316 100644 --- a/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts +++ b/app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.styles.ts @@ -1,6 +1,5 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../../util/theme/models'; -import { getElevatedSurfaceColor } from '../../../../../../util/theme/themeUtils'; const styleSheet = (params: { theme: Theme }) => StyleSheet.create({ @@ -8,7 +7,7 @@ const styleSheet = (params: { theme: Theme }) => paddingVertical: 0, }, webview: { - backgroundColor: getElevatedSurfaceColor(params.theme), + backgroundColor: params.theme.colors.background.default, }, }); diff --git a/app/components/UI/Ramp/hooks/useContinueWithQuote.ts b/app/components/UI/Ramp/hooks/useContinueWithQuote.ts index f960f8a5bde..ddf0084b155 100644 --- a/app/components/UI/Ramp/hooks/useContinueWithQuote.ts +++ b/app/components/UI/Ramp/hooks/useContinueWithQuote.ts @@ -9,10 +9,9 @@ import { strings } from '../../../../../locales/i18n'; import { FIAT_ORDER_PROVIDERS } from '../../../../constants/on-ramp'; import { selectHasAgreedTransakNativePolicy } from '../../../../reducers/fiatOrders'; import Device from '../../../../util/device'; -import { useTheme } from '../../../../util/theme'; import { - buildQuoteWithWidgetTheme, + buildQuoteWithRedirectUrl, getCheckoutContext, getWidgetRedirectConfig, } from '../utils/buildQuoteWithRedirectUrl'; @@ -124,7 +123,6 @@ export function useContinueWithQuote( const hasAgreedTransakNativePolicy = useSelector( selectHasAgreedTransakNativePolicy, ); - const theme = useTheme(); const currency = userRegion?.country?.currency || 'USD'; @@ -254,11 +252,7 @@ export function useContinueWithQuote( ); useExternalBrowser = redirectConfig.useExternalBrowser; redirectUrl = redirectConfig.redirectUrl; - const quoteForWidget = buildQuoteWithWidgetTheme( - quote, - redirectUrl, - theme, - ); + const quoteForWidget = buildQuoteWithRedirectUrl(quote, redirectUrl); buyWidget = await getBuyWidgetData(quoteForWidget); } catch (error) { throw new Error( @@ -376,7 +370,6 @@ export function useContinueWithQuote( getBuyWidgetData, addPrecreatedOrder, navigateAfterExternalBrowser, - theme, ], ); diff --git a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts index 5eb7e494409..bd0f2c7fb20 100644 --- a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts +++ b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.test.ts @@ -1,48 +1,17 @@ import type { Quote } from '@metamask/ramps-controller'; -import { brandColor, darkTheme, lightTheme } from '@metamask/design-tokens'; import { - appendWidgetThemeToBuyUrl, - buildQuoteWithWidgetTheme, getCheckoutContext, getAggregatorRedirectConfig, getWidgetRedirectConfig, } from './buildQuoteWithRedirectUrl'; -import { AppThemeKey, type Theme } from '../../../../util/theme/models'; - -let mockIsPureBlackEnabled = false; jest.mock('./getRampCallbackBaseUrl', () => ({ getRampCallbackBaseUrl: () => 'https://callback.example/base', })); -jest.mock('../../../../util/theme/themeUtils', () => ({ - get isPureBlackEnabled() { - return mockIsPureBlackEnabled; - }, -})); - -const createTheme = ( - themeAppearance: AppThemeKey.light | AppThemeKey.dark, -): Theme => { - const base = themeAppearance === AppThemeKey.dark ? darkTheme : lightTheme; - return { - colors: base.colors, - themeAppearance, - typography: base.typography, - shadows: base.shadows, - brandColors: brandColor, - }; -}; - -const makeQuote = ( - browser?: 'APP_BROWSER' | 'IN_APP_OS_BROWSER', - buyURL = 'https://on-ramp.example/providers/transak/buy', -): Quote => +const makeQuote = (browser?: 'APP_BROWSER' | 'IN_APP_OS_BROWSER'): Quote => ({ - quote: { - ...(browser ? { buyWidget: { browser } } : {}), - buyURL, - }, + quote: browser ? { buyWidget: { browser } } : {}, }) as unknown as Quote; describe('getCheckoutContext', () => { @@ -121,64 +90,3 @@ describe('getWidgetRedirectConfig', () => { expect(result.redirectUrl).toBe('https://callback.example/base'); }); }); - -describe('appendWidgetThemeToBuyUrl', () => { - beforeEach(() => { - mockIsPureBlackEnabled = false; - }); - - it('returns the original buy URL when pure black is disabled', () => { - const buyURL = 'https://on-ramp.example/providers/transak/buy'; - - expect( - appendWidgetThemeToBuyUrl(buyURL, createTheme(AppThemeKey.dark)), - ).toBe(buyURL); - }); - - it('appends widget theme params when pure black is enabled', () => { - mockIsPureBlackEnabled = true; - const theme = createTheme(AppThemeKey.dark); - const result = appendWidgetThemeToBuyUrl( - 'https://on-ramp.example/providers/transak/buy', - theme, - ); - const parsed = new URL(result); - - expect(parsed.searchParams.get('colorMode')).toBe('DARK'); - expect(parsed.searchParams.get('widgetBackgroundFillColor')).toBe( - theme.colors.background.alternative, - ); - expect(parsed.searchParams.get('backgroundColors')).toBe( - [ - theme.colors.background.alternative, - theme.colors.background.alternative, - theme.colors.background.muted, - ].join(','), - ); - }); -}); - -describe('buildQuoteWithWidgetTheme', () => { - beforeEach(() => { - mockIsPureBlackEnabled = false; - }); - - it('rewrites redirect and theme params on the quote buy URL', () => { - mockIsPureBlackEnabled = true; - const theme = createTheme(AppThemeKey.dark); - const quote = makeQuote('APP_BROWSER'); - const result = buildQuoteWithWidgetTheme( - quote, - 'https://callback.example/base', - theme, - ); - const parsed = new URL(result.quote?.buyURL ?? ''); - - expect(parsed.searchParams.get('redirectUrl')).toBe( - 'https://callback.example/base', - ); - expect(parsed.searchParams.get('widgetBackgroundFillColor')).toBe( - theme.colors.background.alternative, - ); - }); -}); diff --git a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts index 830a36df28e..32ffe48814d 100644 --- a/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts +++ b/app/components/UI/Ramp/utils/buildQuoteWithRedirectUrl.ts @@ -1,7 +1,4 @@ import { isExternalBrowserQuote, type Quote } from '@metamask/ramps-controller'; -import type { Theme } from '../../../../util/theme/models'; -import { isPureBlackEnabled } from '../../../../util/theme/themeUtils'; -import { generateThemeParameters } from './depositUtils'; import { getRampCallbackBaseUrl } from './getRampCallbackBaseUrl'; /** @@ -27,54 +24,6 @@ export function buildQuoteWithRedirectUrl( }; } -/** - * Appends Transak widget theme query params to a buy URL when pure black is - * enabled so provider-hosted checkout surfaces match the elevated BottomSheet. - */ -export function appendWidgetThemeToBuyUrl( - buyURL: string, - theme: Theme, -): string { - if (!isPureBlackEnabled) { - return buyURL; - } - - const buyUrl = new URL(buyURL); - const themeParams = generateThemeParameters( - theme.themeAppearance, - theme.colors, - ); - - Object.entries(themeParams).forEach(([key, value]) => { - buyUrl.searchParams.set(key, value); - }); - - return buyUrl.toString(); -} - -/** - * Rewrites a quote's buy URL with redirect + optional pure-black widget theme. - */ -export function buildQuoteWithWidgetTheme( - quote: Quote, - redirectUrl: string, - theme: Theme, -): Quote { - const quoteWithRedirect = buildQuoteWithRedirectUrl(quote, redirectUrl); - const buyURL = quoteWithRedirect.quote?.buyURL; - if (!buyURL) { - return quoteWithRedirect; - } - - return { - ...quoteWithRedirect, - quote: { - ...quoteWithRedirect.quote, - buyURL: appendWidgetThemeToBuyUrl(buyURL, theme), - }, - }; -} - function getProviderDeeplinkRedirectUrl(providerCode: string): string { return `metamask://on-ramp/providers/${providerCode}`; } diff --git a/app/components/UI/Ramp/utils/depositUtils.test.ts b/app/components/UI/Ramp/utils/depositUtils.test.ts index 5c77d9a905e..a7c770d9b7a 100644 --- a/app/components/UI/Ramp/utils/depositUtils.test.ts +++ b/app/components/UI/Ramp/utils/depositUtils.test.ts @@ -16,14 +16,6 @@ import { mockTheme } from '../../../../util/theme'; import { AppThemeKey } from '../../../../util/theme/models'; import { MOCK_ETH_TOKEN } from '../testUtils/constants'; -let mockIsPureBlackEnabled = false; - -jest.mock('../../../../util/theme/themeUtils', () => ({ - get isPureBlackEnabled() { - return mockIsPureBlackEnabled; - }, -})); - jest.mock('../../../../../locales/i18n', () => ({ strings: jest.fn(), })); @@ -227,10 +219,6 @@ describe('generateThemeParameters', () => { themeAppearance: AppThemeKey.dark, }; - beforeEach(() => { - mockIsPureBlackEnabled = false; - }); - it('should generate correct theme parameters for light mode', () => { const themeAppearance = AppThemeKey.light; const colors = mockTheme.colors; @@ -286,66 +274,6 @@ describe('generateThemeParameters', () => { surfaceFillColor: colors.background.muted, }); }); - - it('adds elevated widget background for pure black dark mode', () => { - mockIsPureBlackEnabled = true; - const colors = mockDarkTheme.colors; - const result = generateThemeParameters(AppThemeKey.dark, colors); - - expect(result).toEqual({ - themeColor: colors.primary.default, - colorMode: 'DARK', - backgroundColors: [ - colors.background.alternative, - colors.background.alternative, - colors.background.muted, - ].join(','), - textColors: [ - colors.text.default, - colors.text.default, - colors.text.alternative, - ].join(','), - borderColors: [ - colors.border.default, - colors.border.muted, - colors.border.muted, - ].join(','), - primaryButtonFillColor: colors.icon.default, - primaryButtonTextColor: colors.icon.inverse, - surfaceFillColor: colors.background.muted, - widgetBackgroundFillColor: colors.background.alternative, - }); - }); - - it('keeps default widget background for pure black light mode', () => { - mockIsPureBlackEnabled = true; - const colors = mockTheme.colors; - const result = generateThemeParameters(AppThemeKey.light, colors); - - expect(result).toEqual({ - themeColor: colors.primary.default, - colorMode: 'LIGHT', - backgroundColors: [ - colors.background.default, - colors.background.default, - colors.background.muted, - ].join(','), - textColors: [ - colors.text.default, - colors.text.default, - colors.text.alternative, - ].join(','), - borderColors: [ - colors.border.default, - colors.border.muted, - colors.border.muted, - ].join(','), - primaryButtonFillColor: colors.icon.default, - primaryButtonTextColor: colors.icon.inverse, - surfaceFillColor: colors.background.muted, - widgetBackgroundFillColor: colors.background.default, - }); - }); }); describe('timestampToTransakFormat', () => { diff --git a/app/components/UI/Ramp/utils/depositUtils.ts b/app/components/UI/Ramp/utils/depositUtils.ts index 6b6739d9b38..bcadaaabdb3 100644 --- a/app/components/UI/Ramp/utils/depositUtils.ts +++ b/app/components/UI/Ramp/utils/depositUtils.ts @@ -4,7 +4,6 @@ import { FIAT_ORDER_STATES } from '../../../../constants/on-ramp'; import { renderNumber } from '../../../../util/number'; import { strings } from '../../../../../locales/i18n'; import { AppThemeKey, Colors } from '../../../../util/theme/models'; -import { isPureBlackEnabled } from '../../../../util/theme/themeUtils'; const emailRegex = /^[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~](\.?[-!#$%&'*+/0-9=?A-Z^_a-z`{|}~])*@[a-zA-Z0-9](-*\.?[a-zA-Z0-9])*\.[a-zA-Z](-?[a-zA-Z0-9])+$/; @@ -106,22 +105,11 @@ export const generateThemeParameters = ( themeAppearance: AppThemeKey, colors: Colors, ) => { - const widgetBackgroundFillColor = - isPureBlackEnabled && themeAppearance === AppThemeKey.dark - ? colors.background.alternative - : colors.background.default; - - const backgroundColors = isPureBlackEnabled - ? [ - widgetBackgroundFillColor, - widgetBackgroundFillColor, - colors.background.muted, - ].join(',') - : [ - colors.background.default, - colors.background.default, - colors.background.alternative, - ].join(','); + const backgroundColors = [ + colors.background.default, + colors.background.default, + colors.background.alternative, + ].join(','); const textColors = [ colors.text.default, @@ -135,7 +123,7 @@ export const generateThemeParameters = ( colors.border.muted, ].join(','); - const themeParameters = { + return { themeColor: colors.primary.default, colorMode: themeAppearance === AppThemeKey.light ? 'LIGHT' : 'DARK', backgroundColors, @@ -145,15 +133,6 @@ export const generateThemeParameters = ( primaryButtonTextColor: colors.icon.inverse, surfaceFillColor: colors.background.muted, }; - - if (!isPureBlackEnabled) { - return themeParameters; - } - - return { - ...themeParameters, - widgetBackgroundFillColor, - }; }; /** From ee7acd935a5ac65301c6dd1dfd843be30f40f16a Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Wed, 15 Jul 2026 19:05:29 -0700 Subject: [PATCH 4/6] fix(ramps): remove ScreenLayout wrapper from error state in native Checkout The ScreenLayout component has a hardcoded backgroundColor that was overriding the BottomSheet background, causing a white/grey flash in the error state of the Transak checkout flow. --- .../UI/Ramp/Views/Checkout/Checkout.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx index a44254cac29..367a3285307 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx @@ -671,26 +671,22 @@ const Checkout = () => { keyboardAvoidingViewEnabled={false} > {sharedHeader} - - - { - setKey((prevKey) => prevKey + 1); - setError(''); - isRedirectionHandledRef.current = false; - lastLoadCompleteUrlRef.current = null; - loadUrlErrorsRef.current.clear(); - loadStartTimeRef.current = null; - closeSourceRef.current = null; - urlHistoryRef.current = { current: null, previous: null }; - stepIndexRef.current = 0; - previousNavStateUrlRef.current = null; - }} - location="Provider Webview" - /> - - + { + setKey((prevKey) => prevKey + 1); + setError(''); + isRedirectionHandledRef.current = false; + lastLoadCompleteUrlRef.current = null; + loadUrlErrorsRef.current.clear(); + loadStartTimeRef.current = null; + closeSourceRef.current = null; + urlHistoryRef.current = { current: null, previous: null }; + stepIndexRef.current = 0; + previousNavStateUrlRef.current = null; + }} + location="Provider Webview" + /> ); } From f256f7dbb16a63db701d1d64038a167ae216f11f Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Wed, 15 Jul 2026 19:07:58 -0700 Subject: [PATCH 5/6] fix(ramps): remove hardcoded background from ErrorView The ErrorView component was setting backgroundColor: colors.background.default on its screen wrapper, which conflicted with the BottomSheet container background (especially in pure-black mode where the sheet uses bg-alternative). The container is always responsible for the surface color, so ErrorView should be transparent and inherit from its parent. --- app/components/UI/Ramp/Aggregator/components/ErrorView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/app/components/UI/Ramp/Aggregator/components/ErrorView.tsx b/app/components/UI/Ramp/Aggregator/components/ErrorView.tsx index d4750508dc1..740adee21d2 100644 --- a/app/components/UI/Ramp/Aggregator/components/ErrorView.tsx +++ b/app/components/UI/Ramp/Aggregator/components/ErrorView.tsx @@ -28,7 +28,6 @@ const createStyles = ( flex: options?.asScreen ? 1 : undefined, justifyContent: 'center', alignItems: 'center', - backgroundColor: colors.background.default, }, content: { width: '100%', From 21e559a270145bf4ed985967b35ec4625cf3911c Mon Sep 17 00:00:00 2001 From: georgewrmarshall Date: Wed, 15 Jul 2026 19:13:11 -0700 Subject: [PATCH 6/6] chore(ramps): add comments explaining Transak color overrides --- app/components/UI/Ramp/Views/Checkout/Checkout.tsx | 3 +++ app/styles/common.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx index 367a3285307..5e15e2c132f 100644 --- a/app/components/UI/Ramp/Views/Checkout/Checkout.tsx +++ b/app/components/UI/Ramp/Views/Checkout/Checkout.tsx @@ -103,6 +103,9 @@ const Checkout = () => { const params = useParams(); const { styles } = useStyles(styleSheet, {}); const { themeAppearance } = useTheme(); + // Intentionally overrides the design-system BottomSheet background to match + // Transak's iframe, which we cannot style. Keeps the native chrome seamless + // with the embedded checkout flow. See colors.transak* in app/styles/common.ts. const transakBgClassName = themeAppearance === AppThemeKey.dark ? `bg-[${colors.transakBackgroundDark}]` diff --git a/app/styles/common.ts b/app/styles/common.ts index d4f529db362..7949c79a0a5 100644 --- a/app/styles/common.ts +++ b/app/styles/common.ts @@ -27,6 +27,9 @@ export const colors = { gettingStartedPageBackgroundColor: '#EAC2FF', gettingStartedTextColor: '#3D065F', gettingStartedPageBackgroundColorLightMode: '#FFF2EB', + // Transak's iframe renders these background colors — values are set by Transak + // and outside our control. Used to match the BottomSheet surface so the + // checkout feels seamless. Update only if Transak changes their theme colors. transakBackgroundDark: '#1a1a1a', transakBackgroundLight: '#ffffff', };