Files
com.blitzwallet/app/components/admin/homeComponents/sendBitcoin/stablecoinSendScreen.js
T

925 lines
28 KiB
JavaScript

import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigation, useRoute } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import {
CustomKeyboardAvoidingView,
ThemeText,
} from '../../../../functions/CustomElements';
import CustomButton from '../../../../functions/CustomElements/button';
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
import FormattedBalanceInput from '../../../../functions/CustomElements/formattedBalanceInput';
import CustomNumberKeyboard from '../../../../functions/CustomElements/customNumberKeyboard';
import CustomSearchInput from '../../../../functions/CustomElements/searchInput';
import ChoosePaymentMethod from './components/choosePaymentMethodContainer';
import SwipeButtonNew from '../../../../functions/CustomElements/sliderButton';
import GetThemeColors from '../../../../hooks/themeColors';
import {
APPROXIMATE_SYMBOL,
CENTER,
COLORS,
CONTENT_KEYBOARD_OFFSET,
SIZES,
USDB_TOKEN_ID,
} from '../../../../constants';
import {
HIDDEN_OPACITY,
INSET_WINDOW_WIDTH,
WINDOWWIDTH,
} from '../../../../constants/theme';
import { useGlobalThemeContext } from '../../../../../context-store/theme';
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
import { useNodeContext } from '../../../../../context-store/nodeContext';
import { useKeysContext } from '../../../../../context-store/keys';
import { useActiveCustodyAccount } from '../../../../../context-store/activeAccount';
import {
isSendingPayingEventEmiiter,
SENDING_PAYMENT_EVENT_NAME,
useSparkWallet,
} from '../../../../../context-store/sparkContext';
import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext';
import { useFlashnet } from '../../../../../context-store/flashnetContext';
import { useGlobalContextProvider } from '../../../../../context-store/context';
import fetchBackend from '../../../../../db/handleBackend';
import { sendSparkPayment, sendSparkTokens } from '../../../../functions/spark';
import { bulkUpdateSparkTransactions } from '../../../../functions/spark/transactions';
import {
calculateFlashnetAmountIn,
dollarsToSats,
} from '../../../../functions/spark/flashnet';
import EmojiQuickBar from '../../../../functions/CustomElements/emojiBar';
import useCurrencyDisplay from '../../../../hooks/useCurrencyDisplay';
import useDisplayCurrencyController from '../../../../hooks/useDisplayCurrencyController';
import SendTransactionFeeInfo from './components/feeInfo';
import { formatStablecoinAmount } from '../../../../functions/sendBitcoin';
import { SliderProgressAnimation } from '../../../../functions/CustomElements/sendPaymentAnimation';
import { formatBalanceAmount } from '../../../../functions';
import { useBudgetWarning } from '../../../../hooks/useBudgetWarning';
import {
getDefaultDisplayCurrency,
resolveUsdFiatStats,
} from '../../../../functions/displayCurrency';
import CurrencySwitchButton from '../../../../functions/CustomElements/currencySwitchButton';
const QUOTE_TTL_MS = 115_000;
function truncateAddress(addr) {
if (!addr || addr.length <= 16) return addr || '';
return `${addr.slice(0, 8)}...${addr.slice(-6)}`;
}
function formatCountdown(ms) {
const secs = Math.max(0, Math.floor(ms / 1000));
const m = Math.floor(secs / 60);
const s = secs % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
}
function capitalizeChain(chain) {
if (typeof chain !== 'string') return '';
const lowered = chain.toLowerCase();
const firstLetter = lowered[0].toUpperCase();
return firstLetter + lowered.slice(1);
}
export default function StablecoinSendScreen() {
const navigate = useNavigation();
const route = useRoute();
const {
address,
chain,
chainLabel,
asset,
selectedPaymentMethod,
prefillAmount,
} = route.params;
const { t } = useTranslation();
const { theme, darkModeType } = useGlobalThemeContext();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const { bottomPadding } = useGlobalInsets();
const didWarnAboutBudget = useRef(null);
const { fiatStats } = useNodeContext();
const { masterInfoObject } = useGlobalContextProvider();
const { contactsPrivateKey, publicKey } = useKeysContext();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { sparkInformation } = useSparkWallet();
const { bitcoinBalance, dollarBalanceToken, dollarBalanceSat } =
useUserBalanceContext();
const { swapUSDPriceDollars, poolInfoRef } = useFlashnet();
const [screenMode, setScreenMode] = useState('EDIT_AMOUNT'); // 'EDIT_AMOUNT' | 'CONFIRM_PAYMENT'
const [rawInput, setRawInput] = useState(
prefillAmount != null ? String(prefillAmount) : '',
);
console.log(rawInput);
const [description, setDescription] = useState('');
const [quote, setQuote] = useState(null);
const [quoteLoading, setQuoteLoading] = useState(false);
const [quoteError, setQuoteError] = useState(null);
const [countdown, setCountdown] = useState(null);
const [sending, setSending] = useState(false);
const [isAmountFocused, setIsAmountFocused] = useState(true);
const [retriggerQuoteFetch, setRetriggerQuoteFetch] = useState(0);
const debounceRef = useRef(null);
const countdownRef = useRef(null);
const quoteExpiresAt = useRef(null);
const isSendingPayment = useRef(null);
const progressAnimationRef = useRef(null);
const fetchTokenRef = useRef(0);
const balanceRef = useRef({
bitcoin: bitcoinBalance,
dollarToken: dollarBalanceToken,
});
balanceRef.current.bitcoin = bitcoinBalance;
balanceRef.current.dollarToken = dollarBalanceToken;
const sourceMethod = selectedPaymentMethod || 'BTC';
const usdFiatStats = useMemo(
() => resolveUsdFiatStats(fiatStats, swapUSDPriceDollars),
[fiatStats, swapUSDPriceDollars],
);
const initialDisplayCurrency = useMemo(
() =>
sourceMethod === 'USD'
? 'USD'
: getDefaultDisplayCurrency({
paymentMode: sourceMethod,
masterInfoObject,
fiatStats,
}),
[sourceMethod, masterInfoObject, fiatStats],
);
const { displayCurrency, currencyRates, isLoadingRate, selectCurrency } =
useDisplayCurrencyController({
initialCurrency: initialDisplayCurrency,
fiatStats,
usdFiatStats,
masterInfoObject,
});
useEffect(() => {
isSendingPayment.current = sending;
}, [sending]);
const {
primaryDisplay,
conversionFiatStats,
convertSatsToDisplay,
convertDisplayToSats,
} = useCurrencyDisplay({
displayCurrency: sourceMethod === 'USD' ? 'USD' : displayCurrency,
fiatStats,
usdFiatStats,
currencyRates,
masterInfoObject,
isSendingPayment: isSendingPayment.current,
});
// Kept current so the success-page handoff reads the live display config
// rather than a stale value captured in the handleSend useCallback closure.
const primaryDisplayRef = useRef(primaryDisplay);
useEffect(() => {
primaryDisplayRef.current = primaryDisplay;
}, [primaryDisplay]);
const convertedSendAmount = convertDisplayToSats(rawInput);
const { shouldWarn } = useBudgetWarning(convertedSendAmount);
const clearCountdown = useCallback(() => {
if (countdownRef.current) clearInterval(countdownRef.current);
countdownRef.current = null;
setCountdown(null);
}, []);
const startCountdown = useCallback(
expiresAt => {
clearCountdown();
quoteExpiresAt.current = expiresAt;
countdownRef.current = setInterval(() => {
const remaining = quoteExpiresAt.current - Date.now();
if (remaining <= 0) {
clearInterval(countdownRef.current);
countdownRef.current = null;
setCountdown(null);
setQuote(null);
setRetriggerQuoteFetch(prev => prev + 1);
} else {
setCountdown(remaining);
}
}, 1000);
},
[clearCountdown, t],
);
const fetchQuote = useCallback(
async (sats, myToken) => {
if (sats <= 0 || !contactsPrivateKey || !publicKey) return;
setQuoteError(null);
setQuote(null);
clearCountdown();
try {
const apiSourceMethod = sourceMethod === 'BTC' ? 'spark' : 'usdb';
// Adding flag for backend to not manipulate the amount so we don't cause balance issues + failures
const maxBalance =
sourceMethod === 'BTC'
? balanceRef.current.bitcoin
: balanceRef.current.dollarToken * Math.pow(10, 6);
const isUsingMax = maxBalance > 0 && sats >= maxBalance * 0.98;
// ── stale-response guard ──────────────────────────────────────────────
if (myToken !== fetchTokenRef.current) return;
const result = await fetchBackend(
'createFlashnetStablecoinQuoteV2',
{
recipientAddress: address,
destinationChain: chain,
destinationAsset: asset,
amountSats: sats,
sourceMethod: apiSourceMethod,
refundAddress: sparkInformation.sparkAddress,
isUsingMax,
},
contactsPrivateKey,
publicKey,
);
// ── stale-response guard ──────────────────────────────────────────────
if (myToken !== fetchTokenRef.current) return;
if (!result || result.error) {
const { code, message, minimumSats } = result.error;
if (code === 'amount_too_small') {
if (minimumSats) {
throw new Error(
t('flashnetUSDCUSDTMessages.amount_too_small_minimum', {
minimumSats,
}),
);
}
throw new Error(
t('flashnetUSDCUSDTMessages.amount_too_small_chain_fees'),
);
}
throw new Error(t(`flashnetUSDCUSDTMessages.${code}`));
}
const expiresAt = result.expiresAt || Date.now() + QUOTE_TTL_MS;
const formattedFee =
sourceMethod === 'BTC'
? result.fee
: dollarsToSats(
result?.fee / Math.pow(10, 6),
poolInfoRef.currentPriceAInB,
);
setQuote({ ...result, fee: formattedFee, expiresAt });
startCountdown(expiresAt);
} catch (err) {
// Only surface the error if this is still the active request
if (myToken !== fetchTokenRef.current) return;
setQuoteError(err.message || t('wallet.stablecoinSend.quoteError'));
} finally {
// Only clear loading if this is still the active request
if (myToken === fetchTokenRef.current) {
setQuoteLoading(false);
}
}
},
[
address,
chain,
asset,
sourceMethod,
contactsPrivateKey,
publicKey,
clearCountdown,
startCountdown,
t,
],
);
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (rawInput > 0) {
const exceedsBalance =
sourceMethod === 'BTC'
? convertedSendAmount > balanceRef.current.bitcoin
: Number(rawInput) > Number(balanceRef.current.dollarToken);
// Stop quote fetch if we do not have an available balance
if (exceedsBalance) {
fetchTokenRef.current += 1;
setQuoteLoading(false);
setQuote(null);
setQuoteError(null);
clearCountdown();
return;
}
setQuoteLoading(true);
// Invalidate any in-flight response right now, before the debounce fires.
fetchTokenRef.current += 1;
// Find max balance without going over actual user balance
const amountIn = calculateFlashnetAmountIn({
baseAmountIn:
sourceMethod === 'BTC'
? convertDisplayToSats(rawInput)
: rawInput * Math.pow(10, 6),
isUsdAssetIn: sourceMethod === 'USD',
dollarBalanceSat,
maxBalance: bitcoinBalance,
currentPriceAInB: poolInfoRef.currentPriceAInB,
bufferMultiplier: 1,
});
debounceRef.current = setTimeout(
() => fetchQuote(amountIn, fetchTokenRef.current),
800,
);
} else {
fetchTokenRef.current += 1;
setQuoteLoading(false);
setQuote(null);
setQuoteError(null);
clearCountdown();
}
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [rawInput, sourceMethod, fetchQuote, clearCountdown, retriggerQuoteFetch]);
useEffect(() => {
return () => {
clearCountdown();
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [clearCountdown]);
// Handle back press from CONFIRM_PAYMENT — return to EDIT_AMOUNT
useEffect(() => {
const unsubscribe = navigate.addListener('beforeRemove', e => {
if (screenMode !== 'CONFIRM_PAYMENT') return;
if (isSendingPayment.current) return;
e.preventDefault();
setScreenMode('EDIT_AMOUNT');
});
return unsubscribe;
}, [navigate, screenMode]);
const openPicker = () => {
if (!isAmountFocused) return;
if (isConfirmMode || sourceMethod === 'USD') return;
navigate.navigate('CustomHalfModal', {
wantedContent: 'displayCurrencySelect',
sliderHight: 0.6,
currentCurrency: displayCurrency,
onSelectCurrency: async code => {
const response = await selectCurrency(code);
if (response?.didWork) setRawInput('');
return response;
},
});
};
const handleMethodToggle = () => {
navigate.navigate('CustomHalfModal', {
wantedContent: 'SelectPaymentMethod',
selectedPaymentMethod: sourceMethod,
fromPage: 'StablecoinSendScreen',
});
};
const handleSend = useCallback(async () => {
if (!quote || sending) return;
if (Date.now() >= quote.expiresAt) {
setQuoteError(t('wallet.stablecoinSend.quoteExpired'));
setQuote(null);
clearCountdown();
setScreenMode('EDIT_AMOUNT');
return;
}
setSending(true);
isSendingPayingEventEmiiter.emit(SENDING_PAYMENT_EVENT_NAME, true);
clearCountdown();
try {
let result;
if (sourceMethod === 'BTC') {
result = await sendSparkPayment({
receiverSparkAddress: quote.depositAddress,
amountSats: Number(quote.amountIn),
mnemonic: currentWalletMnemoinc,
});
} else {
// USDb: quote.amountIn is in token micro-units (e.g. 1_000_000 = $1)
result = await sendSparkTokens({
tokenIdentifier: USDB_TOKEN_ID,
tokenAmount: Number(quote.amountIn),
receiverSparkAddress: quote.depositAddress,
mnemonic: currentWalletMnemoinc,
});
}
if (!result.didWork) throw new Error(result.error);
const sparkTransferId =
sourceMethod === 'USD' ? result.response : result.response?.id;
const pendingTx = {
id: sparkTransferId,
paymentStatus: 'pending',
paymentType: 'spark',
accountId: sparkInformation.identityPubKey,
details: {
amount: quote.amountIn,
fee: quote?.fee,
totalFee: 0,
supportFee: 0,
description: description || '',
address: quote.depositAddress,
sourceSparkAddress: sparkInformation.sparkAddress,
time: Date.now(),
createdAt: Date.now(),
direction: 'OUTGOING',
isFlashnetStablecoin: true,
quoteId: quote.quoteId,
destinationAddress: address,
destinationChain: chain,
destinationAsset: asset,
sourceMethod,
isLRC20Payment: sourceMethod === 'USD',
...(sourceMethod === 'USD' ? { LRC20Token: USDB_TOKEN_ID } : {}),
},
};
await bulkUpdateSparkTransactions([pendingTx], 'fullUpdate');
fetchBackend(
'submitFlashnetStablecoinOrder',
{
quoteId: quote.quoteId,
sparkTxHash: sparkTransferId,
sourceSparkAddress: sparkInformation.sparkAddress,
},
contactsPrivateKey,
publicKey,
);
if (progressAnimationRef.current) {
progressAnimationRef.current.completeProgress();
await new Promise(res => setTimeout(res, 600));
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
navigate.reset({
index: 0,
routes: [
{
name: 'HomeAdmin',
params: {
screen: 'Home',
},
},
{
name: 'ConfirmTxPage',
params: {
transaction: pendingTx,
paymentDisplay: primaryDisplayRef.current,
displayAmount: rawInput,
},
},
],
});
});
});
} catch (err) {
console.log(err, 'error navigating to bla bla bla');
requestAnimationFrame(() => {
requestAnimationFrame(() => {
navigate.reset({
index: 0,
routes: [
{
name: 'HomeAdmin',
params: {
screen: 'Home',
},
},
{
name: 'ConfirmTxPage',
params: {
transaction: {},
error: err.message,
lnurlAddress: undefined,
blitzContactInfo: undefined,
},
},
],
});
});
});
} finally {
isSendingPayingEventEmiiter.emit(SENDING_PAYMENT_EVENT_NAME, false);
}
}, [
quote,
sending,
sourceMethod,
navigate,
currentWalletMnemoinc,
sparkInformation,
description,
address,
chain,
asset,
contactsPrivateKey,
publicKey,
clearCountdown,
t,
rawInput,
]);
const handleEmoji = newDescription => {
setDescription(newDescription);
};
const isQuoteLoading =
(quoteLoading || (quote && countdown === null)) && !sending;
const hasEnoughBalance =
sourceMethod === 'BTC'
? convertedSendAmount <= Number(bitcoinBalance)
: Number(rawInput) <= Number(dollarBalanceToken);
const canConfirm =
!!quote &&
!isQuoteLoading &&
!sending &&
convertedSendAmount > 0 &&
hasEnoughBalance;
const canReview =
(convertedSendAmount > 0 && hasEnoughBalance && !sending && quote) ||
quoteLoading;
const handleReview = useCallback(() => {
if (!convertedSendAmount || convertedSendAmount <= 0) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.noAmount'),
});
return;
}
if (!hasEnoughBalance) {
navigate.navigate('ErrorScreen', {
errorMessage: t('screens.inAccount.swapsPage.insufficientBalance'),
});
return;
}
if (isQuoteLoading) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.quoteStillLoading'),
});
return;
}
if (quoteError) {
navigate.navigate('ErrorScreen', { errorMessage: quoteError });
return;
}
if (!quote) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.noQuote'),
});
return;
}
if (Date.now() >= quote.expiresAt) {
setQuoteError(t('wallet.stablecoinSend.quoteExpired'));
setQuote(null);
clearCountdown();
return;
}
setIsAmountFocused(true);
setScreenMode('CONFIRM_PAYMENT');
}, [
canConfirm,
quote,
quoteError,
clearCountdown,
t,
convertedSendAmount,
isQuoteLoading,
sourceMethod,
rawInput,
bitcoinBalance,
dollarBalanceToken,
navigate,
hasEnoughBalance,
]);
const rowBg = backgroundOffset;
const memorizedKeyboardStyle = useMemo(() => {
return {
paddingBottom: !isAmountFocused ? 0 : bottomPadding,
};
}, [isAmountFocused]);
const isConfirmMode = screenMode === 'CONFIRM_PAYMENT';
const receiveAmountContent = `${APPROXIMATE_SYMBOL}${formatBalanceAmount(
formatStablecoinAmount(
quote?.estimatedOut || 0,
2,
chain === 'bsc' ? 18 : 6,
),
false,
masterInfoObject,
)} ${asset}`;
useEffect(() => {
if (
isConfirmMode &&
shouldWarn &&
!didWarnAboutBudget.current &&
!isSendingPayment.current
) {
didWarnAboutBudget.current = true;
navigate.navigate('CustomHalfModal', {
wantedContent: 'nearBudgetLimitWarning',
sliderHight: 0.6,
sendingAmount: convertedSendAmount,
});
}
}, [isConfirmMode, shouldWarn, convertedSendAmount]);
return (
<CustomKeyboardAvoidingView globalThemeViewStyles={memorizedKeyboardStyle}>
<View style={styles.replacementContainer}>
<CustomSettingsTopBar
label={`${t('constants.send')}`}
containerStyles={{ marginBottom: 0 }}
rightContent={
isConfirmMode ? (
countdown != null ? (
<ThemeText
styles={[
styles.countdownText,
{ backgroundColor: backgroundOffset },
countdown < 30000 && {
color:
theme && darkModeType
? COLORS.darkModeText
: COLORS.cancelRed,
},
]}
content={formatCountdown(countdown)}
/>
) : null
) : (
<CurrencySwitchButton
displayCurrency={
sourceMethod === 'USD' ? 'USD' : displayCurrency
}
onPress={openPicker}
disabled={isLoadingRate || sourceMethod === 'USD'}
/>
)
}
/>
<ThemeText
styles={styles.sectionTitle}
content={`${t('wallet.stablecoinSend.networkLabel', {
currency: asset,
chain: chainLabel || capitalizeChain(chain),
})}`}
/>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
<View style={CENTER}>
<FormattedBalanceInput
maxWidth={0.9}
amountValue={rawInput}
inputDenomination={primaryDisplay.denomination}
forceCurrency={primaryDisplay.forceCurrency}
forceFiatStats={primaryDisplay.forceFiatStats}
activeOpacity={!convertedSendAmount ? 0.5 : 1}
/>
<ThemeText
styles={styles.receiveAmount}
content={receiveAmountContent}
/>
</View>
{/* Confirm mode: fee info */}
{isConfirmMode && (
<SendTransactionFeeInfo
paymentFee={quote?.fee}
isLightningPayment={true}
isDecoding={isQuoteLoading}
/>
)}
{/* Confirm mode: destination info */}
{isConfirmMode && (
<TouchableOpacity
onPress={() =>
navigate.navigate('ErrorScreen', {
errorMessage: address,
})
}
style={[styles.destinationBox, { backgroundColor: rowBg }]}
>
<ThemeText
styles={styles.quoteValue}
content={`${truncateAddress(address)}`}
/>
</TouchableOpacity>
)}
</ScrollView>
{/* Source method picker: shown in edit mode only */}
{!isConfirmMode && (
<ChoosePaymentMethod
theme={theme}
darkModeType={darkModeType}
determinePaymentMethod={sourceMethod}
handleSelectPaymentMethod={handleMethodToggle}
bitcoinBalance={bitcoinBalance}
dollarBalanceToken={dollarBalanceToken}
masterInfoObject={masterInfoObject}
fiatStats={fiatStats}
uiState="EDIT_AMOUNT"
t={t}
containerStyles={{ marginTop: 5 }}
showPayWith={true}
/>
)}
{/* Description input: edit mode only */}
{!isConfirmMode && (
<CustomSearchInput
onFocusFunction={() => setIsAmountFocused(false)}
onBlurFunction={() => setIsAmountFocused(true)}
placeholderText={t('constants.paymentDescriptionPlaceholder')}
setInputText={setDescription}
inputText={description}
textInputMultiline={true}
textAlignVertical="baseline"
maxLength={150}
containerStyles={{
width: INSET_WINDOW_WIDTH,
marginTop: 10,
...CENTER,
}}
/>
)}
{/* EDIT_AMOUNT: keyboard + Review button */}
{!isConfirmMode && isAmountFocused && (
<CustomNumberKeyboard
setInputValue={setRawInput}
showDot={primaryDisplay.denomination === 'fiat'}
fiatStats={conversionFiatStats}
usingForBalance={true}
/>
)}
{!isConfirmMode && isAmountFocused && (
<CustomButton
textContent={t('constants.review')}
actionFunction={handleReview}
buttonStyles={{
...CENTER,
opacity: canReview ? 1 : HIDDEN_OPACITY,
}}
useLoading={isQuoteLoading}
/>
)}
{/* CONFIRM_PAYMENT: swipe button */}
{isConfirmMode && (
<View style={styles.buttonContainer}>
{sending ? (
<SliderProgressAnimation
ref={progressAnimationRef}
isVisible={true}
textColor={COLORS.darkModeText}
backgroundColor={
theme && darkModeType ? backgroundOffset : COLORS.primary
}
width={0.95}
/>
) : (
<SwipeButtonNew
onSwipeSuccess={handleSend}
width={0.85}
resetAfterSuccessAnimDuration={true}
shouldResetAfterSuccess={!sending}
containerStyles={{
opacity: canConfirm ? 1 : HIDDEN_OPACITY,
}}
thumbIconStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
railStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
/>
)}
</View>
)}
</View>
{/* Emoji bar for description input */}
{!isAmountFocused && !isConfirmMode && (
<EmojiQuickBar description={description} onEmojiSelect={handleEmoji} />
)}
</CustomKeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
globalContainer: {
flex: 1,
},
sectionTitle: {
fontSize: SIZES.medium,
opacity: HIDDEN_OPACITY,
textAlign: 'center',
marginBottom: CONTENT_KEYBOARD_OFFSET,
// textTransform: 'capitalize',
},
replacementContainer: {
flexGrow: 1,
width: WINDOWWIDTH,
...CENTER,
},
scrollContent: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
},
satValue: {
fontSize: SIZES.medium,
marginTop: 4,
},
destinationBox: {
width: '80%',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
borderRadius: 8,
...CENTER,
marginTop: 30,
},
receiveAmount: {
opacity: HIDDEN_OPACITY,
...CENTER,
},
countdownText: {
fontSize: SIZES.medium,
includeFontPadding: false,
paddingVertical: 5,
paddingHorizontal: 15,
borderRadius: 20,
},
quoteValue: {
fontSize: SIZES.medium,
includeFontPadding: false,
},
buttonContainer: {
width: '100%',
alignItems: 'center',
paddingVertical: 10,
},
});