-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
fix(ramps): dynamic per-provider BottomSheet background for checkout WebView (TMCU-1087) #33429
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3e0d153
9ad1c71
636d24d
81b255e
5670675
cfaeced
18d6da0
a605387
acff4c1
e8df53f
3925bab
5b7917e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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) { | ||
|
|
@@ -252,6 +263,7 @@ const CheckoutWebView = () => { | |
| isFullscreen | ||
| isInteractable={!Device.isAndroid()} | ||
| keyboardAvoidingViewEnabled={false} | ||
| style={providerBgStyle} | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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={ | ||
|
|
||
| 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 } }) => | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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, | ||
| }, | ||
| }); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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(); | ||
|
|
@@ -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, | ||
|
|
@@ -695,6 +708,7 @@ const Checkout = () => { | |
| isFullscreen | ||
| isInteractable={!Device.isAndroid()} | ||
| keyboardAvoidingViewEnabled={false} | ||
| twClassName={providerBgTwClassName} | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
| 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, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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', | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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< | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
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
loading.issues.mov