* update translations * simplifying payments display * remove unconsistant icons * adding translations * fix usdb value display * fix tokens bug
1824 lines
62 KiB
JavaScript
1824 lines
62 KiB
JavaScript
import { StyleSheet, View, TouchableOpacity, ScrollView } from 'react-native';
|
|
import {
|
|
CENTER,
|
|
CONTENT_KEYBOARD_OFFSET,
|
|
QUICK_PAY_STORAGE_KEY,
|
|
SATSPERBITCOIN,
|
|
USDB_TOKEN_ID,
|
|
} from '../../../../constants';
|
|
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
|
|
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
|
import { useGlobalContextProvider } from '../../../../../context-store/context';
|
|
import {
|
|
CustomKeyboardAvoidingView,
|
|
GlobalThemeView,
|
|
ThemeText,
|
|
} from '../../../../functions/CustomElements';
|
|
import SendTransactionFeeInfo from './components/feeInfo';
|
|
import usePaymentValidation from './functions/paymentValidation';
|
|
import decodeSendAddress from './functions/decodeSendAdress';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
// import {useWebView} from '../../../../../context-store/webViewContext';
|
|
import GetThemeColors from '../../../../hooks/themeColors';
|
|
import CustomSearchInput from '../../../../functions/CustomElements/searchInput';
|
|
import FullLoadingScreen from '../../../../functions/CustomElements/loadingScreen';
|
|
import AcceptButtonSendPage from './components/acceptButton';
|
|
import NumberInputSendPage from './components/numberInput';
|
|
import FormattedBalanceInput from '../../../../functions/CustomElements/formattedBalanceInput';
|
|
import { useGlobalThemeContext } from '../../../../../context-store/theme';
|
|
import { useNodeContext } from '../../../../../context-store/nodeContext';
|
|
import ErrorWithPayment from './components/errorScreen';
|
|
import SwipeButtonNew from '../../../../functions/CustomElements/sliderButton';
|
|
import { crashlyticsLogReport } from '../../../../functions/crashlyticsLogs';
|
|
import {
|
|
isSendingPayingEventEmiiter,
|
|
SENDING_PAYMENT_EVENT_NAME,
|
|
useSparkWallet,
|
|
} from '../../../../../context-store/sparkContext';
|
|
import { sparkPaymenWrapper } from '../../../../functions/spark/payments';
|
|
import InvoiceInfo from './components/invoiceInfo';
|
|
import formatSparkPaymentAddress from './functions/formatSparkPaymentAddress';
|
|
import ChoosePaymentMethod from './components/choosePaymentMethodContainer';
|
|
import ChooseLRC20TokenContainer from './components/chooseLRC20TokenContainer';
|
|
import { useActiveCustodyAccount } from '../../../../../context-store/activeAccount';
|
|
import { useTranslation } from 'react-i18next';
|
|
import {
|
|
COLORS,
|
|
HIDDEN_OPACITY,
|
|
INSET_WINDOW_WIDTH,
|
|
SIZES,
|
|
WINDOWWIDTH,
|
|
} from '../../../../constants/theme';
|
|
import { SliderProgressAnimation } from '../../../../functions/CustomElements/sendPaymentAnimation';
|
|
import { InputTypes } from 'bitcoin-address-parser';
|
|
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
|
|
import { useWebView } from '../../../../../context-store/webViewContext';
|
|
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
|
|
import EmojiQuickBar from '../../../../functions/CustomElements/emojiBar';
|
|
import { useGlobalContactsInfo } from '../../../../../context-store/globalContacts';
|
|
import { bulkUpdateSparkTransactions } from '../../../../functions/spark/transactions';
|
|
import { useKeysContext } from '../../../../../context-store/keys';
|
|
import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext';
|
|
import CustomButton from '../../../../functions/CustomElements/button';
|
|
import { useFlashnet } from '../../../../../context-store/flashnetContext';
|
|
import SwapRatesChangedState from './components/swapRatesChangedState';
|
|
import {
|
|
dollarsToSats,
|
|
satsToDollars,
|
|
getLightningPaymentQuote,
|
|
USD_ASSET_ADDRESS,
|
|
} from '../../../../functions/spark/flashnet';
|
|
import useCurrencyDisplay from '../../../../hooks/useCurrencyDisplay';
|
|
import useDisplayCurrencyController from '../../../../hooks/useDisplayCurrencyController';
|
|
import normalizeLNURLAddress from '../../../../functions/lnurl/normalizeLNURLAddress';
|
|
import { publishMessage } from '../../../../functions/messaging/publishMessage';
|
|
import { useToast } from '../../../../../context-store/toastManager';
|
|
import useDebounce from '../../../../hooks/useDebounce';
|
|
import customUUID from '../../../../functions/customUUID';
|
|
import { useBudgetWarning } from '../../../../hooks/useBudgetWarning';
|
|
import { getLNAddressForLiquidPayment } from './functions/payments';
|
|
import formatTokensNumber from '../../../../functions/lrc20/formatTokensBalance';
|
|
import {
|
|
getDefaultDisplayCurrency,
|
|
resolveUsdFiatStats,
|
|
} from '../../../../functions/displayCurrency';
|
|
import CurrencySwitchButton from '../../../../functions/CustomElements/currencySwitchButton';
|
|
import SecondaryAmountDisplay from './components/secondaryAmountDisplay';
|
|
import { lnurlCurrencyToRate } from '../../../../functions/sendBitcoin/lnurlCurrencyRate';
|
|
import { PROVIDER_COUNTRY_CURRENCY } from '../../../../functions/sendBitcoin/getPhonePaymentAddress';
|
|
import { fiatCurrencies } from '../../../../functions/currencyOptions';
|
|
|
|
export default function SendPaymentScreen(props) {
|
|
console.log('CONFIRM SEND PAYMENT SCREEN');
|
|
const navigate = useNavigation();
|
|
const {
|
|
btcAdress,
|
|
fromPage,
|
|
publishMessageFunc,
|
|
comingFromAccept,
|
|
enteredPaymentInfo = {},
|
|
errorMessage,
|
|
contactInfo,
|
|
masterTokenInfo = {},
|
|
selectedPaymentMethod = '',
|
|
preSelectedPaymentMethod,
|
|
selectedContact,
|
|
retrivedContact,
|
|
paymentDisplayCurrency,
|
|
paymentDisplayFiatStats,
|
|
} = props.route.params;
|
|
|
|
const paramsRef = useRef({
|
|
btcAdress,
|
|
});
|
|
|
|
const { poolInfoRef, swapLimits, swapUSDPriceDollars } = useFlashnet();
|
|
const { t } = useTranslation();
|
|
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
|
|
useUserBalanceContext();
|
|
|
|
// Latest-value ref for balances (same pattern as stablecoinSendScreen). The
|
|
// send-decision memos/effects read balance through this ref instead of the
|
|
// reactive context values, so balance is dropped from their dependency
|
|
// arrays. A burst of incoming payments then can't recompute the payment
|
|
// method, flip uiState, or refire fee estimation mid-send. The actual send
|
|
// still validates against live balance via sparkInfoRef.current.
|
|
const balanceRef = useRef({
|
|
bitcoinBalance,
|
|
dollarBalanceSat,
|
|
dollarBalanceToken,
|
|
});
|
|
balanceRef.current.bitcoinBalance = bitcoinBalance;
|
|
balanceRef.current.dollarBalanceSat = dollarBalanceSat;
|
|
balanceRef.current.dollarBalanceToken = dollarBalanceToken;
|
|
const { sendWebViewRequest } = useWebView();
|
|
const { currentWalletMnemoinc } = useActiveCustodyAccount();
|
|
const { accountMnemoinc, contactsPrivateKey, publicKey } = useKeysContext();
|
|
const { sparkInformation, showTokensInformation, sparkInfoRef } =
|
|
useSparkWallet();
|
|
const { masterInfoObject } = useGlobalContextProvider();
|
|
const { liquidNodeInformation, fiatStats } = useNodeContext();
|
|
|
|
const { globalContactsInformation } = useGlobalContactsInfo();
|
|
const { theme, darkModeType } = useGlobalThemeContext();
|
|
const { textColor, backgroundOffset, backgroundColor } = GetThemeColors();
|
|
const { bottomPadding } = useGlobalInsets();
|
|
|
|
const didWarnAboutBudget = useRef(null);
|
|
const [isAmountFocused, setIsAmountFocused] = useState(true);
|
|
const [showProgressAnimation, setShowProgressAnimation] = useState(false);
|
|
const progressAnimationRef = useRef(null);
|
|
const hasTriggeredFastPay = useRef(false);
|
|
const convertedSendAmountRef = useRef(null);
|
|
const determinePaymentMethodRef = useRef(null);
|
|
const didRequireChoiceRef = useRef(false);
|
|
const uiStateRef = useRef(null);
|
|
const primaryDisplayRef = useRef(null);
|
|
const conversionFiatStatsRef = useRef(null);
|
|
const quoteId = useRef(null);
|
|
const isMountedRef = useRef(true);
|
|
// Tracks the decoded address we've already applied a local-currency default for,
|
|
// so the phone-payment currency default runs once per address (not every render).
|
|
const appliedLocalCurrencyRef = useRef(null);
|
|
|
|
// Drives the SWAP_RATES_CHANGED uiState when Flashnet rate drift breaks swap viability.
|
|
const [rateChangeDetected, setRateChangeDetected] = useState(false);
|
|
// Captures swapUSDPriceDollars on CONFIRM_PAYMENT entry (ref = no extra re-render).
|
|
const rateAtConfirmEntryRef = useRef(null);
|
|
|
|
const [didSelectPaymentMethod, setDidSelectPaymentMethod] = useState(false);
|
|
const [isDecoding, setIsDecoding] = useState(true);
|
|
const [paymentInfo, setPaymentInfo] = useState({});
|
|
const [lnFeeEstimate, setLnFeeEstimate] = useState(
|
|
enteredPaymentInfo?.lnFeeEstimate ?? null,
|
|
);
|
|
const [isEstimatingFee, setIsEstimatingFee] = useState(false);
|
|
const { showToast } = useToast();
|
|
|
|
const prevSelectedPaymentInfo = useRef({
|
|
preSelectedPaymentMethod,
|
|
enteredInfo: enteredPaymentInfo?.inputCurrency,
|
|
selectedPaymentMethod,
|
|
selectedLRC20Asset: masterTokenInfo?.tokenName,
|
|
});
|
|
|
|
const paymentMode =
|
|
preSelectedPaymentMethod === 'USD' ||
|
|
enteredPaymentInfo?.inputCurrency === 'USD' ||
|
|
selectedPaymentMethod === 'USD'
|
|
? 'USD'
|
|
: 'BTC';
|
|
|
|
const [paymentDescription, setPaymentDescription] = useState('');
|
|
const [loadingMessage, setLoadingMessage] = useState(
|
|
sparkInformation.didConnect
|
|
? t('wallet.sendPages.sendPaymentScreen.initialLoadingMessage')
|
|
: t('wallet.sendPages.sendPaymentScreen.connectToSparkMessage'),
|
|
);
|
|
const [refreshDecode, setRefreshDecode] = useState(0);
|
|
const isSendingPayment = useRef(null);
|
|
const userPaymentMethod = selectedPaymentMethod || preSelectedPaymentMethod;
|
|
const combinedPaymentDescription =
|
|
paymentDescription ||
|
|
paymentInfo?.data?.label ||
|
|
paymentInfo?.data?.message ||
|
|
'';
|
|
|
|
// Payment type flags
|
|
const isLightningPayment = paymentInfo?.paymentNetwork === 'lightning';
|
|
const isLiquidPayment = paymentInfo?.paymentNetwork === 'liquid';
|
|
const isBitcoinPayment = paymentInfo?.paymentNetwork === 'Bitcoin';
|
|
const isSparkPayment = paymentInfo?.paymentNetwork === 'spark';
|
|
const isLNURLPayment = paymentInfo?.type === InputTypes.LNURL_PAY;
|
|
const isUsingBranta = paymentInfo?.isUsingBranta;
|
|
|
|
const enabledLRC20 = showTokensInformation;
|
|
const defaultToken = enabledLRC20
|
|
? masterInfoObject?.defaultSpendToken || 'Bitcoin'
|
|
: 'Bitcoin';
|
|
|
|
const selectedContactInfo = contactInfo || paymentInfo?.blitzContactInfo;
|
|
|
|
// Intentionally keyed off `contactInfo` only: including blitzContactInfo here
|
|
// would swap the token selector for the BTC/USD chooser and unpin the payment
|
|
// method for @username sends. Display is handled by the InvoiceInfo props.
|
|
const useFullTokensDisplay =
|
|
enabledLRC20 &&
|
|
isSparkPayment &&
|
|
paymentInfo?.data?.expectedToken !== USDB_TOKEN_ID &&
|
|
!contactInfo;
|
|
|
|
// const showSendMax = !dollarBalanceSat && !bitcoinBalance;
|
|
|
|
// finds the true min swap amount
|
|
const min_usd_swap_amount = useMemo(() => {
|
|
return Math.round(
|
|
dollarsToSats(swapLimits.usd, poolInfoRef.currentPriceAInB),
|
|
);
|
|
}, [poolInfoRef.currentPriceAInB, swapLimits]);
|
|
|
|
const minLNURLSatAmount = isLNURLPayment
|
|
? paymentInfo?.data?.minSendable / 1000
|
|
: 0;
|
|
const maxLNURLSatAmount = isLNURLPayment
|
|
? paymentInfo?.data?.maxSendable / 1000
|
|
: 0;
|
|
|
|
// Tokens only exist on Spark — a bolt11/on-chain/LNURL invoice can never be
|
|
// funded with the default spend token. Before the decode lands paymentNetwork
|
|
// is undefined, so keep the user's pick: processSparkAddress reads it to
|
|
// decide isLRC20.
|
|
const selectedLRC20Asset =
|
|
!paymentInfo?.paymentNetwork || useFullTokensDisplay
|
|
? masterTokenInfo?.tokenName || defaultToken
|
|
: 'Bitcoin';
|
|
const seletctedToken =
|
|
masterTokenInfo?.details ||
|
|
sparkInformation?.tokens?.[selectedLRC20Asset] ||
|
|
{};
|
|
const tokenDecimals = seletctedToken?.tokenMetadata?.decimals ?? 0;
|
|
const tokenBalance = seletctedToken?.balance ?? 0;
|
|
const isUsingLRC20 = selectedLRC20Asset?.toLowerCase() !== 'bitcoin';
|
|
// The ticker actually shown on this screen. USDB-funded sends that weren't
|
|
// picked through the token selector display as fiat, so the confirm screen
|
|
// must be told the label instead of re-deriving it from the tx's token info.
|
|
const displayTokenTicker = isUsingLRC20
|
|
? seletctedToken?.tokenMetadata?.tokenTicker
|
|
: '';
|
|
|
|
const sendingAmount = paymentInfo?.sendAmount || 0;
|
|
const canEditAmount = paymentInfo?.canEditPayment === true;
|
|
const receiverExpectsCurrency = paymentInfo?.data?.expectedReceive || 'sats';
|
|
|
|
// True when the payment amount itself meets the swap minimum for this direction.
|
|
// For editable amounts or zero-amount invoices the user can change the value,
|
|
// so we treat them as viable and let downstream validation enforce the limit.
|
|
const amountViableForSwap = useMemo(() => {
|
|
if (canEditAmount || paymentInfo?.usingZeroAmountInvoice) return true;
|
|
const amountSat = Number(paymentInfo?.amountSat) || 0;
|
|
|
|
// BTC→USD swap minimum is expressed in USD, converted to sats
|
|
if (receiverExpectsCurrency === 'tokens')
|
|
return amountSat >= min_usd_swap_amount;
|
|
// USD→BTC swap minimum is expressed directly in sats
|
|
return amountSat >= swapLimits.bitcoin;
|
|
}, [
|
|
canEditAmount,
|
|
paymentInfo?.usingZeroAmountInvoice,
|
|
paymentInfo?.amountSat,
|
|
paymentInfo?.data?.expectedReceive,
|
|
min_usd_swap_amount,
|
|
swapLimits.bitcoin,
|
|
receiverExpectsCurrency,
|
|
]);
|
|
|
|
const isBTCOnlyPayment =
|
|
isBitcoinPayment ||
|
|
(isLightningPayment && paymentInfo?.usingZeroAmountInvoice);
|
|
|
|
const resolvedPaymentMethod = useMemo(() => {
|
|
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
|
|
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
|
|
balanceRef.current;
|
|
if (!paymentInfo || !Object.keys(paymentInfo || {}).length)
|
|
return undefined;
|
|
|
|
if (isBitcoinPayment) return 'BTC';
|
|
if (isUsingLRC20) return 'BTC';
|
|
if (isSparkPayment && useFullTokensDisplay) return 'BTC';
|
|
if (isLightningPayment && paymentInfo?.usingZeroAmountInvoice) return 'BTC';
|
|
|
|
const flashnetReady = sparkInformation?.didConnectToFlashnet;
|
|
const receiverExpectsTokens = receiverExpectsCurrency === 'tokens';
|
|
const amountSat = paymentInfo?.amountSat || 0;
|
|
|
|
const canUse = method => {
|
|
if (method === 'USD') {
|
|
if (Number(dollarBalanceToken) < 0.01) return false;
|
|
if (receiverExpectsTokens) return dollarBalanceSat >= amountSat;
|
|
return (
|
|
flashnetReady &&
|
|
Number(dollarBalanceToken) >= swapLimits.usd &&
|
|
amountViableForSwap
|
|
);
|
|
} else {
|
|
if (!bitcoinBalance) return false;
|
|
if (receiverExpectsTokens)
|
|
return (
|
|
flashnetReady &&
|
|
bitcoinBalance >= swapLimits.bitcoin &&
|
|
amountViableForSwap
|
|
);
|
|
return bitcoinBalance >= amountSat;
|
|
}
|
|
};
|
|
|
|
const resolvePreferred = preferred => {
|
|
if (canUse(preferred)) return preferred;
|
|
const opposite = preferred === 'USD' ? 'BTC' : 'USD';
|
|
if (canUse(opposite)) return opposite;
|
|
return preferred;
|
|
};
|
|
|
|
if (preSelectedPaymentMethod)
|
|
return resolvePreferred(preSelectedPaymentMethod);
|
|
if (userPaymentMethod) return userPaymentMethod;
|
|
if (enteredPaymentInfo?.inputCurrency)
|
|
return resolvePreferred(enteredPaymentInfo.inputCurrency);
|
|
|
|
const btcViable = canUse('BTC');
|
|
const usdViable = canUse('USD');
|
|
if (btcViable && usdViable)
|
|
return receiverExpectsTokens
|
|
? dollarBalanceSat >= bitcoinBalance
|
|
? 'USD'
|
|
: 'BTC'
|
|
: bitcoinBalance >= dollarBalanceSat
|
|
? 'BTC'
|
|
: 'USD';
|
|
if (btcViable) return 'BTC';
|
|
if (usdViable) return 'USD';
|
|
return 'BTC';
|
|
}, [
|
|
preSelectedPaymentMethod,
|
|
enteredPaymentInfo?.inputCurrency,
|
|
userPaymentMethod,
|
|
paymentInfo,
|
|
isBitcoinPayment,
|
|
isUsingLRC20,
|
|
isSparkPayment,
|
|
useFullTokensDisplay,
|
|
isLightningPayment,
|
|
sparkInformation?.didConnectToFlashnet,
|
|
swapLimits,
|
|
canEditAmount,
|
|
min_usd_swap_amount,
|
|
amountViableForSwap,
|
|
receiverExpectsCurrency,
|
|
]);
|
|
|
|
const paymentFee =
|
|
resolvedPaymentMethod === 'USD' && paymentInfo?.usdPaymentFee != null
|
|
? (paymentInfo.usdPaymentFee || 0) + (paymentInfo.usdSupportFee || 0)
|
|
: (paymentInfo?.paymentFee || 0) + (paymentInfo?.supportFee || 0);
|
|
|
|
const effectivePaymentFee =
|
|
isLightningPayment && lnFeeEstimate !== null
|
|
? lnFeeEstimate + (paymentInfo?.supportFee || 0)
|
|
: paymentFee;
|
|
|
|
const usdFiatStats = useMemo(
|
|
() => resolveUsdFiatStats(fiatStats, swapUSDPriceDollars),
|
|
[fiatStats, swapUSDPriceDollars],
|
|
);
|
|
// When arriving from a flow where the user already picked a display rate
|
|
// (e.g. a contact send), honor that currency instead of re-deriving the
|
|
// device-based default. Falls back to the legacy default otherwise.
|
|
const initialDisplayCurrency = useMemo(
|
|
() =>
|
|
paymentDisplayCurrency ||
|
|
getDefaultDisplayCurrency({
|
|
paymentMode: resolvedPaymentMethod || paymentMode,
|
|
masterInfoObject,
|
|
fiatStats,
|
|
}),
|
|
[
|
|
paymentDisplayCurrency,
|
|
resolvedPaymentMethod,
|
|
paymentMode,
|
|
masterInfoObject,
|
|
fiatStats,
|
|
],
|
|
);
|
|
const additionalRates = useMemo(
|
|
() =>
|
|
paymentDisplayCurrency && paymentDisplayFiatStats?.value
|
|
? { [paymentDisplayCurrency]: paymentDisplayFiatStats }
|
|
: undefined,
|
|
[paymentDisplayCurrency, paymentDisplayFiatStats],
|
|
);
|
|
const {
|
|
displayCurrency,
|
|
currencyRates,
|
|
isLoadingRate,
|
|
selectCurrency,
|
|
loadAndSetCurrency,
|
|
injectRate,
|
|
hasUserSelectedDisplayCurrency,
|
|
} = useDisplayCurrencyController({
|
|
initialCurrency: initialDisplayCurrency,
|
|
fiatStats,
|
|
usdFiatStats,
|
|
masterInfoObject,
|
|
additionalRates,
|
|
});
|
|
|
|
const {
|
|
primaryDisplay,
|
|
secondaryDisplay,
|
|
conversionFiatStats,
|
|
convertSatsToDisplay,
|
|
convertDisplayToSats,
|
|
buildAmountSnapshot,
|
|
} = useCurrencyDisplay({
|
|
displayCurrency,
|
|
fiatStats,
|
|
usdFiatStats,
|
|
currencyRates,
|
|
masterInfoObject,
|
|
isSendingPayment: isSendingPayment.current,
|
|
});
|
|
|
|
// Prefer the user's verbatim entered string (display-truth) when we carried it across
|
|
// the accept/contact handoff and it was entered in the denomination we're now showing.
|
|
// Only genuine fixed-from-invoice amounts (no entry) fall back to reconversion.
|
|
// Guard keys on denomination only (fiat vs sats), not the specific currency: the contact
|
|
// handoff pins paymentDisplayCurrency/paymentDisplayFiatStats and the confirm view hides the
|
|
// currency switcher, so a same-denomination/different-currency mismatch cannot occur here.
|
|
const hasEnteredDisplay =
|
|
paymentInfo?.enteredDisplayAmount != null &&
|
|
paymentInfo?.enteredDisplayAmount !== '' &&
|
|
paymentInfo?.enteredDisplayDenomination === primaryDisplay.denomination;
|
|
|
|
const displayAmount = canEditAmount
|
|
? sendingAmount
|
|
: hasEnteredDisplay
|
|
? paymentInfo.enteredDisplayAmount
|
|
: convertSatsToDisplay(sendingAmount);
|
|
|
|
const convertedSendAmount = !isUsingLRC20
|
|
? canEditAmount
|
|
? convertDisplayToSats(sendingAmount) // User entered amount, convert to sats
|
|
: Number(sendingAmount) // Fixed invoice amount, already in sats
|
|
: Number(sendingAmount);
|
|
|
|
// use stablepool info ref so the fiat amount doesnt change in between inputs leading to a false amount being sent.
|
|
const fiatValueConvertedSendAmount = Math.round(
|
|
satsToDollars(
|
|
convertedSendAmount,
|
|
enteredPaymentInfo?.stablePoolInfoRef?.currentPriceAInB ||
|
|
poolInfoRef.currentPriceAInB,
|
|
).toFixed(2) * Math.pow(10, 6),
|
|
);
|
|
|
|
const requiresUserMethodSelection = useMemo(() => {
|
|
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
|
|
const { bitcoinBalance, dollarBalanceSat } = balanceRef.current;
|
|
if (resolvedPaymentMethod === undefined) return false;
|
|
if (
|
|
!!preSelectedPaymentMethod ||
|
|
useFullTokensDisplay ||
|
|
isUsingLRC20 ||
|
|
didSelectPaymentMethod
|
|
)
|
|
return false;
|
|
|
|
if (!sparkInformation?.didConnectToFlashnet) return false;
|
|
|
|
if (isBitcoinPayment) return false;
|
|
|
|
if (isLightningPayment && paymentInfo?.usingZeroAmountInvoice) return false;
|
|
|
|
const formattedFee = isLightningPayment ? effectivePaymentFee : 0;
|
|
|
|
const hasEnoughBTC = bitcoinBalance >= convertedSendAmount + formattedFee;
|
|
const hasEnoughUSD = dollarBalanceSat >= convertedSendAmount + formattedFee;
|
|
|
|
const canSendWithBTC =
|
|
resolvedPaymentMethod === 'BTC' && receiverExpectsCurrency === 'sats'
|
|
? hasEnoughBTC
|
|
: hasEnoughBTC && amountViableForSwap;
|
|
|
|
const canSendWIthUSD =
|
|
resolvedPaymentMethod === 'USD' && receiverExpectsCurrency === 'tokens'
|
|
? hasEnoughUSD
|
|
: hasEnoughUSD && amountViableForSwap;
|
|
|
|
if (
|
|
(canSendWithBTC && canSendWIthUSD) ||
|
|
(!canSendWithBTC && !canSendWIthUSD)
|
|
)
|
|
return true;
|
|
else return false;
|
|
}, [
|
|
isBitcoinPayment,
|
|
useFullTokensDisplay,
|
|
isUsingLRC20,
|
|
didSelectPaymentMethod,
|
|
sparkInformation?.didConnectToFlashnet,
|
|
paymentInfo?.data?.expectedReceive,
|
|
amountViableForSwap,
|
|
resolvedPaymentMethod,
|
|
receiverExpectsCurrency,
|
|
preSelectedPaymentMethod,
|
|
isLightningPayment,
|
|
]);
|
|
|
|
const { shouldWarn } = useBudgetWarning(convertedSendAmount);
|
|
|
|
useEffect(() => {
|
|
primaryDisplayRef.current = primaryDisplay;
|
|
}, [primaryDisplay]);
|
|
useEffect(() => {
|
|
conversionFiatStatsRef.current = conversionFiatStats;
|
|
}, [conversionFiatStats]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
isMountedRef.current = false;
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
determinePaymentMethodRef.current = resolvedPaymentMethod;
|
|
}, [resolvedPaymentMethod]);
|
|
|
|
const estimateLightningFee = useCallback(
|
|
async (amount, id) => {
|
|
// eslint-disable-next-line no-shadow -- read frozen balance, decoupled from live deps
|
|
const { bitcoinBalance, dollarBalanceSat, dollarBalanceToken } =
|
|
balanceRef.current;
|
|
if (!amount || !isLightningPayment || !canEditAmount) {
|
|
setIsEstimatingFee(false);
|
|
return;
|
|
}
|
|
if (quoteId.current !== id) return;
|
|
|
|
const balance =
|
|
resolvedPaymentMethod === 'USD' ? dollarBalanceSat : bitcoinBalance;
|
|
const bufferAmount = amount * 1.1;
|
|
|
|
// Skip if balance easily covers the send + estimated fee buffer, or if
|
|
// already over balance (validation will catch it without needing a fee)
|
|
if (bufferAmount < balance || amount > balance) {
|
|
setIsEstimatingFee(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const formattedSparkPaymentInfo = formatSparkPaymentAddress(
|
|
paymentInfo,
|
|
false,
|
|
);
|
|
let invoice = formattedSparkPaymentInfo.address;
|
|
if (paymentInfo.type === InputTypes.LNURL_PAY && !invoice) {
|
|
const invoiceResponse = await getLNAddressForLiquidPayment(
|
|
paymentInfo.decodedInput,
|
|
amount,
|
|
);
|
|
|
|
if (!invoiceResponse.pr) throw new Error('No invoice received');
|
|
invoice = invoiceResponse.pr;
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
data: { ...(prev.data || {}), invoice },
|
|
}));
|
|
}
|
|
|
|
if (!invoice) {
|
|
setIsEstimatingFee(false);
|
|
return;
|
|
}
|
|
if (resolvedPaymentMethod === 'USD') {
|
|
const quote = await getLightningPaymentQuote(
|
|
currentWalletMnemoinc,
|
|
invoice,
|
|
USD_ASSET_ADDRESS,
|
|
undefined,
|
|
undefined,
|
|
{ amountSats: amount },
|
|
);
|
|
if (!quote.didWork)
|
|
throw new Error(quote.error || 'Fee quote failed');
|
|
if (quoteId.current !== id) return;
|
|
const estimatedAmmFeeSat = Math.round(
|
|
dollarsToSats(
|
|
quote.quote.estimatedAmmFee / Math.pow(10, 6),
|
|
poolInfoRef.currentPriceAInB,
|
|
),
|
|
);
|
|
const dollarAmountRequired = quote.quote.tokenAmountRequired;
|
|
const userDollarBalance = dollarBalanceToken * Math.pow(10, 6);
|
|
const fee = quote.quote.estimatedLightningFee + estimatedAmmFeeSat;
|
|
if (dollarAmountRequired > userDollarBalance) {
|
|
showToast({
|
|
type: 'error',
|
|
title: t('errormessages.lightningAmountRequiredWarning', {
|
|
amount: displayCorrectDenomination({
|
|
amount: parseFloat(
|
|
formatTokensNumber(quote.quote.tokenAmountRequired, 6),
|
|
).toFixed(2),
|
|
masterInfoObject: {
|
|
...masterInfoObject,
|
|
userBalanceDenomination: 'fiat',
|
|
},
|
|
fiatStats,
|
|
convertAmount: false,
|
|
forceCurrency: 'USD',
|
|
}),
|
|
}),
|
|
duration: 6000,
|
|
});
|
|
}
|
|
setLnFeeEstimate(fee);
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
paymentFee: fee,
|
|
supportFee: prev.supportFee ?? 0,
|
|
swapPaymentQuote: {
|
|
...quote.quote,
|
|
bitcoinBalance,
|
|
dollarBalanceSat,
|
|
},
|
|
}));
|
|
} else {
|
|
const feeResult = await sparkPaymenWrapper({
|
|
getFee: true,
|
|
paymentType: 'lightning',
|
|
address: invoice,
|
|
amountSats: amount,
|
|
masterInfoObject,
|
|
sparkInformation: sparkInfoRef.current,
|
|
mnemonic: currentWalletMnemoinc,
|
|
sendWebViewRequest,
|
|
});
|
|
if (!feeResult.didWork) throw new Error('Fee estimation failed');
|
|
if (quoteId.current !== id) return;
|
|
const fee = feeResult.fee;
|
|
if (fee + amount > bitcoinBalance) {
|
|
showToast({
|
|
type: 'error',
|
|
title: t('errormessages.lightningAmountFeeWarning', {
|
|
amount: displayCorrectDenomination({
|
|
amount: fee,
|
|
masterInfoObject: {
|
|
...masterInfoObject,
|
|
userBalanceDenomination: 'sats',
|
|
},
|
|
fiatStats,
|
|
}),
|
|
}),
|
|
duration: 6000,
|
|
});
|
|
}
|
|
setLnFeeEstimate(fee);
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
paymentFee: fee,
|
|
supportFee: prev.supportFee ?? 0,
|
|
}));
|
|
}
|
|
} catch {
|
|
showToast({
|
|
type: 'error',
|
|
title: t('wallet.sendPages.sendPaymentScreen.feeEstimateError'),
|
|
});
|
|
} finally {
|
|
if (quoteId.current === id) {
|
|
setIsEstimatingFee(false);
|
|
}
|
|
}
|
|
},
|
|
[
|
|
isLightningPayment,
|
|
canEditAmount,
|
|
resolvedPaymentMethod,
|
|
paymentInfo,
|
|
currentWalletMnemoinc,
|
|
masterInfoObject,
|
|
fiatStats,
|
|
sparkInfoRef,
|
|
sendWebViewRequest,
|
|
showToast,
|
|
t,
|
|
],
|
|
);
|
|
|
|
const debouncedEstimateFee = useDebounce(estimateLightningFee, 600);
|
|
|
|
useEffect(() => {
|
|
if (requiresUserMethodSelection && !didRequireChoiceRef.current) {
|
|
didRequireChoiceRef.current = true;
|
|
}
|
|
}, [requiresUserMethodSelection]);
|
|
|
|
// Fast pay logic
|
|
const canUseFastPay =
|
|
sparkInformation.didConnect &&
|
|
Object.keys(paymentInfo || {}).length > 0 &&
|
|
masterInfoObject[QUICK_PAY_STORAGE_KEY]?.isFastPayEnabled &&
|
|
masterInfoObject[QUICK_PAY_STORAGE_KEY]?.fastPayThresholdSats >=
|
|
convertedSendAmount &&
|
|
!isUsingLRC20 &&
|
|
(!didRequireChoiceRef.current || didSelectPaymentMethod) &&
|
|
!requiresUserMethodSelection &&
|
|
convertedSendAmount >= effectivePaymentFee &&
|
|
!shouldWarn;
|
|
|
|
const uiState = useMemo(() => {
|
|
if (canEditAmount && !isSendingPayment.current) {
|
|
return 'EDIT_AMOUNT'; // Show number pad + description input
|
|
}
|
|
|
|
// Rate-change intercept: show before CHOOSE_METHOD / CONFIRM_PAYMENT so it
|
|
// takes over the screen whenever the Flashnet rate broke swap viability.
|
|
if (rateChangeDetected) {
|
|
return 'SWAP_RATES_CHANGED';
|
|
}
|
|
|
|
if (
|
|
requiresUserMethodSelection &&
|
|
!isSendingPayment.current &&
|
|
!isBitcoinPayment &&
|
|
!isUsingLRC20 &&
|
|
!preSelectedPaymentMethod
|
|
) {
|
|
return 'CHOOSE_METHOD'; // Show info screen with button to select method
|
|
}
|
|
|
|
return 'CONFIRM_PAYMENT'; // Show swipe button
|
|
}, [
|
|
canEditAmount,
|
|
rateChangeDetected,
|
|
requiresUserMethodSelection,
|
|
didSelectPaymentMethod,
|
|
isBitcoinPayment,
|
|
isUsingLRC20,
|
|
canUseFastPay,
|
|
preSelectedPaymentMethod,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
uiStateRef.current = uiState;
|
|
}, [uiState]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
uiState === 'CONFIRM_PAYMENT' &&
|
|
shouldWarn &&
|
|
!didWarnAboutBudget.current &&
|
|
!isSendingPayment.current &&
|
|
!isDecoding
|
|
) {
|
|
didWarnAboutBudget.current = true;
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'nearBudgetLimitWarning',
|
|
sliderHight: 0.6,
|
|
sendingAmount: convertedSendAmount,
|
|
});
|
|
}
|
|
}, [uiState, shouldWarn, convertedSendAmount, isDecoding]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
prevSelectedPaymentInfo.current.preSelectedPaymentMethod !==
|
|
preSelectedPaymentMethod ||
|
|
prevSelectedPaymentInfo.current.enteredInfo !==
|
|
enteredPaymentInfo?.inputCurrency ||
|
|
prevSelectedPaymentInfo.current.selectedPaymentMethod !==
|
|
selectedPaymentMethod ||
|
|
prevSelectedPaymentInfo.current.selectedLRC20Asset !== selectedLRC20Asset
|
|
) {
|
|
console.log(
|
|
'Payment method or input currency changed, resetting payment info',
|
|
);
|
|
if (uiStateRef.current !== 'EDIT_AMOUNT') return;
|
|
console.log('Resetting payment info for new selection');
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
sendAmount: '',
|
|
feeQuote: undefined,
|
|
swapPaymentQuote: undefined,
|
|
paymentFee: 0,
|
|
supportFee: 0,
|
|
}));
|
|
prevSelectedPaymentInfo.current = {
|
|
preSelectedPaymentMethod,
|
|
enteredInfo: enteredPaymentInfo?.inputCurrency,
|
|
selectedPaymentMethod,
|
|
selectedLRC20Asset,
|
|
};
|
|
}
|
|
}, [
|
|
preSelectedPaymentMethod,
|
|
enteredPaymentInfo?.inputCurrency,
|
|
selectedPaymentMethod,
|
|
selectedLRC20Asset,
|
|
]);
|
|
|
|
const paymentValidation = usePaymentValidation({
|
|
paymentInfo,
|
|
convertedSendAmount,
|
|
paymentFee: effectivePaymentFee,
|
|
determinePaymentMethod: resolvedPaymentMethod,
|
|
selectedPaymentMethod: userPaymentMethod,
|
|
bitcoinBalance,
|
|
dollarBalanceSat,
|
|
dollarBalanceToken,
|
|
min_usd_swap_amount,
|
|
swapLimits,
|
|
isUsingLRC20,
|
|
seletctedToken,
|
|
minLNURLSatAmount,
|
|
maxLNURLSatAmount,
|
|
isDecoding,
|
|
canEditAmount,
|
|
t,
|
|
masterInfoObject,
|
|
fiatStats,
|
|
inputDenomination: primaryDisplay.denomination,
|
|
primaryDisplay,
|
|
conversionFiatStats,
|
|
sparkInformation,
|
|
poolInfoRef,
|
|
});
|
|
console.log(paymentValidation);
|
|
|
|
const canSendPayment =
|
|
paymentValidation.canProceed &&
|
|
sendingAmount !== 0 &&
|
|
uiState === 'CONFIRM_PAYMENT';
|
|
|
|
const isUsingFastPay = canUseFastPay && canSendPayment && !canEditAmount;
|
|
|
|
// Rate-sensitive swap path: only Flashnet swaps (USD→BTC / BTC→USDB) are affected
|
|
// by live rate changes. Mirrors needsSwap in paymentValidation.
|
|
const needsRateSwap =
|
|
(resolvedPaymentMethod === 'USD' && receiverExpectsCurrency === 'sats') ||
|
|
(resolvedPaymentMethod === 'BTC' && receiverExpectsCurrency === 'tokens');
|
|
|
|
// Snapshot + detection effect.
|
|
// Captures the rate on CONFIRM_PAYMENT entry; when a subsequent rate tick
|
|
// breaks swap viability (convertedSendAmount shrinks, min_usd_swap_amount rises),
|
|
// sets rateChangeDetected → transitions to SWAP_RATES_CHANGED uiState.
|
|
// Resets completely when the user leaves the confirm flow.
|
|
useEffect(() => {
|
|
if (isDecoding) return;
|
|
if (uiState === 'CONFIRM_PAYMENT') {
|
|
// Capture rate once on entry
|
|
if (rateAtConfirmEntryRef.current === null) {
|
|
rateAtConfirmEntryRef.current = swapUSDPriceDollars;
|
|
}
|
|
// Detect drift that broke viability
|
|
if (
|
|
rateAtConfirmEntryRef.current !== null &&
|
|
swapUSDPriceDollars !== rateAtConfirmEntryRef.current &&
|
|
!paymentValidation.canProceed &&
|
|
needsRateSwap &&
|
|
!isSendingPayment.current
|
|
) {
|
|
setRateChangeDetected(true);
|
|
}
|
|
} else if (uiState !== 'SWAP_RATES_CHANGED') {
|
|
// Leaving the confirm flow entirely (EDIT / CHOOSE / back) — full reset.
|
|
// Guard against SWAP_RATES_CHANGED so the state doesn't clear itself.
|
|
rateAtConfirmEntryRef.current = null;
|
|
setRateChangeDetected(false);
|
|
}
|
|
}, [
|
|
uiState,
|
|
swapUSDPriceDollars,
|
|
paymentValidation.canProceed,
|
|
needsRateSwap,
|
|
isDecoding,
|
|
]);
|
|
|
|
const handleRateChangedReset = useCallback(() => {
|
|
rateAtConfirmEntryRef.current = null;
|
|
setRateChangeDetected(false);
|
|
if (isLNURLPayment || isSparkPayment) {
|
|
// Full reset — mirrors hasParamsChanged effect sequence exactly
|
|
setIsAmountFocused(true);
|
|
setPaymentInfo({});
|
|
setLnFeeEstimate(null);
|
|
setIsEstimatingFee(false);
|
|
isSendingPayment.current = null;
|
|
setPaymentDescription('');
|
|
hasTriggeredFastPay.current = false;
|
|
didRequireChoiceRef.current = false;
|
|
setLoadingMessage(
|
|
sparkInformation.didConnect && sparkInformation.identityPubKey
|
|
? t('wallet.sendPages.sendPaymentScreen.initialLoadingMessage')
|
|
: t('wallet.sendPages.sendPaymentScreen.connectToSparkMessage'),
|
|
);
|
|
setDidSelectPaymentMethod(false);
|
|
setShowProgressAnimation(false);
|
|
setRefreshDecode(x => x + 1);
|
|
} else {
|
|
navigate.goBack();
|
|
}
|
|
}, [
|
|
isLNURLPayment,
|
|
isSparkPayment,
|
|
navigate,
|
|
sparkInformation.didConnect,
|
|
sparkInformation.identityPubKey,
|
|
t,
|
|
]);
|
|
|
|
const errorMessageNavigation = useCallback(
|
|
reason => {
|
|
if (!isMountedRef.current) return;
|
|
navigate.navigate('ConfirmPaymentScreen', {
|
|
btcAdress: '',
|
|
fromPage: '',
|
|
publishMessageFunc: null,
|
|
comingFromAccept: null,
|
|
enteredPaymentInfo: {},
|
|
errorMessage:
|
|
reason ||
|
|
t('wallet.sendPages.sendPaymentScreen.fallbackErrorMessage'),
|
|
});
|
|
},
|
|
[navigate, t],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const currentParams = { btcAdress };
|
|
const prevParams = paramsRef.current;
|
|
|
|
const hasParamsChanged =
|
|
currentParams.btcAdress &&
|
|
currentParams.btcAdress !== prevParams.btcAdress;
|
|
|
|
if (hasParamsChanged) {
|
|
setIsAmountFocused(true);
|
|
setPaymentInfo({});
|
|
setLnFeeEstimate(null);
|
|
setIsEstimatingFee(false);
|
|
isSendingPayment.current = null;
|
|
setPaymentDescription('');
|
|
hasTriggeredFastPay.current = false;
|
|
didRequireChoiceRef.current = false;
|
|
setLoadingMessage(
|
|
!!sparkInformation.didConnect && !!sparkInformation.identityPubKey
|
|
? t('wallet.sendPages.sendPaymentScreen.initialLoadingMessage')
|
|
: t('wallet.sendPages.sendPaymentScreen.connectToSparkMessage'),
|
|
);
|
|
setDidSelectPaymentMethod(false);
|
|
setShowProgressAnimation(false);
|
|
setRefreshDecode(x => x + 1);
|
|
paramsRef.current = currentParams;
|
|
}
|
|
}, [
|
|
btcAdress,
|
|
sparkInformation.didConnect,
|
|
sparkInformation.identityPubKey,
|
|
t,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
convertedSendAmountRef.current = convertedSendAmount;
|
|
}, [convertedSendAmount]);
|
|
|
|
useEffect(() => {
|
|
if (!canEditAmount || !isLightningPayment) return;
|
|
setLnFeeEstimate(null);
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
data: { ...(prev.data || {}), invoice: '' },
|
|
paymentFee: 0,
|
|
}));
|
|
if (convertedSendAmount > 0) {
|
|
const id = customUUID();
|
|
quoteId.current = id;
|
|
setIsEstimatingFee(true);
|
|
debouncedEstimateFee(convertedSendAmount, id);
|
|
}
|
|
}, [convertedSendAmount, canEditAmount, isLightningPayment]);
|
|
|
|
useEffect(() => {
|
|
async function decodePayment() {
|
|
crashlyticsLogReport('Starting decode address');
|
|
setIsDecoding(true);
|
|
await decodeSendAddress({
|
|
fiatStats,
|
|
btcAdress: paramsRef.current.btcAdress,
|
|
goBackFunction: errorMessageNavigation,
|
|
setPaymentInfo,
|
|
liquidNodeInformation,
|
|
masterInfoObject: {
|
|
...masterInfoObject,
|
|
userBalanceDenomination:
|
|
primaryDisplayRef.current?.denomination || 'sats',
|
|
},
|
|
// setWebViewArgs,
|
|
// webViewRef,
|
|
navigate,
|
|
// maxZeroConf:
|
|
// minMaxLiquidSwapAmounts?.submarineSwapStats?.limits?.maximalZeroConf,
|
|
comingFromAccept: enteredPaymentInfo.fromContacts,
|
|
enteredPaymentInfo,
|
|
setLoadingMessage,
|
|
paymentInfo,
|
|
fromPage,
|
|
sparkInformation,
|
|
seletctedToken,
|
|
currentWalletMnemoinc,
|
|
t,
|
|
sendWebViewRequest,
|
|
contactInfo,
|
|
globalContactsInformation,
|
|
accountMnemoinc,
|
|
usablePaymentMethod:
|
|
userPaymentMethod || determinePaymentMethodRef.current,
|
|
bitcoinBalance,
|
|
dollarBalanceSat,
|
|
convertedSendAmount: convertedSendAmountRef.current,
|
|
poolInfoRef,
|
|
swapLimits,
|
|
// usd_multiplier_coefiicent,
|
|
min_usd_swap_amount,
|
|
primaryDisplay: primaryDisplayRef.current,
|
|
conversionFiatStats: conversionFiatStatsRef.current,
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
});
|
|
setIsDecoding(false);
|
|
}
|
|
|
|
if (!sparkInformation.didConnect || !sparkInformation.identityPubKey)
|
|
return;
|
|
if (isSendingPayment.current) return;
|
|
|
|
requestAnimationFrame(() => {
|
|
decodePayment();
|
|
});
|
|
}, [
|
|
sparkInformation.didConnect,
|
|
sparkInformation.identityPubKey,
|
|
refreshDecode,
|
|
// selectedPaymentMethod,
|
|
]);
|
|
|
|
// After an LNURL pay decode, default the amount input to the payment location's
|
|
// local fiat currency for phone-number payments, and seed any LNURL-advertised
|
|
// rate so a manual switch to that currency uses the provided rate. Runs once per
|
|
// decoded address and never overrides a manual currency selection.
|
|
useEffect(() => {
|
|
const country = paymentInfo?.phonePaymentCountry;
|
|
const descriptor = paymentInfo?.lnurlCurrency || null;
|
|
|
|
// Nothing to do for non-phone pays that don't advertise a currency.
|
|
if (paymentDisplayCurrency && paymentDisplayFiatStats) return;
|
|
if (!country && !descriptor) return;
|
|
if (hasUserSelectedDisplayCurrency) return;
|
|
|
|
const address = paymentInfo?.data?.address;
|
|
if (appliedLocalCurrencyRef.current === address) return;
|
|
|
|
const code = (
|
|
descriptor?.code ||
|
|
(country ? PROVIDER_COUNTRY_CURRENCY[country] : '') ||
|
|
''
|
|
).toUpperCase();
|
|
if (!code || !fiatCurrencies.some(c => c.id === code)) return;
|
|
|
|
appliedLocalCurrencyRef.current = address;
|
|
|
|
const rate = lnurlCurrencyToRate(descriptor);
|
|
if (rate) {
|
|
// Phone pay: seed the rate and switch to it. Non-phone: seed only, so the
|
|
// provided rate is used if the user manually switches to this currency.
|
|
injectRate(code, rate, { setAsDisplay: !!country });
|
|
} else if (country) {
|
|
// Phone pay with no usable LNURL rate: fall back to our internal rate. If
|
|
// that fails, the existing BTC/device-currency default stays.
|
|
loadAndSetCurrency(code);
|
|
}
|
|
}, [
|
|
paymentInfo,
|
|
hasUserSelectedDisplayCurrency,
|
|
injectRate,
|
|
loadAndSetCurrency,
|
|
]);
|
|
|
|
// Fast pay auto-trigger
|
|
useEffect(() => {
|
|
if (
|
|
!isUsingFastPay ||
|
|
hasTriggeredFastPay.current ||
|
|
isSendingPayment.current
|
|
)
|
|
return;
|
|
|
|
setShowProgressAnimation(true);
|
|
|
|
if (progressAnimationRef.current) {
|
|
requestAnimationFrame(() => {
|
|
progressAnimationRef.current.startAtBeginning();
|
|
});
|
|
}
|
|
|
|
const fastPayTrigger = setTimeout(() => {
|
|
hasTriggeredFastPay.current = true;
|
|
if (progressAnimationRef.current) {
|
|
progressAnimationRef.current.startProgress();
|
|
}
|
|
sendPayment();
|
|
}, 250);
|
|
|
|
return () => {
|
|
clearTimeout(fastPayTrigger);
|
|
};
|
|
}, [isUsingFastPay]);
|
|
|
|
const publishMessageFuncForContact = useCallback(
|
|
txid => {
|
|
const UUID = customUUID();
|
|
const sendObject = {
|
|
amountMsat: convertedSendAmountRef.current * 1000,
|
|
uuid: UUID,
|
|
wasSeen: null,
|
|
didSend: null,
|
|
isRedeemed: null,
|
|
description: combinedPaymentDescription || '',
|
|
isRequest: false,
|
|
paymentDenomination:
|
|
paymentInfo.data.expectedReceive === 'tokens' ? 'USD' : 'BTC',
|
|
amountDollars:
|
|
paymentInfo.data.expectedReceive === 'tokens'
|
|
? satsToDollars(
|
|
convertedSendAmount,
|
|
poolInfoRef.currentPriceAInB,
|
|
).toFixed(2)
|
|
: null,
|
|
...(globalContactsInformation.myProfile?.uniqueName
|
|
? {
|
|
senderProfileSnapshot: {
|
|
uniqueName: globalContactsInformation.myProfile.uniqueName,
|
|
},
|
|
}
|
|
: {}),
|
|
};
|
|
publishMessage({
|
|
toPubKey: selectedContact.uuid,
|
|
fromPubKey: globalContactsInformation.myProfile.uuid,
|
|
data: {
|
|
...sendObject,
|
|
txid,
|
|
name:
|
|
globalContactsInformation.myProfile?.name ||
|
|
globalContactsInformation.myProfile?.uniqueName,
|
|
},
|
|
globalContactsInformation,
|
|
selectedContact,
|
|
isLNURLPayment: selectedContact?.isLNURL,
|
|
privateKey: contactsPrivateKey,
|
|
retrivedContact,
|
|
currentTime: Date.now(),
|
|
masterInfoObject,
|
|
});
|
|
},
|
|
[
|
|
selectedContact,
|
|
retrivedContact,
|
|
globalContactsInformation,
|
|
contactsPrivateKey,
|
|
masterInfoObject,
|
|
combinedPaymentDescription,
|
|
paymentInfo,
|
|
],
|
|
);
|
|
const effectivePublishMessageFunc =
|
|
paymentInfo?.publishMessageFunc ||
|
|
publishMessageFunc ||
|
|
(selectedContact ? publishMessageFuncForContact : null);
|
|
|
|
const sendPayment = useCallback(async () => {
|
|
if (!paymentValidation.isValid) {
|
|
const error = paymentValidation.getErrorMessage(
|
|
paymentValidation.primaryError,
|
|
);
|
|
navigate.navigate('ErrorScreen', { errorMessage: error });
|
|
return;
|
|
}
|
|
|
|
if (isSendingPayment.current) return;
|
|
|
|
isSendingPayment.current = true;
|
|
setShowProgressAnimation(true);
|
|
|
|
try {
|
|
const formattedSparkPaymentInfo = formatSparkPaymentAddress(
|
|
paymentInfo,
|
|
selectedLRC20Asset?.toLowerCase() !== 'bitcoin',
|
|
);
|
|
|
|
const memo =
|
|
paymentInfo.type === InputTypes.BOLT11
|
|
? enteredPaymentInfo?.description || combinedPaymentDescription
|
|
: combinedPaymentDescription || enteredPaymentInfo?.description;
|
|
|
|
const paymentObject = {
|
|
getFee: false,
|
|
...formattedSparkPaymentInfo,
|
|
isUsingLRC20,
|
|
amountSats: isUsingLRC20
|
|
? paymentInfo?.sendAmount * 10 ** tokenDecimals
|
|
: convertedSendAmount,
|
|
masterInfoObject,
|
|
fee: effectivePaymentFee,
|
|
memo,
|
|
userBalance: bitcoinBalance,
|
|
sparkInformation: sparkInfoRef.current,
|
|
feeQuote: paymentInfo.feeQuote,
|
|
swapPaymentQuote: paymentInfo.swapPaymentQuote,
|
|
usingZeroAmountInvoice: paymentInfo.usingZeroAmountInvoice,
|
|
seletctedToken: selectedLRC20Asset,
|
|
mnemonic: currentWalletMnemoinc,
|
|
sendWebViewRequest,
|
|
contactInfo,
|
|
fromMainSendScreen: true,
|
|
usablePaymentMethod: resolvedPaymentMethod,
|
|
paymentInfo,
|
|
fiatValueConvertedSendAmount,
|
|
poolInfoRef,
|
|
extraDetails: paymentInfo?.isUsingBranta
|
|
? {
|
|
brantaMerchantName: paymentInfo.brantaMerchantName,
|
|
brantaMerchantLogo: paymentInfo.brantaMerchantLogo,
|
|
}
|
|
: {},
|
|
};
|
|
|
|
const paymentResponse = await sparkPaymenWrapper(paymentObject);
|
|
|
|
// Handle deferred save if identityPubKey wasn't available during payment
|
|
if (paymentResponse.shouldSave) {
|
|
let retries = 0;
|
|
const maxRetries = 20; // 10 seconds max wait
|
|
|
|
while (!sparkInfoRef.current.identityPubKey && retries < maxRetries) {
|
|
console.log('WATIINT FOR IDENTITY PUBKEY');
|
|
await new Promise(res => setTimeout(res, 500));
|
|
retries++;
|
|
}
|
|
|
|
if (sparkInfoRef.current.identityPubKey) {
|
|
const tx = {
|
|
...paymentResponse.response,
|
|
accountId: sparkInfoRef.current.identityPubKey,
|
|
};
|
|
await bulkUpdateSparkTransactions([tx], 'paymentWrapperTx', 0);
|
|
} else {
|
|
console.error('Failed to get identityPubKey after waiting');
|
|
}
|
|
|
|
isSendingPayingEventEmiiter.emit(SENDING_PAYMENT_EVENT_NAME, false);
|
|
}
|
|
|
|
if (progressAnimationRef.current) {
|
|
progressAnimationRef.current.completeProgress();
|
|
await new Promise(res => setTimeout(res, 600));
|
|
}
|
|
|
|
if (paymentResponse.didWork) {
|
|
if (
|
|
(fromPage === 'contacts' && paymentResponse.response?.id) ||
|
|
fromPage === 'paylink'
|
|
) {
|
|
effectivePublishMessageFunc(paymentResponse.response.id);
|
|
}
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
navigate.reset({
|
|
index: 0,
|
|
routes: [
|
|
{
|
|
name: 'HomeAdmin',
|
|
params: {
|
|
screen: 'Home',
|
|
},
|
|
},
|
|
{
|
|
name: 'ConfirmTxPage',
|
|
params: {
|
|
transaction: paymentResponse.response,
|
|
lnurlAddress:
|
|
paymentInfo?.type === InputTypes.LNURL_PAY
|
|
? normalizeLNURLAddress(paymentInfo?.data?.address)
|
|
: undefined,
|
|
blitzContactInfo: selectedContactInfo,
|
|
paymentDisplay: primaryDisplayRef.current,
|
|
displayAmount,
|
|
displayTokenTicker,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
requestAnimationFrame(() => {
|
|
requestAnimationFrame(() => {
|
|
navigate.reset({
|
|
index: 0,
|
|
routes: [
|
|
{
|
|
name: 'HomeAdmin',
|
|
params: {
|
|
screen: 'Home',
|
|
},
|
|
},
|
|
{
|
|
name: 'ConfirmTxPage',
|
|
params: {
|
|
transaction: paymentResponse.response,
|
|
error: paymentResponse.error,
|
|
lnurlAddress:
|
|
paymentInfo?.type === InputTypes.LNURL_PAY
|
|
? normalizeLNURLAddress(paymentInfo?.data?.address)
|
|
: undefined,
|
|
blitzContactInfo: selectedContactInfo,
|
|
paymentDisplay: primaryDisplayRef.current,
|
|
displayAmount,
|
|
displayTokenTicker,
|
|
},
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in sendPayment:', error);
|
|
// Reset state on error
|
|
isSendingPayment.current = false;
|
|
setShowProgressAnimation(false);
|
|
// Optionally show error to user
|
|
errorMessageNavigation(error.message);
|
|
}
|
|
}, [
|
|
canSendPayment,
|
|
paymentInfo,
|
|
selectedLRC20Asset,
|
|
enteredPaymentInfo,
|
|
combinedPaymentDescription,
|
|
isUsingLRC20,
|
|
tokenDecimals,
|
|
convertedSendAmount,
|
|
masterInfoObject,
|
|
effectivePaymentFee,
|
|
bitcoinBalance,
|
|
sparkInformation,
|
|
currentWalletMnemoinc,
|
|
sendWebViewRequest,
|
|
contactInfo,
|
|
fromPage,
|
|
effectivePublishMessageFunc,
|
|
navigate,
|
|
errorMessageNavigation,
|
|
resolvedPaymentMethod,
|
|
fiatValueConvertedSendAmount,
|
|
paymentValidation,
|
|
displayAmount,
|
|
displayTokenTicker,
|
|
selectedContactInfo,
|
|
]);
|
|
|
|
const handleSelectPaymentMethod = useCallback(
|
|
showNextScreen => {
|
|
if (showNextScreen) {
|
|
if (!paymentValidation.isValid) {
|
|
const error = paymentValidation.getErrorMessage(
|
|
paymentValidation.primaryError,
|
|
);
|
|
navigate.navigate('ErrorScreen', { errorMessage: error });
|
|
return;
|
|
}
|
|
|
|
setDidSelectPaymentMethod(true);
|
|
} else {
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'SelectPaymentMethod',
|
|
selectedPaymentMethod: resolvedPaymentMethod,
|
|
});
|
|
}
|
|
},
|
|
[navigate, paymentValidation, resolvedPaymentMethod],
|
|
);
|
|
const handleSelectTokenPress = useCallback(() => {
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'SelectLRC20Token',
|
|
});
|
|
}, [navigate]);
|
|
|
|
const handleEmoji = newDescription => {
|
|
setPaymentDescription(newDescription);
|
|
};
|
|
|
|
const openCurrencyPicker = () => {
|
|
if (!isAmountFocused) return;
|
|
if (isUsingLRC20) return;
|
|
if (isSendingPayment.current) return;
|
|
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'displayCurrencySelect',
|
|
sliderHight: 0.6,
|
|
currentCurrency: displayCurrency,
|
|
onSelectCurrency: async code => {
|
|
const response = await selectCurrency(code);
|
|
if (!response?.didWork) return response;
|
|
if (canEditAmount) {
|
|
setPaymentInfo(prev => ({
|
|
...prev,
|
|
sendAmount: '',
|
|
feeQuote: undefined,
|
|
swapPaymentQuote: undefined,
|
|
paymentFee: 0,
|
|
supportFee: 0,
|
|
}));
|
|
}
|
|
return response;
|
|
},
|
|
});
|
|
};
|
|
|
|
const memorizedKeyboardStyle = useMemo(() => {
|
|
return {
|
|
paddingBottom: !isAmountFocused ? 0 : bottomPadding,
|
|
};
|
|
}, [isAmountFocused, bottomPadding]);
|
|
|
|
const handleBrandaVerificationUrl = useCallback(() => {
|
|
navigate.navigate('CustomWebView', {
|
|
headerText: 'Branta',
|
|
webViewURL: paymentInfo?.verificationURL,
|
|
});
|
|
}, [paymentInfo?.verificationURL, navigate, t]);
|
|
|
|
if (
|
|
(!Object.keys(paymentInfo).length && !errorMessage) ||
|
|
!sparkInformation.didConnect
|
|
) {
|
|
return (
|
|
<GlobalThemeView useStandardWidth={true}>
|
|
<CustomSettingsTopBar />
|
|
<FullLoadingScreen text={loadingMessage} />
|
|
</GlobalThemeView>
|
|
);
|
|
}
|
|
|
|
if (errorMessage) {
|
|
return <ErrorWithPayment reason={errorMessage} />;
|
|
}
|
|
|
|
return (
|
|
<CustomKeyboardAvoidingView globalThemeViewStyles={memorizedKeyboardStyle}>
|
|
<View style={styles.replacementContainer}>
|
|
<CustomSettingsTopBar
|
|
label={t('constants.send')}
|
|
showLeftImage={isUsingBranta}
|
|
iconNew="BadgeCheck"
|
|
leftImageStyles={{ height: 25 }}
|
|
leftImageFunction={handleBrandaVerificationUrl}
|
|
rightContent={
|
|
!isUsingLRC20 && uiState !== 'CONFIRM_PAYMENT' ? (
|
|
<CurrencySwitchButton
|
|
displayCurrency={displayCurrency}
|
|
onPress={openCurrencyPicker}
|
|
disabled={isLoadingRate}
|
|
/>
|
|
) : null
|
|
}
|
|
/>
|
|
<ScrollView contentContainerStyle={styles.balanceScrollContainer}>
|
|
{/* Amount display */}
|
|
{uiState !== 'SWAP_RATES_CHANGED' && (
|
|
<View>
|
|
<FormattedBalanceInput
|
|
maxWidth={0.9}
|
|
amountValue={displayAmount}
|
|
inputDenomination={primaryDisplay.denomination}
|
|
forceCurrency={primaryDisplay.forceCurrency}
|
|
forceFiatStats={primaryDisplay.forceFiatStats}
|
|
activeOpacity={!sendingAmount ? 0.5 : 1}
|
|
customCurrencyCode={displayTokenTicker}
|
|
maxDecimals={isUsingLRC20 ? tokenDecimals : 2}
|
|
/>
|
|
{uiState === 'CONFIRM_PAYMENT' && !isUsingLRC20 && (
|
|
<SecondaryAmountDisplay
|
|
amountSats={convertedSendAmount}
|
|
secondaryDisplay={secondaryDisplay}
|
|
/>
|
|
)}
|
|
</View>
|
|
)}
|
|
|
|
{/* Send max button for edit mode */}
|
|
{/* {!useAltLayout && uiState === 'EDIT_AMOUNT' && showSendMax && (
|
|
<SendMaxComponent
|
|
fiatStats={fiatStats}
|
|
sparkInformation={sparkInformation}
|
|
paymentInfo={paymentInfo}
|
|
setPaymentInfo={setPaymentInfo}
|
|
masterInfoObject={masterInfoObject}
|
|
paymentFee={paymentFee}
|
|
paymentType={paymentInfo?.paymentNetwork}
|
|
// minMaxLiquidSwapAmounts={minMaxLiquidSwapAmounts}
|
|
selectedLRC20Asset={selectedLRC20Asset}
|
|
seletctedToken={seletctedToken}
|
|
useAltLayout={useAltLayout}
|
|
/>
|
|
)} */}
|
|
|
|
{/* Fee info for fixed amount */}
|
|
{uiState === 'CONFIRM_PAYMENT' && (
|
|
<SendTransactionFeeInfo
|
|
paymentFee={effectivePaymentFee}
|
|
isLightningPayment={isLightningPayment}
|
|
isLiquidPayment={isLiquidPayment}
|
|
isBitcoinPayment={isBitcoinPayment}
|
|
isSparkPayment={isSparkPayment}
|
|
isDecoding={isDecoding}
|
|
display={primaryDisplay}
|
|
/>
|
|
)}
|
|
|
|
{/* Invoice info */}
|
|
{uiState === 'CONFIRM_PAYMENT' && (
|
|
<InvoiceInfo
|
|
paymentInfo={paymentInfo}
|
|
contactInfo={selectedContactInfo}
|
|
fromPage={fromPage || (selectedContactInfo ? 'contacts' : '')}
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
isUsingBranta={isUsingBranta}
|
|
isDollarBalance={
|
|
resolvedPaymentMethod === 'USD' ||
|
|
selectedLRC20Asset === USDB_TOKEN_ID
|
|
}
|
|
isToken={isUsingLRC20 && selectedLRC20Asset !== USDB_TOKEN_ID}
|
|
/>
|
|
)}
|
|
{uiState === 'CHOOSE_METHOD' && (
|
|
<ChoosePaymentMethod
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
determinePaymentMethod={resolvedPaymentMethod}
|
|
handleSelectPaymentMethod={handleSelectPaymentMethod}
|
|
bitcoinBalance={bitcoinBalance}
|
|
dollarBalanceToken={dollarBalanceToken}
|
|
masterInfoObject={masterInfoObject}
|
|
fiatStats={fiatStats}
|
|
uiState={uiState}
|
|
t={t}
|
|
showPayWith={true}
|
|
/>
|
|
)}
|
|
|
|
{/* SWAP_RATES_CHANGED — rate drifted and broke swap viability */}
|
|
{uiState === 'SWAP_RATES_CHANGED' && <SwapRatesChangedState />}
|
|
</ScrollView>
|
|
|
|
{/* EDIT_AMOUNT State - Show input controls */}
|
|
{uiState === 'EDIT_AMOUNT' && (
|
|
<>
|
|
{/* Token selector for Spark LRC20 payments */}
|
|
{enabledLRC20 &&
|
|
paymentInfo.type === 'spark' &&
|
|
canEditAmount &&
|
|
useFullTokensDisplay && (
|
|
<ChooseLRC20TokenContainer
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
determinePaymentMethod={resolvedPaymentMethod}
|
|
handleSelectPaymentMethod={handleSelectTokenPress}
|
|
bitcoinBalance={bitcoinBalance}
|
|
dollarBalanceToken={dollarBalanceToken}
|
|
masterInfoObject={masterInfoObject}
|
|
fiatStats={fiatStats}
|
|
uiState={uiState}
|
|
seletctedToken={seletctedToken}
|
|
selectedLRC20Asset={selectedLRC20Asset}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{!(
|
|
enabledLRC20 &&
|
|
paymentInfo.type === 'spark' &&
|
|
canEditAmount &&
|
|
useFullTokensDisplay
|
|
) && (
|
|
<ChoosePaymentMethod
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
determinePaymentMethod={resolvedPaymentMethod}
|
|
handleSelectPaymentMethod={handleSelectPaymentMethod}
|
|
bitcoinBalance={bitcoinBalance}
|
|
dollarBalanceToken={dollarBalanceToken}
|
|
masterInfoObject={masterInfoObject}
|
|
fiatStats={fiatStats}
|
|
uiState={uiState}
|
|
t={t}
|
|
showBitcoinCardOnly={isBTCOnlyPayment}
|
|
showPayWith={true}
|
|
/>
|
|
)}
|
|
<CustomSearchInput
|
|
onFocusFunction={() => setIsAmountFocused(false)}
|
|
onBlurFunction={() => setIsAmountFocused(true)}
|
|
placeholderText={t('constants.paymentDescriptionPlaceholder')}
|
|
setInputText={setPaymentDescription}
|
|
inputText={combinedPaymentDescription}
|
|
textInputMultiline={true}
|
|
textAlignVertical={'baseline'}
|
|
maxLength={paymentInfo?.data?.commentAllowed || 150}
|
|
containerStyles={{
|
|
width: INSET_WINDOW_WIDTH,
|
|
marginTop: 10,
|
|
}}
|
|
/>
|
|
|
|
{isAmountFocused && (
|
|
<NumberInputSendPage
|
|
paymentInfo={paymentInfo}
|
|
setPaymentInfo={setPaymentInfo}
|
|
fiatStats={conversionFiatStats}
|
|
selectedLRC20Asset={selectedLRC20Asset}
|
|
seletctedToken={seletctedToken}
|
|
inputDenomination={primaryDisplay.denomination}
|
|
primaryDisplay={primaryDisplay}
|
|
/>
|
|
)}
|
|
|
|
{
|
|
// !useAltLayout &&
|
|
isAmountFocused && (
|
|
<AcceptButtonSendPage
|
|
decodeSendAddress={decodeSendAddress}
|
|
errorMessageNavigation={errorMessageNavigation}
|
|
btcAdress={btcAdress}
|
|
paymentInfo={paymentInfo}
|
|
convertedSendAmount={convertedSendAmount}
|
|
paymentDescription={combinedPaymentDescription}
|
|
setPaymentInfo={setPaymentInfo}
|
|
setLoadingMessage={setLoadingMessage}
|
|
fromPage={fromPage}
|
|
sparkInformation={sparkInformation}
|
|
seletctedToken={seletctedToken}
|
|
useAltLayout={false} //will be useAltLayout in future
|
|
sendWebViewRequest={sendWebViewRequest}
|
|
globalContactsInformation={globalContactsInformation}
|
|
canUseFastPay={canUseFastPay}
|
|
selectedPaymentMethod={resolvedPaymentMethod}
|
|
bitcoinBalance={bitcoinBalance}
|
|
dollarBalanceSat={dollarBalanceSat}
|
|
isDecoding={isDecoding || isEstimatingFee}
|
|
poolInfoRef={poolInfoRef}
|
|
swapLimits={swapLimits}
|
|
min_usd_swap_amount={min_usd_swap_amount}
|
|
inputDenomination={primaryDisplay.denomination}
|
|
paymentValidation={paymentValidation}
|
|
setDidSelectPaymentMethod={setDidSelectPaymentMethod}
|
|
conversionFiatStats={conversionFiatStats}
|
|
primaryDisplay={primaryDisplay}
|
|
enteredPaymentInfo={enteredPaymentInfo}
|
|
enteredDisplaySnapshot={buildAmountSnapshot(sendingAmount)}
|
|
/>
|
|
)
|
|
}
|
|
</>
|
|
)}
|
|
|
|
{/* SELECT_PAYMENT method state - show button to take user to select half modal */}
|
|
{uiState === 'CHOOSE_METHOD' && (
|
|
<CustomButton
|
|
buttonStyles={{
|
|
...CENTER,
|
|
width: INSET_WINDOW_WIDTH,
|
|
opacity: paymentValidation.isValid ? 1 : HIDDEN_OPACITY,
|
|
}}
|
|
actionFunction={() => handleSelectPaymentMethod(true)}
|
|
textContent={t('constants.review')}
|
|
/>
|
|
)}
|
|
|
|
{/* SWAP_RATES_CHANGED — primary CTA to re-enter amount */}
|
|
{uiState === 'SWAP_RATES_CHANGED' && (
|
|
<CustomButton
|
|
buttonStyles={{
|
|
width: INSET_WINDOW_WIDTH,
|
|
...CENTER,
|
|
}}
|
|
actionFunction={handleRateChangedReset}
|
|
textContent={t(
|
|
'wallet.sendPages.sendPaymentScreen.swapRatesChangedButton',
|
|
)}
|
|
/>
|
|
)}
|
|
|
|
{/* CONFIRM_PAYMENT State - Show swipe button or progress animation */}
|
|
{uiState === 'CONFIRM_PAYMENT' && (
|
|
<View style={styles.buttonContainer}>
|
|
{showProgressAnimation || isUsingFastPay ? (
|
|
<SliderProgressAnimation
|
|
ref={progressAnimationRef}
|
|
isVisible={showProgressAnimation || isUsingFastPay}
|
|
textColor={COLORS.darkModeText}
|
|
backgroundColor={
|
|
theme && darkModeType ? backgroundOffset : COLORS.primary
|
|
}
|
|
width={0.95}
|
|
/>
|
|
) : (
|
|
<SwipeButtonNew
|
|
onSwipeSuccess={sendPayment}
|
|
width={0.85}
|
|
resetAfterSuccessAnimDuration={true}
|
|
// shouldAnimateViewOnSuccess={true}
|
|
shouldResetAfterSuccess={!canSendPayment}
|
|
// shouldDisplaySuccessState={isSendingPayment}
|
|
containerStyles={{
|
|
opacity: canSendPayment ? 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 && uiState === 'EDIT_AMOUNT' && (
|
|
<EmojiQuickBar
|
|
description={combinedPaymentDescription}
|
|
onEmojiSelect={handleEmoji}
|
|
/>
|
|
)}
|
|
</CustomKeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
replacementContainer: {
|
|
flexGrow: 1,
|
|
width: WINDOWWIDTH,
|
|
...CENTER,
|
|
},
|
|
sectionTitle: {
|
|
fontSize: SIZES.medium,
|
|
opacity: HIDDEN_OPACITY,
|
|
textAlign: 'center',
|
|
marginBottom: CONTENT_KEYBOARD_OFFSET,
|
|
},
|
|
balanceScrollContainer: {
|
|
flexGrow: 1,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
paddingVertical: 10,
|
|
},
|
|
maxAndAcceptContainer: {
|
|
width: INSET_WINDOW_WIDTH,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
marginTop: 10,
|
|
...CENTER,
|
|
},
|
|
buttonContainer: {
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
},
|
|
selectTokenText: {
|
|
textDecorationLine: 'underline',
|
|
paddingVertical: 10,
|
|
textAlign: 'center',
|
|
},
|
|
selectMethodButton: {
|
|
width: INSET_WINDOW_WIDTH,
|
|
marginBottom: 20,
|
|
},
|
|
});
|