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
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../../../util/theme/models';

const styleSheet = (params: { theme: Theme }) =>
const styleSheet = (params: { theme: Theme; vars: { providerBg: string } }) =>
StyleSheet.create({
headerWithoutPadding: {
paddingVertical: 0,
},
webview: {
backgroundColor: params.theme.colors.background.default,
// Matches the provider iframe background to suppress the native white
// flash before content loads. Unknown providers receive the BottomSheet
// default surface color from getProviderWebviewColors.
backgroundColor: params.vars.providerBg,

@georgewrmarshall georgewrmarshall Jul 16, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applying background color to the web view, without this webview defaults to white and you get a white flash.

Before

Image

After

There is still some color issues but that's because of the provider webviews themselves so we get this small bar of grey before the webview loads

Image
loading.issues.mov

},
});

Expand Down
16 changes: 14 additions & 2 deletions app/components/UI/Ramp/Aggregator/Views/Checkout/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ import {
} from '../../../../../../component-library/components/Icons/Icon';
import { useStyles } from '../../../../../../component-library/hooks';
import styleSheet from './Checkout.styles';
import { useTheme } from '../../../../../../util/theme';
import { AppThemeKey } from '../../../../../../util/theme/models';
import { getProviderWebviewColors } from '../../../utils/getProviderWebviewColors';
import Device from '../../../../../../util/device';
import { shouldStartLoadWithRequest } from '../../../../../../util/browser';
import { CHECKOUT_TEST_IDS } from './Checkout.testIds';
Expand Down Expand Up @@ -66,10 +69,18 @@ const CheckoutWebView = () => {
const params = useParams<CheckoutParams>();
const handleSuccessfulOrder = useHandleSuccessfulOrder();

const { styles } = useStyles(styleSheet, {});

const { url: uri, customOrderId, provider } = params;

// Resolve the provider's iframe background color for the current theme.
// Applied to both the component-library BottomSheet (via providerBgStyle) and
// the WebView (via styles.webview) so the native chrome matches the embedded
// checkout seamlessly. Unknown providers fall back to the BottomSheet default surface.
const { themeAppearance } = useTheme();
const isDark = themeAppearance === AppThemeKey.dark;
const providerBg = getProviderWebviewColors(provider?.id, isDark);
const { styles } = useStyles(styleSheet, { providerBg });
const providerBgStyle = { backgroundColor: providerBg };

const handleCancelPress = useCallback(() => {
const chainId = selectedAsset?.network?.chainId || '';
if (isBuy) {
Expand Down Expand Up @@ -252,6 +263,7 @@ const CheckoutWebView = () => {
isFullscreen
isInteractable={!Device.isAndroid()}
keyboardAvoidingViewEnabled={false}
style={providerBgStyle}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The component-library BottomSheet accepts a style prop that is spread last onto the dialog surface container, so it correctly overrides the default background. This is the legacy aggregator flow — the MMDS BottomSheet used in the native flow uses twClassName with Tailwind arbitrary values instead (style goes to the KeyboardAvoidingView wrapper there, not the surface).

>
<BottomSheetHeader
endAccessory={
Expand Down
7 changes: 5 additions & 2 deletions app/components/UI/Ramp/Views/Checkout/Checkout.styles.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { StyleSheet } from 'react-native';
import { Theme } from '../../../../../util/theme/models';

const styleSheet = (params: { theme: Theme }) =>
const styleSheet = (params: { theme: Theme; vars: { providerBg: string } }) =>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The useStyles hook wraps any extra params in a vars object at runtime, so accessing params.providerBg directly would always be undefined. The type annotation here makes the correct access path (params.vars.providerBg) explicit and enforced by TypeScript. This was the root cause of the WebView backgroundColor never being applied before this fix.

StyleSheet.create({
headerWithoutPadding: {
paddingVertical: 0,
},
webview: {
backgroundColor: params.theme.colors.background.default,
// Matches the provider iframe background to suppress the native white
// flash before content loads. Unknown providers receive the BottomSheet
// default surface color from getProviderWebviewColors.
backgroundColor: params.vars.providerBg,
},
});

Expand Down
16 changes: 15 additions & 1 deletion app/components/UI/Ramp/Views/Checkout/Checkout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { getProviderWebviewColors } from '../../utils/getProviderWebviewColors';
import Device from '../../../../../util/device';
import { shouldStartLoadWithRequest } from '../../../../../util/browser';
import { CHECKOUT_TEST_IDS } from './Checkout.testIds';
Expand Down Expand Up @@ -98,7 +101,7 @@ const Checkout = () => {
const [key, setKey] = useState(0);
const navigation = useNavigation();
const params = useParams<CheckoutParams>();
const { styles } = useStyles(styleSheet, {});
const { themeAppearance } = useTheme();
const { addOrder, addPrecreatedOrder, getOrderFromCallback } =
useRampsOrders();
const { trackEvent, createEventBuilder } = useAnalytics();
Expand All @@ -116,6 +119,16 @@ const Checkout = () => {
cryptocurrency,
headlessSessionId,
} = params ?? {};

// Resolve the provider's iframe background color for the current theme.
// Applied to both the MMDS BottomSheet (via twClassName) and the WebView
// (via styles.webview) so the native chrome matches the embedded checkout
// seamlessly. Unknown providers fall back to the BottomSheet default surface.
const isDark = themeAppearance === AppThemeKey.dark;
const providerBg = getProviderWebviewColors(providerCode, isDark);
const { styles } = useStyles(styleSheet, { providerBg });
const providerBgTwClassName = `bg-[${providerBg}]`;

const effectiveOrderId = (orderIdParam ?? customOrderId)?.trim() || null;

// Headless deposit (TRAM-3623): when a headless session drives this Checkout,
Expand Down Expand Up @@ -695,6 +708,7 @@ const Checkout = () => {
isFullscreen
isInteractable={!Device.isAndroid()}
keyboardAvoidingViewEnabled={false}
twClassName={providerBgTwClassName}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The MMDS BottomSheet's style prop applies to its KeyboardAvoidingView wrapper, not the dialog surface itself. The twClassName prop with a Tailwind arbitrary value (bg-[#hex]) is the correct way to override the surface background in MMDS. The computed class is built from the resolved providerBg string so it adapts dynamically to both the provider and current theme.

>
{sharedHeader}
<WebView
Expand Down
53 changes: 53 additions & 0 deletions app/components/UI/Ramp/utils/getProviderWebviewColors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { brandColor, darkTheme, lightTheme } from '@metamask/design-tokens';
import { colors } from '../../../../styles/common';

/**
* Background colors used by each provider's iframe/webview in dark and light mode.
* These are set by the provider and outside our control — we match them in the
* BottomSheet surface so the native chrome feels seamless with the embedded checkout.
*
* Add or update entries here when a provider's iframe background changes.
* Keys are lowercase substrings of the provider ID returned by the ramps controller
* (e.g. "transak" matches both "transak" and "transak-native").
*
* The "default" entry mirrors the MMDS BottomSheet surface colors and is used
* when no provider-specific color is known.
*/

interface ProviderWebviewColors {
dark: string;
light: string;
}

// Intentionally overrides the design-system BottomSheet background to match
// each provider's iframe, which we cannot style. Keeps the native chrome
// seamless with the embedded checkout flow. See colors.*CheckoutDark in
// app/styles/common.ts. Update only if a provider changes their iframe theme.
const PROVIDER_WEBVIEW_COLORS: Record<string, ProviderWebviewColors> = {
// Defaults to MMDS BottomSheet surface colors when no provider-specific
// color is known, ensuring the WebView background always matches the sheet.
default: {
dark: darkTheme.colors.background.alternative,
light: lightTheme.colors.background.default,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'default' entry lives in the map rather than as a separate constant so it naturally participates in the same lookup path. Any provider ID not matched by the substring search resolves to this entry, giving the WebView a background that matches the MMDS BottomSheet's own default surface without any special-case logic in the caller.

},
transak: { dark: colors.transakCheckoutDark, light: brandColor.white },
moonpay: { dark: colors.moonpayCheckoutDark, light: brandColor.white },
banxa: { dark: colors.banxaCheckoutDark, light: brandColor.white },
};

/**
* Returns the provider's iframe background color for the current theme.
* Falls back to the MMDS BottomSheet default surface for unknown providers.
*/
export function getProviderWebviewColors(
providerCode: string | undefined,
isDark: boolean,
): string {
const key =
providerCode &&
Object.keys(PROVIDER_WEBVIEW_COLORS).find(
(k) => k !== 'default' && providerCode.toLowerCase().includes(k),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substring matching (e.g. 'transak' matches 'transak-native') is intentional — ramp provider IDs often have suffixes like '-native' or '-v2' that vary across SDK versions. The k !== 'default' guard keeps the sentinel entry from accidentally matching provider IDs that happen to contain the word 'default'.

);
const entry = PROVIDER_WEBVIEW_COLORS[key || 'default'];
return isDark ? entry.dark : entry.light;
}
7 changes: 7 additions & 0 deletions app/styles/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export const colors = {
gettingStartedPageBackgroundColor: '#EAC2FF',
gettingStartedTextColor: '#3D065F',
gettingStartedPageBackgroundColorLightMode: '#FFF2EB',
// Provider iframe backgrounds — these colors are set by each provider and
// outside our control. We match them in the checkout BottomSheet so the
// native chrome feels seamless with the embedded webview. Update only if
// a provider changes their iframe theme colors.
transakCheckoutDark: '#1a1a1a',

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only dark-mode hex values are stored here because light-mode for all current providers is plain white (brandColor.white). If a provider ever needs a non-white light-mode background, this pattern would need to extend to a light constant as well. The dark values can't use design tokens because they are provider-specific colors outside the design system.

moonpayCheckoutDark: '#131416',
banxaCheckoutDark: '#0D0D0F',
};

export const onboardingCarouselColors: Record<
Expand Down
Loading