/* eslint-disable @typescript-eslint/indent */ /* eslint-disable no-nested-ternary */ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context'; import Toast from '@arcblock/ux/lib/Toast'; // eslint-disable-next-line import/no-extraneous-dependencies import Header from '@blocklet/ui-react/lib/Header'; import type { TCheckoutSessionExpanded, TCustomer, TPaymentCurrency, TPaymentMethod, TPaymentIntent, TPaymentMethodExpanded, } from '@blocklet/payment-types'; import { ArrowBackOutlined } from '@mui/icons-material'; import { Box, Fade, Stack, type BoxProps } from '@mui/material'; import { styled } from '@mui/system'; import { fromTokenToUnit } from '@ocap/util'; import { useSetState } from 'ahooks'; import { useEffect, useMemo, useRef, useState } from 'react'; import { FormProvider, useForm, useWatch } from 'react-hook-form'; import trim from 'lodash/trim'; import type { LiteralUnion } from 'type-fest'; import { usePaymentContext } from '../contexts/payment'; import api from '../libs/api'; import { findCurrency, formatError, getQueryParams, getStatementDescriptor, isMobileSafari, isValidCountry, showStaking, } from '../libs/util'; import type { CheckoutCallbacks, CheckoutContext, CheckoutFormData } from '../types'; import PaymentError from './error'; import CheckoutFooter from './footer'; import PaymentForm, { hasDidWallet } from './form'; // import PaymentHeader from './header'; import OverviewSkeleton from './skeleton/overview'; import PaymentSkeleton from './skeleton/payment'; import PaymentSuccess from './success'; import PaymentSummary from './summary'; import { useMobile } from '../hooks/mobile'; import { formatPhone } from '../libs/phone-validator'; import { getCurrencyPreference } from '../libs/currency'; // eslint-disable-next-line react/no-unused-prop-types type Props = CheckoutContext & CheckoutCallbacks & { completed?: boolean; error?: any; showCheckoutSummary?: boolean }; function PaymentInner({ checkoutSession, paymentMethods, paymentLink, paymentIntent, customer, completed = false, mode, onPaid, onError, onChange, action, showCheckoutSummary = true, quotes, rateUnavailable, rateError, }: MainProps) { const { t } = useLocaleContext(); const { settings, session } = usePaymentContext(); const { isMobile } = useMobile(); const [state, setState] = useSetState<{ checkoutSession: TCheckoutSessionExpanded; quotes?: any; rateUnavailable?: boolean; rateError?: string; paymentIntent?: TPaymentIntent | null; liveRateInfo?: { rate?: string; provider_id?: string; provider_name?: string; base_currency?: string; timestamp_ms?: number; volatility_threshold?: number; }; liveQuoteSnapshot?: { id: string; quoted_amount: string; exchange_rate: string; expires_at: number; rate_timestamp_ms?: number | null; renewed?: boolean; }; liveRateUnavailable?: boolean; liveRateError?: string; isRateLoading?: boolean; }>({ checkoutSession, quotes, rateUnavailable, rateError, paymentIntent, liveRateInfo: undefined, liveQuoteSnapshot: undefined, liveRateUnavailable: false, liveRateError: undefined, isRateLoading: false, }); // Track if currency is being switched (vs just rate refresh) // Only show skeleton during currency switch, not during rate refresh const isCurrencySwitchRef = useRef(false); const prevCurrencyIdRef = useRef(null); const query = getQueryParams(window.location.href); const availableCurrencyIds = useMemo(() => { const currencyIds = new Set(); paymentMethods.forEach((method) => { method.payment_currencies.forEach((currency) => { if (currency.active) { currencyIds.add(currency.id); } }); }); return Array.from(currencyIds); }, [paymentMethods]); const defaultCurrencyId = useMemo(() => { // Keep session currency stable when a promotion is already applied. // Otherwise auto-picking from URL/local preference/no-wallet may switch currency // on refresh and trigger a second recalculate-promotion that removes discount // with `currency_incompatible`. const hasAppliedDiscount = Boolean((state.checkoutSession as any)?.discounts?.length); if ( hasAppliedDiscount && state.checkoutSession.currency_id && availableCurrencyIds.includes(state.checkoutSession.currency_id) ) { return state.checkoutSession.currency_id; } // 1. first check url currencyId if (query.currencyId && availableCurrencyIds.includes(query.currencyId)) { return query.currencyId; } // 2. if user has no wallet, use the first available currency of stripe payment method if (session?.user && !hasDidWallet(session.user)) { const stripeCurrencyId = paymentMethods .find((m) => m.type === 'stripe') ?.payment_currencies.find((c) => c.active)?.id; if (stripeCurrencyId) { return stripeCurrencyId; } } // 3. then check user's saved currency preference const savedPreference = getCurrencyPreference(session?.user?.did, availableCurrencyIds); if (savedPreference) { return savedPreference; } // 4. finally use the currency in checkoutSession or the first available currency if (state.checkoutSession.currency_id && availableCurrencyIds.includes(state.checkoutSession.currency_id)) { return state.checkoutSession.currency_id; } return availableCurrencyIds?.[0]; }, [query.currencyId, availableCurrencyIds, session?.user, state.checkoutSession, paymentMethods]); const defaultMethodId = paymentMethods.find((m) => m.payment_currencies.some((c) => c.id === defaultCurrencyId))?.id; const hideSummaryCard = mode.endsWith('-minimal') || !showCheckoutSummary; const methods = useForm({ defaultValues: { customer_name: customer?.name || session?.user?.fullName || '', customer_email: customer?.email || session?.user?.email || '', customer_phone: formatPhone(customer?.phone || session?.user?.phone || ''), payment_method: defaultMethodId, payment_currency: defaultCurrencyId, billing_address: Object.assign( { country: session?.user?.address?.country || '', state: session?.user?.address?.province || '', city: session?.user?.address?.city || '', line1: session?.user?.address?.line1 || '', line2: session?.user?.address?.line2 || '', postal_code: session?.user?.address?.postalCode || '', }, customer?.address || {}, { country: isValidCountry(customer?.address?.country || session?.user?.address?.country || '') ? customer?.address?.country : 'us', } ), }, }); useEffect(() => { const hasAppliedDiscount = Boolean((state.checkoutSession as any)?.discounts?.length); const currentCurrency = methods.getValues('payment_currency'); const currentMethod = methods.getValues('payment_method'); if (defaultCurrencyId) { // Avoid overriding current currency on refresh when a promotion is already applied. if (!hasAppliedDiscount || !currentCurrency) { methods.setValue('payment_currency', defaultCurrencyId); } } if (defaultMethodId) { if (!hasAppliedDiscount || !currentMethod) { methods.setValue('payment_method', defaultMethodId); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [defaultCurrencyId, defaultMethodId, state.checkoutSession.discounts]); useEffect(() => { if (!isMobileSafari()) { return () => {}; } let scrollTop = 0; const focusinHandler = () => { scrollTop = window.scrollY; }; const focusoutHandler = () => { window.scrollTo(0, scrollTop); }; document.body.addEventListener('focusin', focusinHandler); document.body.addEventListener('focusout', focusoutHandler); return () => { document.body.removeEventListener('focusin', focusinHandler); document.body.removeEventListener('focusout', focusoutHandler); }; }, []); const currencyId = useWatch({ control: methods.control, name: 'payment_currency', defaultValue: defaultCurrencyId }); const currency = (findCurrency(paymentMethods as TPaymentMethodExpanded[], currencyId as string) as TPaymentCurrency) || settings.baseCurrency; const method = paymentMethods.find((x: any) => x.id === currency.payment_method_id) as TPaymentMethod; const hasDynamicPricing = useMemo( () => state.checkoutSession.line_items.some( (item: any) => (item.upsell_price || item.price)?.pricing_type === 'dynamic' ), [state.checkoutSession.line_items] ); // Stripe/USD payments don't need exchange rate - base_amount is already in USD const isStripePayment = method?.type === 'stripe'; const needsExchangeRate = hasDynamicPricing && !isStripePayment; const effectiveRateUnavailable = needsExchangeRate && (state.rateUnavailable || state.liveRateUnavailable); // Don't expose technical errors to UI - only log them for debugging if (state.liveRateError || state.rateError) { console.error('[Rate Error]', { liveRateError: state.liveRateError, rateError: state.rateError }); } // Manual refresh function - exposed for retry button const refreshRateRef = useRef<(() => Promise) | null>(null); useEffect(() => { if (!currencyId) { return; } // Detect currency switch (not initial load) const isCurrencySwitch = prevCurrencyIdRef.current !== null && prevCurrencyIdRef.current !== currencyId; prevCurrencyIdRef.current = currencyId; if (isCurrencySwitch) { isCurrencySwitchRef.current = true; // Show skeleton during currency switch setState({ isRateLoading: true }); } if (needsExchangeRate) { setState({ liveRateInfo: undefined, liveQuoteSnapshot: undefined, liveRateUnavailable: false, liveRateError: undefined, }); liveRateRefreshRef.current = false; refreshRateRef.current?.(); } else { setState({ liveRateInfo: undefined, liveQuoteSnapshot: undefined, liveRateUnavailable: false, liveRateError: undefined, }); liveRateRefreshRef.current = false; // For Stripe payments (no exchange rate needed), clear loading after promotion recalc // If no discounts, clear immediately if (isCurrencySwitch && !(state.checkoutSession as any)?.discounts?.length) { setState({ isRateLoading: false }); isCurrencySwitchRef.current = false; } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currencyId, needsExchangeRate]); useEffect(() => { // Skip exchange rate fetching for Stripe payments (USD base_amount is used directly) if (!state.checkoutSession?.id || completed || !needsExchangeRate) { return undefined; } let cancelled = false; let consecutiveFailures = 0; const baseInterval = 30000; const MAX_INTERVAL = 5 * 60 * 1000; const QUICK_RETRY_DELAY = 1000; // 1s quick retry const MAX_QUICK_RETRIES = 2; // Quick retry 2 times before exponential backoff let currentInterval = baseInterval; let timer: ReturnType | null = null; const scheduleNext = () => { if (timer) { clearInterval(timer); } timer = window.setInterval(() => { fetchRate(false); }, currentInterval) as unknown as ReturnType; }; const fetchRate = async (isManualRetry = false) => { if (document.hidden && !isManualRetry) { return; } if (typeof navigator !== 'undefined' && !navigator.onLine) { return; } if (liveRateRefreshRef.current) { return; } liveRateRefreshRef.current = true; // Quick retry logic: try up to MAX_QUICK_RETRIES times with short delays let quickRetryCount = 0; let lastError: any = null; try { // eslint-disable-next-line no-await-in-loop while (quickRetryCount <= MAX_QUICK_RETRIES) { try { // eslint-disable-next-line no-await-in-loop const { data } = await api.get(`/api/checkout-sessions/${state.checkoutSession.id}/exchange-rate`, { params: currencyId ? { currency_id: currencyId } : undefined, }); if (cancelled) { return; } consecutiveFailures = 0; currentInterval = baseInterval; // Final Freeze: Only store rate info, no quote_snapshot setState({ liveRateInfo: data, liveQuoteSnapshot: undefined, // Quote is created at submit time only liveRateUnavailable: false, liveRateError: undefined, }); // Clear loading state after rate fetch - but only if this was a currency switch // and no discounts need recalculation (recalculatePromotion will clear it otherwise) if (isCurrencySwitchRef.current && !(state.checkoutSession as any)?.discounts?.length) { setState({ isRateLoading: false }); isCurrencySwitchRef.current = false; } scheduleNext(); return; } catch (err: any) { lastError = err; quickRetryCount++; if (quickRetryCount <= MAX_QUICK_RETRIES && !cancelled) { // eslint-disable-next-line no-console console.log( `[Exchange Rate] Quick retry ${quickRetryCount}/${MAX_QUICK_RETRIES} after ${QUICK_RETRY_DELAY}ms` ); // eslint-disable-next-line no-await-in-loop await new Promise((resolve) => { setTimeout(resolve, QUICK_RETRY_DELAY); }); } } } // All quick retries failed if (cancelled) { return; } consecutiveFailures++; const technicalError = lastError?.response?.data?.error || formatError(lastError); console.error('[Exchange Rate Fetch Error]', { error: technicalError, consecutiveFailures, sessionId: state.checkoutSession?.id, currencyId, }); setState({ liveRateUnavailable: true, liveRateError: undefined, }); if (consecutiveFailures >= 3) { console.warn('Exchange rate fetch failed multiple times', { consecutiveFailures, technicalError }); } const nextInterval = Math.min(baseInterval * 2 ** (consecutiveFailures - 1), MAX_INTERVAL); currentInterval = nextInterval; scheduleNext(); } finally { liveRateRefreshRef.current = false; } }; // Expose refresh function for manual retry refreshRateRef.current = async () => { liveRateRefreshRef.current = false; // Allow immediate retry await fetchRate(true); }; fetchRate(false); const handleVisibilityChange = () => { if (!document.hidden) { fetchRate(false); } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => { cancelled = true; refreshRateRef.current = null; if (timer) { clearInterval(timer); } document.removeEventListener('visibilitychange', handleVisibilityChange); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [state.checkoutSession?.id, currencyId, completed, needsExchangeRate]); const recalculatePromotion = async () => { if ((state.checkoutSession as any)?.discounts?.length) { try { await api.post(`/api/checkout-sessions/${state.checkoutSession.id}/recalculate-promotion`, { currency_id: currencyId, }); // Wait for session update before clearing loading state await onPromotionUpdate(); } catch (err) { console.error('[recalculatePromotion] Error:', err); } finally { // Clear loading state after promotion recalculation AND session update complete if (isCurrencySwitchRef.current) { setState({ isRateLoading: false }); isCurrencySwitchRef.current = false; } } } else if (isCurrencySwitchRef.current) { // No discounts to recalculate - clear loading immediately setState({ isRateLoading: false }); isCurrencySwitchRef.current = false; } }; useEffect(() => { if (onChange) { onChange(methods.getValues()); } recalculatePromotion(); }, [currencyId]); // eslint-disable-line const onUpsell = async (from: string, to: string) => { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/upsell`, { from, to }); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onDownsell = async (from: string) => { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/downsell`, { from }); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onApplyCrossSell = async (to: string) => { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`, { to }); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onQuantityChange = async (itemId: string, quantity: number) => { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/adjust-quantity`, { itemId, quantity, currency_id: currencyId, }); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data, ...(data.rateUnavailable !== undefined && { rateUnavailable: data.rateUnavailable }), ...(data.rateError !== undefined && { rateError: data.rateError }), ...(data.quotes !== undefined && { quotes: data.quotes }), }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onCancelCrossSell = async () => { try { const { data } = await api.delete(`/api/checkout-sessions/${state.checkoutSession.id}/cross-sell`); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onChangeAmount = async ({ priceId, amount }: { priceId: string; amount: string }) => { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/amount`, { priceId, amount: fromTokenToUnit(amount, currency.decimal).toString(), }); if (data.discounts?.length) { recalculatePromotion(); return; } setState({ checkoutSession: data }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const onPromotionUpdate = async () => { try { const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`); setState({ checkoutSession: data.checkoutSession }); } catch (err) { console.error(err); Toast.error(formatError(err)); } }; const handlePaid = (result: any) => { setState({ checkoutSession: result.checkoutSession }); onPaid(result); }; const handleQuoteUpdated = (payload: { checkoutSession: TCheckoutSessionExpanded; quotes?: any; rateUnavailable?: boolean; rateError?: string; paymentIntent?: TPaymentIntent | null; }) => { setState({ checkoutSession: payload.checkoutSession, ...(payload.quotes !== undefined && { quotes: payload.quotes }), ...(payload.rateUnavailable !== undefined && { rateUnavailable: payload.rateUnavailable }), ...(payload.rateError !== undefined && { rateError: payload.rateError }), ...(payload.paymentIntent !== undefined && { paymentIntent: payload.paymentIntent }), }); }; const handlePaymentIntentUpdate = (intent: TPaymentIntent | null) => { setState({ paymentIntent: intent }); }; const quoteRefreshRef = useRef(false); const liveRateRefreshRef = useRef(false); const handleQuoteExpired = async (forceRefresh = false) => { if (quoteRefreshRef.current) { return; } quoteRefreshRef.current = true; try { const { data } = await api.get(`/api/checkout-sessions/retrieve/${state.checkoutSession.id}`, { params: forceRefresh ? { forceRefresh: '1' } : undefined, }); handleQuoteUpdated({ checkoutSession: data.checkoutSession, quotes: data.quotes, rateUnavailable: data.rateUnavailable, rateError: data.rateError, paymentIntent: data.paymentIntent, }); } catch (err) { console.error(err); Toast.error(formatError(err)); } finally { quoteRefreshRef.current = false; } }; // trialing can be limited with trial_currency, which can be a list let trialInDays = Number(state.checkoutSession?.subscription_data?.trial_period_days || 0); let trialEnd = Number(state.checkoutSession?.subscription_data?.trial_end || 0); const trialCurrencyIds = (state.checkoutSession?.subscription_data?.trial_currency || '') .split(',') .map(trim) .filter(Boolean); if (trialCurrencyIds.length > 0 && trialCurrencyIds.includes(currencyId) === false) { trialInDays = 0; trialEnd = 0; } const showFeatures = `${paymentLink?.metadata?.show_product_features}` === 'true'; return ( {!hideSummaryCard && ( { try { const { data } = await api.put(`/api/checkout-sessions/${state.checkoutSession.id}/slippage`, { slippage_config: slippageConfig, }); handleQuoteUpdated({ checkoutSession: data.checkoutSession || state.checkoutSession, quotes: data.quotes, rateUnavailable: data.rateUnavailable, rateError: data.rateError, }); } catch (err) { console.error('Failed to update slippage', err); } }} /> {mode === 'standalone' && !isMobile && ( )} )} {completed && ( { return total + (item.price?.product?.vendor_config?.length || 0); }, 0)} sessionId={state.checkoutSession.id} payee={getStatementDescriptor(state.checkoutSession.line_items)} action={state.checkoutSession.mode} invoiceId={state.checkoutSession.invoice_id} subscriptionId={state.checkoutSession.subscription_id} subscriptions={state.checkoutSession.subscriptions} message={ paymentLink?.after_completion?.hosted_confirmation?.custom_message || t( `payment.checkout.completed.${ state.checkoutSession.submit_type === 'donate' ? 'donate' : state.checkoutSession.mode }` ) } /> )} {!completed && ( )} {mode === 'standalone' && isMobile && } ); } export default function Payment({ checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, completed = false, error = null, mode, onPaid, onError, onChange, goBack, action, showCheckoutSummary = true, quotes, rateUnavailable, rateError, }: Props) { const { t } = useLocaleContext(); const { refresh, livemode, setLivemode } = usePaymentContext(); const [delay, setDelay] = useState(false); const { isMobile } = useMobile(); const hideSummaryCard = mode.endsWith('-minimal') || !showCheckoutSummary; const isMobileSafariEnv = isMobileSafari(); useEffect(() => { setTimeout(() => { // 骨架屏 delay setDelay(true); }, 500); }, []); useEffect(() => { if (checkoutSession) { if (livemode !== checkoutSession.livemode) { setLivemode(checkoutSession.livemode); } } }, [checkoutSession, livemode, setLivemode, refresh]); const renderContent = () => { if (error) { return ; } if (!checkoutSession || !delay) { return ( {!hideSummaryCard && ( )} ); } if (checkoutSession.expires_at <= Math.round(Date.now() / 1000)) { return ( ); } if (!checkoutSession.line_items.length) { return ( ); } return ( ); }; return ( {mode === 'standalone' ? (
) : null} {goBack && ( )} {renderContent()} ); } type MainProps = CheckoutContext & CheckoutCallbacks & { completed?: boolean; showCheckoutSummary?: boolean }; type RootProps = { mode: LiteralUnion<'standalone' | 'inline' | 'popup', string> } & BoxProps; export const Root: React.FC = styled(Box)` box-sizing: border-box; display: flex; flex-direction: column; align-items: center; overflow: hidden; position: relative; .cko-container { overflow: hidden; width: 100%; display: flex; flex-direction: row; justify-content: center; position: relative; flex: 1; padding: 1px; } .base-card { border: none; border-radius: 0; padding: ${(props) => (props.mode === 'standalone' ? '100px 40px 20px' : '20px 0')}; box-shadow: none; flex: 1; max-width: 582px; } .cko-overview { position: relative; flex-direction: column; display: ${(props) => (props.mode.endsWith('-minimal') ? 'none' : 'flex')}; background: ${({ theme }) => theme.palette.background.default}; min-height: 'auto'; } .cko-header { left: 0; margin-bottom: 0; position: absolute; right: 0; top: 0; transition: background-color 0.15s ease, box-shadow 0.15s ease-out; } .cko-product { flex: 1; overflow: hidden; } .cko-product-summary { width: 100%; } .cko-ellipsis { width: 100%; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .cko-payment { width: 502px; padding-left: ${(props) => (props.mode === 'standalone' ? '40px' : '20px')}; position: relative; &:before { -webkit-animation-fill-mode: both; content: ''; height: 100%; position: absolute; left: 0px; top: 0px; transform-origin: left center; width: 8px; box-shadow: -4px 0px 8px 0px rgba(2, 7, 19, 0.04); } } .cko-payment-contact { overflow: hidden; } .cko-footer { display: ${(props) => (props.mode.endsWith('-minimal') ? 'none' : 'block')}; text-align: center; margin-top: 20px; } .cko-payment-form { .MuiFormLabel-root { color: ${({ theme }) => theme.palette.grey.A700}; font-weight: 500; margin-top: 12px; margin-bottom: 4px; } .MuiBox-root { margin: 0; } .MuiFormHelperText-root { margin-left: 14px; } } .cko-payment-methods { } .cko-payment-submit { .MuiButtonBase-root { font-size: 1.3rem; position: relative; } .cko-submit-progress { position: absolute; top: 0; width: 100%; height: 100%; opacity: 0.3; } } .cko-header { } .product-item { border-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; border: 1px solid; border-color: ${({ theme }) => theme.palette.divider}; .product-item-content { padding: 16px; background: ${({ theme }) => theme.palette.grey[50]}; border-top-left-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; border-top-right-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; } .product-item-upsell { margin-top: 0; padding: 16px; background: ${({ theme }) => theme.palette.grey[100]}; border-bottom-left-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; border-bottom-right-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; } .product-item-content:only-child { border-radius: ${({ theme }) => `${2 * (theme.shape.borderRadius as number)}px`}; } } @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) { padding-top: 0; overflow: auto; &:before { display: none; } .cko-container { flex-direction: column; align-items: center; justify-content: flex-start; gap: 0; overflow: visible; min-width: 200px; } .cko-overview { width: 100%; min-height: auto; flex: none; } .cko-payment { width: 100%; height: fit-content; flex: none; border-top: 1px solid; border-color: ${({ theme }) => theme.palette.divider}; &:before { display: none; } } .cko-footer { position: relative; margin-bottom: 4px; margin-top: 0; bottom: 0; left: 0; transform: translateX(0); } .base-card { box-shadow: none; border-radius: 0; padding: 20px; } } `;