1306 lines
41 KiB
JavaScript
1306 lines
41 KiB
JavaScript
import { StyleSheet, ScrollView, TouchableOpacity, View } from 'react-native';
|
|
import {
|
|
CENTER,
|
|
CONTENT_KEYBOARD_OFFSET,
|
|
SATSPERBITCOIN,
|
|
USDB_TOKEN_ID,
|
|
} from '../../../../constants';
|
|
import { useNavigation } from '@react-navigation/native';
|
|
import { useGlobalContextProvider } from '../../../../../context-store/context';
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import { publishMessage } from '../../../../functions/messaging/publishMessage';
|
|
import {
|
|
CustomKeyboardAvoidingView,
|
|
ThemeText,
|
|
} from '../../../../functions/CustomElements';
|
|
import CustomNumberKeyboard from '../../../../functions/CustomElements/customNumberKeyboard';
|
|
import CustomButton from '../../../../functions/CustomElements/button';
|
|
import FormattedSatText from '../../../../functions/CustomElements/satTextDisplay';
|
|
import { useGlobalContactsInfo } from '../../../../../context-store/globalContacts';
|
|
import CustomSearchInput from '../../../../functions/CustomElements/searchInput';
|
|
import customUUID from '../../../../functions/customUUID';
|
|
import FormattedBalanceInput from '../../../../functions/CustomElements/formattedBalanceInput';
|
|
import { useNodeContext } from '../../../../../context-store/nodeContext';
|
|
import { useAppStatus } from '../../../../../context-store/appStatus';
|
|
import { useKeysContext } from '../../../../../context-store/keys';
|
|
import { crashlyticsLogReport } from '../../../../functions/crashlyticsLogs';
|
|
import convertTextInputValue from '../../../../functions/textInputConvertValue';
|
|
import { useServerTimeOnly } from '../../../../../context-store/serverTime';
|
|
import { useTranslation } from 'react-i18next';
|
|
import CustomSettingsTopBar from '../../../../functions/CustomElements/settingsTopBar';
|
|
import GetThemeColors from '../../../../hooks/themeColors';
|
|
import {
|
|
COLORS,
|
|
HIDDEN_OPACITY,
|
|
INSET_WINDOW_WIDTH,
|
|
SIZES,
|
|
WINDOWWIDTH,
|
|
} from '../../../../constants/theme';
|
|
import { useGlobalThemeContext } from '../../../../../context-store/theme';
|
|
import fetchBackend from '../../../../../db/handleBackend';
|
|
import { getDataFromCollection } from '../../../../../db';
|
|
import { Image } from 'expo-image';
|
|
import loadNewFiatData from '../../../../functions/saveAndUpdateFiatData';
|
|
import giftCardPurchaseAmountTracker from '../../../../functions/apps/giftCardPurchaseTracker';
|
|
import { useSparkWallet } from '../../../../../context-store/sparkContext';
|
|
import getReceiveAddressAndContactForContactsPayment from './internalComponents/getReceiveAddressAndKindForPayment';
|
|
import { useActiveCustodyAccount } from '../../../../../context-store/activeAccount';
|
|
import { sparkPaymenWrapper } from '../../../../functions/spark/payments';
|
|
import { useGlobalInsets } from '../../../../../context-store/insetsProvider';
|
|
import EmojiQuickBar from '../../../../functions/CustomElements/emojiBar';
|
|
import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext';
|
|
import { useFlashnet } from '../../../../../context-store/flashnetContext';
|
|
import {
|
|
dollarsToSats,
|
|
satsToDollars,
|
|
getLightningPaymentQuote,
|
|
USD_ASSET_ADDRESS,
|
|
} from '../../../../functions/spark/flashnet';
|
|
import ChoosePaymentMethod from '../sendBitcoin/components/choosePaymentMethodContainer';
|
|
import displayCorrectDenomination from '../../../../functions/displayCorrectDenomination';
|
|
import ThemeIcon from '../../../../functions/CustomElements/themeIcon';
|
|
import usePaymentValidation from '../sendBitcoin/functions/paymentValidation';
|
|
import { InputTypes, parseInput } from 'bitcoin-address-parser';
|
|
import usePaymentInputDisplay from '../../../../hooks/usePaymentInputDisplay';
|
|
import ContactProfileImage from './internalComponents/profileImage';
|
|
import { getLNAddressForLiquidPayment } from '../sendBitcoin/functions/payments';
|
|
import { useToast } from '../../../../../context-store/toastManager';
|
|
import useDebounce from '../../../../hooks/useDebounce';
|
|
import { useWebView } from '../../../../../context-store/webViewContext';
|
|
import formatTokensNumber from '../../../../functions/lrc20/formatTokensBalance';
|
|
|
|
export default function SendAndRequestPage(props) {
|
|
const {
|
|
selectedRequestMethod = 'BTC',
|
|
selectedPaymentMethod = 'BTC',
|
|
endReceiveType = 'BTC',
|
|
selectedContact,
|
|
paymentType, // 'send' or 'request'
|
|
imageData,
|
|
cardInfo: giftOption,
|
|
} = props.route.params || {};
|
|
|
|
const navigate = useNavigation();
|
|
const { dollarBalanceSat, dollarBalanceToken, bitcoinBalance } =
|
|
useUserBalanceContext();
|
|
const { poolInfoRef, swapLimits, swapUSDPriceDollars } = useFlashnet();
|
|
const { masterInfoObject } = useGlobalContextProvider();
|
|
const { sparkInformation } = useSparkWallet();
|
|
const { contactsPrivateKey, publicKey } = useKeysContext();
|
|
const { isConnectedToTheInternet } = useAppStatus();
|
|
const { fiatStats } = useNodeContext();
|
|
const { globalContactsInformation } = useGlobalContactsInfo();
|
|
const getServerTime = useServerTimeOnly();
|
|
const [amountValue, setAmountValue] = useState('');
|
|
const [isDescriptionFocused, setIsDescriptionFocused] = useState(false);
|
|
const [descriptionValue, setDescriptionValue] = useState(
|
|
giftOption?.memo || '',
|
|
);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const { bottomPadding } = useGlobalInsets();
|
|
const { theme, darkModeType } = useGlobalThemeContext();
|
|
const { backgroundOffset, textColor, backgroundColor } = GetThemeColors();
|
|
const { t } = useTranslation();
|
|
const { sendWebViewRequest } = useWebView();
|
|
const { currentWalletMnemoinc } = useActiveCustodyAccount();
|
|
const { showToast } = useToast();
|
|
const [lnFeeEstimate, setLnFeeEstimate] = useState(null);
|
|
const [swapQuote, setSwapQuote] = useState({});
|
|
const [lnInvoiceData, setLnInvoiceData] = useState(null);
|
|
const [prefetchedDoc, setPrefetchedDoc] = useState(null);
|
|
const lnurlParsedRef = useRef(null);
|
|
const quoteId = useRef(null);
|
|
const poolInfoRefSnapshotRef = useRef(poolInfoRef);
|
|
const prefSelectedPaymentInfo = useRef({
|
|
selectedPaymentMethod,
|
|
selectedRequestMethod,
|
|
});
|
|
const requiresContactDoc =
|
|
paymentType === 'send' &&
|
|
Boolean(selectedContact) &&
|
|
!selectedContact?.isLNURL;
|
|
const resolvedEndReceiveType = requiresContactDoc
|
|
? prefetchedDoc?.lnurlReceiveCurrency?.toLowerCase() === 'usd'
|
|
? 'USD'
|
|
: 'BTC'
|
|
: endReceiveType;
|
|
|
|
useEffect(() => {
|
|
if (typeof giftOption?.memo !== 'string') return;
|
|
setDescriptionValue(giftOption.memo);
|
|
}, [giftOption?.memo]);
|
|
|
|
useEffect(() => {
|
|
let isCurrent = true;
|
|
|
|
if (!requiresContactDoc) {
|
|
setPrefetchedDoc(null);
|
|
return () => {
|
|
isCurrent = false;
|
|
};
|
|
}
|
|
|
|
setPrefetchedDoc(null);
|
|
|
|
const MAX_ATTEMPTS = 5;
|
|
const BASE_DELAY_MS = 375; // ~375, 750, 1500, 3000, 6000 → ~11.6 s worst case, well under 30 s
|
|
|
|
const fetchWithBackoff = async (attempt = 0) => {
|
|
try {
|
|
const doc = await getDataFromCollection(
|
|
'blitzWalletUsers',
|
|
selectedContact.uuid,
|
|
);
|
|
if (isCurrent) setPrefetchedDoc(doc ?? null);
|
|
} catch {
|
|
if (!isCurrent) return;
|
|
|
|
if (attempt < MAX_ATTEMPTS - 1) {
|
|
const delay = BASE_DELAY_MS * 2 ** attempt;
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
if (isCurrent) fetchWithBackoff(attempt + 1);
|
|
} else {
|
|
setPrefetchedDoc(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
fetchWithBackoff();
|
|
|
|
return () => {
|
|
isCurrent = false;
|
|
};
|
|
}, [requiresContactDoc, selectedContact?.uuid]);
|
|
|
|
const paymentMode =
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod
|
|
: selectedRequestMethod;
|
|
|
|
// Determine if we're in USD mode
|
|
const isUSDMode =
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod === 'USD'
|
|
: selectedRequestMethod === 'USD';
|
|
|
|
const [userSetInputDenomination, setUserSetInputDenomination] =
|
|
useState(null);
|
|
|
|
const inputDenomination = userSetInputDenomination
|
|
? userSetInputDenomination
|
|
: paymentMode === 'USD'
|
|
? 'fiat'
|
|
: masterInfoObject.userBalanceDenomination !== 'fiat'
|
|
? 'sats'
|
|
: 'fiat';
|
|
|
|
const {
|
|
primaryDisplay,
|
|
secondaryDisplay,
|
|
conversionFiatStats,
|
|
convertDisplayToSats,
|
|
convertSatsToDisplay,
|
|
getNextDenomination,
|
|
convertForToggle,
|
|
} = usePaymentInputDisplay({
|
|
paymentMode,
|
|
inputDenomination,
|
|
fiatStats,
|
|
usdFiatStats: { coin: 'USD', value: swapUSDPriceDollars },
|
|
masterInfoObject,
|
|
});
|
|
|
|
const displayAmount =
|
|
paymentType !== 'Gift' ? amountValue : convertSatsToDisplay(amountValue);
|
|
|
|
// Calculate sat amount
|
|
const convertedSendAmount =
|
|
paymentType === 'Gift'
|
|
? Number(amountValue)
|
|
: convertDisplayToSats(amountValue);
|
|
|
|
const min_usd_swap_amount = useMemo(() => {
|
|
return Math.round(
|
|
dollarsToSats(
|
|
swapLimits.usd,
|
|
poolInfoRefSnapshotRef.current.currentPriceAInB,
|
|
),
|
|
);
|
|
}, [poolInfoRefSnapshotRef.current.currentPriceAInB, swapLimits]);
|
|
|
|
const estimateLNURLFee = useCallback(
|
|
async (amount, id) => {
|
|
if (quoteId.current !== id) return;
|
|
if (!selectedContact?.isLNURL || paymentType !== 'send' || !amount) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
const balance =
|
|
selectedPaymentMethod === '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) {
|
|
setIsLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (!lnurlParsedRef.current) {
|
|
lnurlParsedRef.current = await parseInput(
|
|
selectedContact.receiveAddress,
|
|
);
|
|
}
|
|
const lnurlInput = lnurlParsedRef.current;
|
|
|
|
const invoiceResponse = await getLNAddressForLiquidPayment(
|
|
lnurlInput,
|
|
amount,
|
|
'',
|
|
);
|
|
if (quoteId.current !== id) return;
|
|
if (!invoiceResponse.pr) throw new Error('No invoice received');
|
|
setLnInvoiceData(invoiceResponse);
|
|
|
|
if (selectedPaymentMethod === 'USD') {
|
|
const quote = await getLightningPaymentQuote(
|
|
currentWalletMnemoinc,
|
|
invoiceResponse.pr,
|
|
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,
|
|
});
|
|
}
|
|
|
|
setSwapQuote(quote.quote);
|
|
setLnFeeEstimate(fee);
|
|
} else {
|
|
const feeResult = await sparkPaymenWrapper({
|
|
getFee: true,
|
|
paymentType: 'lightning',
|
|
address: invoiceResponse.pr,
|
|
amountSats: amount,
|
|
masterInfoObject,
|
|
sparkInformation,
|
|
mnemonic: currentWalletMnemoinc,
|
|
sendWebViewRequest,
|
|
});
|
|
if (!feeResult.didWork) throw new Error('Fee estimation failed');
|
|
const fee = feeResult.fee;
|
|
if (quoteId.current !== id) return;
|
|
if (fee + amount > bitcoinBalance) {
|
|
showToast({
|
|
type: 'error',
|
|
title: t('errormessages.lightningAmountFeeWarning', {
|
|
amount: displayCorrectDenomination({
|
|
amount: fee,
|
|
masterInfoObject: {
|
|
...masterInfoObject,
|
|
userBalanceDenomination: 'sats',
|
|
},
|
|
fiatStats,
|
|
}),
|
|
}),
|
|
duration: 6000,
|
|
});
|
|
}
|
|
setLnFeeEstimate(fee);
|
|
}
|
|
} catch {
|
|
showToast({
|
|
type: 'error',
|
|
title: t('wallet.sendPages.sendPaymentScreen.feeEstimateError'),
|
|
});
|
|
} finally {
|
|
if (quoteId.current === id) {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
},
|
|
[
|
|
selectedContact?.isLNURL,
|
|
selectedContact?.receiveAddress,
|
|
paymentType,
|
|
selectedPaymentMethod,
|
|
currentWalletMnemoinc,
|
|
masterInfoObject,
|
|
sparkInformation,
|
|
sendWebViewRequest,
|
|
showToast,
|
|
t,
|
|
dollarBalanceToken,
|
|
dollarBalanceSat,
|
|
bitcoinBalance,
|
|
],
|
|
);
|
|
|
|
const debouncedEstimateLNURLFee = useDebounce(estimateLNURLFee, 600);
|
|
|
|
const paymentValidation = usePaymentValidation({
|
|
paymentInfo: {
|
|
sendAmount: convertedSendAmount,
|
|
paymentNetwork:
|
|
giftOption || selectedContact?.isLNURL ? 'lightning' : 'spark',
|
|
isLNURLPayment: selectedContact?.isLNURL,
|
|
data: {
|
|
expectedReceive: resolvedEndReceiveType === 'BTC' ? 'sats' : 'tokens',
|
|
expectedToken: resolvedEndReceiveType === 'BTC' ? null : USDB_TOKEN_ID,
|
|
},
|
|
decodedInput: {
|
|
tpye:
|
|
giftOption || selectedContact?.isLNURL ? InputTypes.BOLT11 : 'spark',
|
|
data: { amountMsat: convertedSendAmount * 1000 },
|
|
},
|
|
swapPaymentQuote: Object.keys(swapQuote).length
|
|
? swapQuote
|
|
: {
|
|
amountIn:
|
|
selectedPaymentMethod === 'BTC'
|
|
? convertedSendAmount
|
|
: inputDenomination === 'fiat'
|
|
? amountValue * Math.pow(10, 6)
|
|
: satsToDollars(
|
|
convertedSendAmount,
|
|
poolInfoRef.currentPriceAInB,
|
|
)?.toFixed(2) * Math.pow(10, 6),
|
|
},
|
|
},
|
|
convertedSendAmount,
|
|
paymentFee: lnFeeEstimate ?? 0,
|
|
determinePaymentMethod: selectedPaymentMethod,
|
|
selectedPaymentMethod,
|
|
|
|
bitcoinBalance: bitcoinBalance,
|
|
dollarBalanceSat: dollarBalanceSat,
|
|
dollarBalanceToken: dollarBalanceToken,
|
|
|
|
min_usd_swap_amount: min_usd_swap_amount,
|
|
swapLimits: swapLimits,
|
|
|
|
isUsingLRC20: false,
|
|
|
|
canEditAmount: true,
|
|
|
|
t,
|
|
|
|
masterInfoObject,
|
|
fiatStats,
|
|
inputDenomination: primaryDisplay.denomination,
|
|
primaryDisplay,
|
|
conversionFiatStats,
|
|
|
|
sparkInformation,
|
|
poolInfoRef,
|
|
});
|
|
|
|
const handleSelectPaymentMethod = useCallback(() => {
|
|
if (paymentType === 'send' || paymentType === 'Gift') {
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'SelectPaymentMethod',
|
|
selectedPaymentMethod: selectedPaymentMethod,
|
|
fromPage: 'SendAndRequestPage',
|
|
});
|
|
} else {
|
|
navigate.navigate('CustomHalfModal', {
|
|
wantedContent: 'SelectContactRequestCurrency',
|
|
selectedRecieveOption: selectedRequestMethod,
|
|
});
|
|
}
|
|
}, [navigate, selectedPaymentMethod, selectedRequestMethod]);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
prefSelectedPaymentInfo.current.selectedPaymentMethod !==
|
|
selectedPaymentMethod ||
|
|
prefSelectedPaymentInfo.current.selectedRequestMethod !==
|
|
selectedRequestMethod
|
|
) {
|
|
setAmountValue('');
|
|
setUserSetInputDenomination(null);
|
|
prefSelectedPaymentInfo.current = {
|
|
selectedPaymentMethod,
|
|
selectedRequestMethod,
|
|
};
|
|
}
|
|
}, [selectedPaymentMethod, selectedRequestMethod]);
|
|
|
|
useEffect(() => {
|
|
if (!giftOption) {
|
|
setAmountValue('');
|
|
return;
|
|
}
|
|
const totalSats = Math.round(
|
|
giftOption.selectedDenomination * giftOption.satsPerDollar,
|
|
);
|
|
const localfiatSatsPerDollar =
|
|
(primaryDisplay.forceFiatStats?.value || fiatStats.value) /
|
|
SATSPERBITCOIN;
|
|
setAmountValue(
|
|
String(
|
|
primaryDisplay.denomination !== 'fiat'
|
|
? totalSats
|
|
: Math.round(localfiatSatsPerDollar * totalSats),
|
|
),
|
|
);
|
|
}, [giftOption]);
|
|
|
|
useEffect(() => {
|
|
lnurlParsedRef.current = null;
|
|
setLnFeeEstimate(null);
|
|
}, [selectedContact?.uuid]);
|
|
|
|
useEffect(() => {
|
|
if (!selectedContact?.isLNURL || paymentType !== 'send') return;
|
|
setLnFeeEstimate(null);
|
|
setSwapQuote({});
|
|
if (convertedSendAmount > 0) {
|
|
const id = customUUID();
|
|
quoteId.current = id;
|
|
setIsLoading(true);
|
|
debouncedEstimateLNURLFee(convertedSendAmount, id);
|
|
}
|
|
}, [convertedSendAmount, selectedContact?.isLNURL, paymentType]);
|
|
|
|
const canProceed =
|
|
paymentType === 'request' ? !!amountValue : paymentValidation.canProceed;
|
|
const canReview =
|
|
canProceed && (!requiresContactDoc || Boolean(prefetchedDoc));
|
|
|
|
const handleDenominationToggle = () => {
|
|
if (isDescriptionFocused) return;
|
|
if (paymentType === 'Gift') {
|
|
const nextDenom = getNextDenomination();
|
|
setUserSetInputDenomination(nextDenom);
|
|
} else {
|
|
const nextDenom = getNextDenomination();
|
|
setUserSetInputDenomination(nextDenom);
|
|
setAmountValue(convertForToggle(amountValue, convertTextInputValue));
|
|
}
|
|
};
|
|
|
|
const handleSubmit = useCallback(async () => {
|
|
try {
|
|
if (!canProceed) {
|
|
const error = paymentValidation.getErrorMessage(
|
|
paymentValidation.primaryError,
|
|
);
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: error,
|
|
});
|
|
return;
|
|
}
|
|
if (!isConnectedToTheInternet) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: t('errormessages.nointernet'),
|
|
});
|
|
return;
|
|
}
|
|
if (requiresContactDoc && !prefetchedDoc) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: t('errormessages.contactNotLoaded'),
|
|
});
|
|
return;
|
|
}
|
|
crashlyticsLogReport('Submitting send and request payment');
|
|
setIsLoading(true);
|
|
|
|
const sendingAmountMsat = convertedSendAmount * 1000;
|
|
const contactMessage = descriptionValue;
|
|
const isLNURL = selectedContact.isLNURL;
|
|
const contactName = isLNURL
|
|
? selectedContact.name || selectedContact.receiveAddress.split('@')[0]
|
|
: selectedContact.name || selectedContact.uniqueName;
|
|
|
|
const myProfileMessage = !!descriptionValue
|
|
? descriptionValue
|
|
: t('contacts.sendAndRequestPage.profileMessage', {
|
|
name: contactName,
|
|
});
|
|
const payingContactMessage = !!descriptionValue
|
|
? descriptionValue
|
|
: {
|
|
usingTranslation: true,
|
|
type: 'paid',
|
|
name:
|
|
globalContactsInformation.myProfile.name ||
|
|
globalContactsInformation.myProfile.uniqueName,
|
|
};
|
|
|
|
const currentTime = getServerTime();
|
|
const UUID = customUUID();
|
|
let sendObject = {};
|
|
|
|
if (globalContactsInformation.myProfile.uniqueName) {
|
|
sendObject['senderProfileSnapshot'] = {
|
|
uniqueName: globalContactsInformation.myProfile.uniqueName,
|
|
};
|
|
}
|
|
|
|
if (giftOption) {
|
|
const retrivedContact = await getDataFromCollection(
|
|
'blitzWalletUsers',
|
|
selectedContact.uuid,
|
|
);
|
|
if (!retrivedContact) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: 'errormessages.fullDeeplinkError',
|
|
});
|
|
return;
|
|
}
|
|
if (!retrivedContact.enabledGiftCards) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: t(
|
|
'contacts.sendAndRequestPage.giftCardappVersionError',
|
|
),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const postData = {
|
|
type: 'buyGiftCard',
|
|
productId: giftOption.id, //string
|
|
cardValue: giftOption.selectedDenomination, //number
|
|
quantity: Number(1), //number
|
|
};
|
|
|
|
const response = await fetchBackend(
|
|
'theBitcoinCompanyV3',
|
|
postData,
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
);
|
|
|
|
if (response.result) {
|
|
const { amount, invoice, orderId, uuid } = response.result;
|
|
const fiatRates = await (fiatStats.coin?.toLowerCase() === 'usd'
|
|
? Promise.resolve({ didWork: true, fiatRateResponse: fiatStats })
|
|
: loadNewFiatData(
|
|
'usd',
|
|
contactsPrivateKey,
|
|
publicKey,
|
|
masterInfoObject,
|
|
));
|
|
const USDBTCValue = fiatRates.didWork
|
|
? fiatRates.fiatRateResponse
|
|
: { coin: 'USD', value: 100_000 };
|
|
|
|
const sendingAmountSat = amount;
|
|
const isOverDailyLimit = await giftCardPurchaseAmountTracker({
|
|
sendingAmountSat: sendingAmountSat,
|
|
USDBTCValue: USDBTCValue,
|
|
testOnly: true,
|
|
});
|
|
|
|
if (isOverDailyLimit.shouldBlock) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: isOverDailyLimit.reason,
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendObject['amountMsat'] = amount;
|
|
sendObject['description'] = descriptionValue || '';
|
|
sendObject['uuid'] = UUID;
|
|
sendObject['isRequest'] = false;
|
|
sendObject['isRedeemed'] = null;
|
|
sendObject['wasSeen'] = null;
|
|
sendObject['didSend'] = null;
|
|
sendObject['giftCardInfo'] = {
|
|
amount,
|
|
invoice,
|
|
orderId,
|
|
uuid,
|
|
logo: giftOption.logo,
|
|
name: giftOption.name,
|
|
};
|
|
|
|
navigate.navigate('ConfirmPaymentScreen', {
|
|
btcAdress: invoice,
|
|
comingFromAccept: true,
|
|
enteredPaymentInfo: {
|
|
fromContacts: true,
|
|
amount: amount,
|
|
description:
|
|
descriptionValue ||
|
|
t('contacts.sendAndRequestPage.giftCardDescription', {
|
|
name: contactName,
|
|
giftCardName: giftOption.name,
|
|
}),
|
|
},
|
|
contactInfo: {
|
|
imageData,
|
|
name: contactName,
|
|
uniqueName: selectedContact.uniqueName,
|
|
uuid: selectedContact.uuid,
|
|
},
|
|
preSelectedPaymentMethod: selectedPaymentMethod,
|
|
selectedPaymentMethod: selectedPaymentMethod,
|
|
fromPage: 'contacts',
|
|
publishMessageFunc: () => {
|
|
giftCardPurchaseAmountTracker({
|
|
sendingAmountSat: sendingAmountSat,
|
|
USDBTCValue: USDBTCValue,
|
|
});
|
|
publishMessage({
|
|
toPubKey: selectedContact.uuid,
|
|
fromPubKey: globalContactsInformation.myProfile.uuid,
|
|
data: sendObject,
|
|
globalContactsInformation,
|
|
selectedContact,
|
|
isLNURLPayment: false,
|
|
privateKey: contactsPrivateKey,
|
|
retrivedContact,
|
|
currentTime,
|
|
masterInfoObject,
|
|
});
|
|
},
|
|
});
|
|
} else {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: t('contacts.sendAndRequestPage.cardDetailsError'),
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const {
|
|
receiveAddress,
|
|
retrivedContact,
|
|
didWork,
|
|
error,
|
|
formattedPayingContactMessage,
|
|
} = await getReceiveAddressAndContactForContactsPayment({
|
|
sendingAmountSat: convertedSendAmount,
|
|
selectedContact,
|
|
myProfileMessage,
|
|
payingContactMessage,
|
|
onlyGetContact: paymentType !== 'send',
|
|
prefetchedDoc,
|
|
});
|
|
|
|
if (!didWork) {
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: error,
|
|
useTranslationString: true,
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendObject['amountMsat'] = sendingAmountMsat;
|
|
sendObject['uuid'] = UUID;
|
|
sendObject['wasSeen'] = null;
|
|
sendObject['didSend'] = null;
|
|
sendObject['isRedeemed'] = null;
|
|
|
|
if (paymentType === 'send') {
|
|
sendObject['description'] = contactMessage;
|
|
sendObject['isRequest'] = false;
|
|
sendObject['paymentDenomination'] = resolvedEndReceiveType;
|
|
sendObject['amountDollars'] =
|
|
resolvedEndReceiveType === 'USD'
|
|
? satsToDollars(
|
|
convertedSendAmount,
|
|
poolInfoRefSnapshotRef.current.currentPriceAInB,
|
|
).toFixed(2)
|
|
: null;
|
|
|
|
navigate.navigate('ConfirmPaymentScreen', {
|
|
btcAdress: receiveAddress,
|
|
comingFromAccept: true,
|
|
enteredPaymentInfo: {
|
|
fromContacts: true,
|
|
amount: convertedSendAmount,
|
|
description: myProfileMessage,
|
|
endReceiveType: resolvedEndReceiveType,
|
|
lnFeeEstimate: selectedContact?.isLNURL ? lnFeeEstimate : null,
|
|
swapQuote: Object.keys(swapQuote).length ? swapQuote : null,
|
|
lnInvoiceData: selectedContact?.isLNURL ? lnInvoiceData : null,
|
|
},
|
|
contactInfo: {
|
|
imageData,
|
|
name: contactName,
|
|
isLNURLPayment: selectedContact?.isLNURL,
|
|
payingContactMessage: formattedPayingContactMessage, //handles remote tx description
|
|
uniqueName: retrivedContact?.contacts?.myProfile?.uniqueName,
|
|
uuid: selectedContact.uuid,
|
|
},
|
|
preSelectedPaymentMethod: selectedPaymentMethod,
|
|
selectedPaymentMethod: selectedPaymentMethod,
|
|
fromPage: 'contacts',
|
|
publishMessageFunc: txid =>
|
|
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,
|
|
masterInfoObject,
|
|
}),
|
|
});
|
|
} else {
|
|
sendObject['amountDollars'] =
|
|
selectedRequestMethod === 'USD'
|
|
? satsToDollars(
|
|
convertedSendAmount,
|
|
poolInfoRefSnapshotRef.current.currentPriceAInB,
|
|
).toFixed(2)
|
|
: null;
|
|
sendObject['description'] = descriptionValue;
|
|
sendObject['isRequest'] = true;
|
|
sendObject['paymentDenomination'] = selectedRequestMethod;
|
|
|
|
await publishMessage({
|
|
toPubKey: selectedContact.uuid,
|
|
fromPubKey: globalContactsInformation.myProfile.uuid,
|
|
data: sendObject,
|
|
globalContactsInformation,
|
|
selectedContact,
|
|
isLNURLPayment: selectedContact?.isLNURL,
|
|
privateKey: contactsPrivateKey,
|
|
retrivedContact,
|
|
currentTime,
|
|
masterInfoObject,
|
|
});
|
|
|
|
navigate.goBack();
|
|
}
|
|
} catch (err) {
|
|
console.log(err, 'publishing message error');
|
|
navigate.navigate('ErrorScreen', {
|
|
errorMessage: selectedContact.isLNURL
|
|
? t('errormessages.contactInvoiceGenerationError')
|
|
: t('errormessages.invoiceRetrivalError'),
|
|
});
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [
|
|
isConnectedToTheInternet,
|
|
convertedSendAmount,
|
|
canProceed,
|
|
selectedContact,
|
|
navigate,
|
|
contactsPrivateKey,
|
|
descriptionValue,
|
|
paymentType,
|
|
globalContactsInformation,
|
|
getServerTime,
|
|
giftOption,
|
|
masterInfoObject,
|
|
fiatStats,
|
|
imageData,
|
|
paymentValidation,
|
|
selectedPaymentMethod,
|
|
primaryDisplay,
|
|
isUSDMode,
|
|
inputDenomination,
|
|
resolvedEndReceiveType,
|
|
requiresContactDoc,
|
|
prefetchedDoc,
|
|
t,
|
|
]);
|
|
|
|
const handleEmoji = newDescription => {
|
|
setDescriptionValue(newDescription);
|
|
};
|
|
|
|
const memorizedKeyboardStyle = useMemo(() => {
|
|
return {
|
|
paddingBottom: !isDescriptionFocused ? bottomPadding : 0,
|
|
};
|
|
}, [isDescriptionFocused]);
|
|
|
|
return (
|
|
<CustomKeyboardAvoidingView globalThemeViewStyles={memorizedKeyboardStyle}>
|
|
<View
|
|
style={[
|
|
styles.replacementContainer,
|
|
isDescriptionFocused ? { flexShrink: 1 } : { flexGrow: 1 },
|
|
]}
|
|
>
|
|
<CustomSettingsTopBar
|
|
label={
|
|
paymentType === 'Gift'
|
|
? t('constants.gift')
|
|
: paymentType === 'send'
|
|
? t('constants.send')
|
|
: t('constants.request')
|
|
}
|
|
containerStyles={{ marginBottom: 0 }}
|
|
/>
|
|
|
|
<View style={styles.identityBadge}>
|
|
<TouchableOpacity
|
|
activeOpacity={selectedContact?.isLNURL ? 1 : 0.7}
|
|
style={styles.splitPayContainer}
|
|
onPress={() => {
|
|
if (selectedContact?.isLNURL) return;
|
|
navigate.navigate('AddFriendsToSplit', {
|
|
paymentType,
|
|
selectedContact,
|
|
paymentCurrency: isUSDMode ? 'USD' : 'BTC',
|
|
});
|
|
}}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.contactListLetterImage,
|
|
{ backgroundColor: backgroundOffset },
|
|
]}
|
|
>
|
|
<ContactProfileImage
|
|
updated={imageData?.updated}
|
|
uri={imageData?.localUri}
|
|
darkModeType={darkModeType}
|
|
theme={theme}
|
|
/>
|
|
</View>
|
|
|
|
{!selectedContact?.isLNURL && (
|
|
<View style={[styles.splitPayIcon]}>
|
|
<ThemeIcon size={25} iconName={'CirclePlus'} />
|
|
</View>
|
|
)}
|
|
</TouchableOpacity>
|
|
<View style={styles.identityTextContainer}>
|
|
<ThemeText
|
|
styles={styles.contactName}
|
|
content={
|
|
selectedContact?.name || selectedContact?.uniqueName || ''
|
|
}
|
|
/>
|
|
</View>
|
|
</View>
|
|
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
contentContainerStyle={[
|
|
styles.scrollViewContainer,
|
|
paymentType === 'Gift' && { justifyContent: 'flex-start' },
|
|
{
|
|
opacity:
|
|
!isDescriptionFocused || paymentType === 'Gift'
|
|
? 1
|
|
: HIDDEN_OPACITY,
|
|
},
|
|
]}
|
|
>
|
|
<TouchableOpacity
|
|
activeOpacity={1}
|
|
onPress={handleDenominationToggle}
|
|
>
|
|
<FormattedBalanceInput
|
|
maxWidth={0.9}
|
|
amountValue={displayAmount || 0}
|
|
inputDenomination={primaryDisplay.denomination}
|
|
forceCurrency={primaryDisplay.forceCurrency}
|
|
forceFiatStats={primaryDisplay.forceFiatStats}
|
|
/>
|
|
|
|
<FormattedSatText
|
|
containerStyles={{
|
|
...styles.convertedAmount,
|
|
opacity: !amountValue ? HIDDEN_OPACITY : 1,
|
|
}}
|
|
neverHideBalance={true}
|
|
globalBalanceDenomination={secondaryDisplay.denomination}
|
|
forceCurrency={secondaryDisplay.forceCurrency}
|
|
forceFiatStats={secondaryDisplay.forceFiatStats}
|
|
balance={convertedSendAmount}
|
|
/>
|
|
</TouchableOpacity>
|
|
|
|
{giftOption && (
|
|
<>
|
|
<View style={styles.giftAmountContainer}>
|
|
<TouchableOpacity
|
|
onPress={() =>
|
|
navigate.navigate('SelectGiftCardForContacts', {
|
|
selectedContact,
|
|
imageData,
|
|
})
|
|
}
|
|
style={[
|
|
styles.pill,
|
|
{
|
|
backgroundColor: theme
|
|
? backgroundOffset
|
|
: COLORS.darkModeText,
|
|
},
|
|
]}
|
|
>
|
|
<View
|
|
style={[
|
|
styles.logoContainer,
|
|
{
|
|
backgroundColor: theme
|
|
? COLORS.darkModeText
|
|
: backgroundColor,
|
|
},
|
|
]}
|
|
>
|
|
<Image
|
|
style={styles.cardLogo}
|
|
source={{ uri: giftOption.logo }}
|
|
contentFit="contain"
|
|
/>
|
|
</View>
|
|
<ThemeText
|
|
CustomNumberOfLines={1}
|
|
styles={styles.pillText}
|
|
content={t('contacts.sendAndRequestPage.giftCardText', {
|
|
giftName: giftOption.name,
|
|
})}
|
|
/>
|
|
|
|
<View style={[styles.editButton]}>
|
|
<ThemeIcon
|
|
colorOverride={textColor}
|
|
size={18}
|
|
iconName={'SquarePen'}
|
|
/>
|
|
</View>
|
|
</TouchableOpacity>
|
|
|
|
<View style={styles.memoSection}>
|
|
<ThemeText
|
|
styles={styles.memoLabel}
|
|
content={t('contacts.sendAndRequestPage.giftMessage')}
|
|
/>
|
|
<CustomSearchInput
|
|
textInputMultiline={true}
|
|
placeholderText={t(
|
|
'contacts.sendAndRequestPage.giftMessagePlaceholder',
|
|
)}
|
|
onFocusFunction={() => setIsDescriptionFocused(true)}
|
|
onBlurFunction={() => setIsDescriptionFocused(false)}
|
|
inputText={descriptionValue}
|
|
setInputText={setDescriptionValue}
|
|
maxLength={500}
|
|
textInputStyles={{ minHeight: 100 }}
|
|
textAlignVertical="top"
|
|
/>
|
|
</View>
|
|
|
|
{paymentType === 'Gift' && (
|
|
<View>
|
|
<ThemeText
|
|
styles={styles.memoLabel}
|
|
content={t(
|
|
'contacts.sendAndRequestPage.paymentSourceHeader',
|
|
)}
|
|
/>
|
|
|
|
<ChoosePaymentMethod
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
determinePaymentMethod={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod
|
|
: selectedRequestMethod
|
|
}
|
|
handleSelectPaymentMethod={handleSelectPaymentMethod}
|
|
bitcoinBalance={sparkInformation.balance}
|
|
dollarBalanceToken={dollarBalanceToken}
|
|
masterInfoObject={masterInfoObject}
|
|
fiatStats={fiatStats}
|
|
uiState={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? 'SELECT_INLINE'
|
|
: 'CONTACT_REQUEST'
|
|
}
|
|
t={t}
|
|
selectedMethod={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod
|
|
: selectedRequestMethod
|
|
}
|
|
containerStyles={{ width: '100%', marginTop: 0 }}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</>
|
|
)}
|
|
</ScrollView>
|
|
{paymentType !== 'Gift' && (
|
|
<View style={styles.inputAndGiftContainer}>
|
|
<ChoosePaymentMethod
|
|
theme={theme}
|
|
darkModeType={darkModeType}
|
|
determinePaymentMethod={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod
|
|
: selectedRequestMethod
|
|
}
|
|
handleSelectPaymentMethod={handleSelectPaymentMethod}
|
|
bitcoinBalance={sparkInformation.balance}
|
|
dollarBalanceToken={dollarBalanceToken}
|
|
masterInfoObject={masterInfoObject}
|
|
fiatStats={fiatStats}
|
|
uiState={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? 'SELECT_INLINE'
|
|
: 'CONTACT_REQUEST'
|
|
}
|
|
t={t}
|
|
selectedMethod={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? selectedPaymentMethod
|
|
: selectedRequestMethod
|
|
}
|
|
containerStyles={{ width: '100%', marginBottom: 8 }}
|
|
/>
|
|
|
|
<CustomSearchInput
|
|
onFocusFunction={() => {
|
|
setIsDescriptionFocused(true);
|
|
}}
|
|
onBlurFunction={() => {
|
|
setIsDescriptionFocused(false);
|
|
}}
|
|
placeholderText={t('constants.paymentDescriptionPlaceholder')}
|
|
editable={paymentType === 'send' ? true : !!convertedSendAmount}
|
|
containerStyles={styles.descriptionInput}
|
|
setInputText={setDescriptionValue}
|
|
inputText={descriptionValue}
|
|
textInputMultiline={true}
|
|
textAlignVertical={'center'}
|
|
maxLength={149}
|
|
/>
|
|
</View>
|
|
)}
|
|
|
|
{!isDescriptionFocused && (
|
|
<View>
|
|
{paymentType !== 'Gift' && (
|
|
<CustomNumberKeyboard
|
|
showDot={primaryDisplay.denomination === 'fiat'}
|
|
frompage="sendContactsPage"
|
|
setInputValue={setAmountValue}
|
|
usingForBalance={true}
|
|
fiatStats={conversionFiatStats}
|
|
/>
|
|
)}
|
|
<CustomButton
|
|
buttonStyles={{
|
|
...styles.button,
|
|
opacity: canReview ? 1 : HIDDEN_OPACITY,
|
|
}}
|
|
useLoading={isLoading}
|
|
actionFunction={handleSubmit}
|
|
textContent={
|
|
paymentType === 'send' || paymentType === 'Gift'
|
|
? t('constants.review')
|
|
: t('constants.request')
|
|
}
|
|
/>
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{isDescriptionFocused && (
|
|
<EmojiQuickBar
|
|
description={descriptionValue}
|
|
onEmojiSelect={handleEmoji}
|
|
/>
|
|
)}
|
|
</CustomKeyboardAvoidingView>
|
|
);
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
replacementContainer: {
|
|
flexGrow: 1,
|
|
width: WINDOWWIDTH,
|
|
...CENTER,
|
|
},
|
|
|
|
identityBadge: {
|
|
alignItems: 'center',
|
|
alignSelf: 'center',
|
|
marginTop: 26,
|
|
gap: 10,
|
|
marginBottom: CONTENT_KEYBOARD_OFFSET,
|
|
},
|
|
splitPayContainer: {
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
gap: 5,
|
|
paddingHorizontal: 5,
|
|
},
|
|
splitPayIcon: {
|
|
width: 25,
|
|
height: 25,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
position: 'absolute',
|
|
right: -25,
|
|
},
|
|
contactListLetterImage: {
|
|
height: 60,
|
|
width: 60,
|
|
borderRadius: 30,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
overflow: 'hidden',
|
|
},
|
|
|
|
identityTextContainer: {
|
|
flexDirection: 'column',
|
|
gap: 2,
|
|
},
|
|
|
|
contactName: {
|
|
fontSize: SIZES.medium,
|
|
includeFontPadding: false,
|
|
},
|
|
|
|
scrollViewContainer: {
|
|
paddingTop: 5,
|
|
paddingBottom: 20,
|
|
alignItems: 'center',
|
|
// justifyContent: 'center',
|
|
flexGrow: 1,
|
|
},
|
|
convertedAmount: {
|
|
marginBottom: 16,
|
|
},
|
|
inputAndGiftContainer: {
|
|
width: INSET_WINDOW_WIDTH,
|
|
alignSelf: 'center',
|
|
},
|
|
giftAmountContainer: {
|
|
width: INSET_WINDOW_WIDTH,
|
|
marginTop: 20,
|
|
gap: 20,
|
|
},
|
|
giftAmountText: {
|
|
textAlign: 'center',
|
|
},
|
|
giftContainer: {
|
|
marginTop: CONTENT_KEYBOARD_OFFSET,
|
|
paddingVertical: 10,
|
|
paddingHorizontal: 16,
|
|
borderRadius: 8,
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
alignSelf: 'flex-end',
|
|
},
|
|
giftText: {
|
|
marginRight: 8,
|
|
includeFontPadding: false,
|
|
},
|
|
button: {
|
|
width: 'auto',
|
|
...CENTER,
|
|
},
|
|
pill: {
|
|
width: '100%',
|
|
display: 'flex',
|
|
flexDirection: 'row',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
padding: 10,
|
|
borderRadius: 8,
|
|
gap: 16,
|
|
alignSelf: 'center',
|
|
position: 'relative',
|
|
},
|
|
logoContainer: {
|
|
width: 48,
|
|
aspectRatio: 1,
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
backgroundColor: COLORS.darkModeText,
|
|
borderRadius: 12,
|
|
padding: 6,
|
|
},
|
|
cardLogo: {
|
|
width: '100%',
|
|
height: '100%',
|
|
maxWidth: 80,
|
|
maxHeight: 80,
|
|
borderRadius: 8,
|
|
},
|
|
pillText: {
|
|
flexShrink: 1,
|
|
includeFontPadding: false,
|
|
fontSize: SIZES.medium,
|
|
marginRight: 20,
|
|
},
|
|
editButton: {
|
|
width: 32,
|
|
height: 32,
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
marginLeft: 'auto',
|
|
},
|
|
editIcon: {
|
|
width: 18,
|
|
height: 18,
|
|
},
|
|
memoSection: {},
|
|
memoLabel: {
|
|
marginBottom: 10,
|
|
opacity: 0.8,
|
|
fontSize: SIZES.smedium,
|
|
},
|
|
memoContainer: {
|
|
justifyContent: 'center',
|
|
},
|
|
memoText: {
|
|
lineHeight: 22,
|
|
letterSpacing: 0.3,
|
|
fontSize: SIZES.medium,
|
|
},
|
|
descriptionInput: {
|
|
marginTop: 8,
|
|
},
|
|
});
|