Simplify confirm split payment (#778)
* fix small style issues * fix touch gap * fixing podfile * creating standardized amountIn function * show tx ids not amounts * use standardized amount in * use standardized amount in * fixed no payment information crash * downgrade rn and react-native-quick-crypto * only show margin if two elements exist * adding backup amount * wait till hompage to call analytics data * fix restore bug * updating firebase * fix check logic * wrap to handle thrown execptions * use utils package directly * adding error handling * group notification payments insted of handling them sequentually * handle payment requset on confirm page * improving split payment ux * adding translations * adding firebase sdk warning
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
calculateFlashnetAmountIn,
|
||||
SEND_AMOUNT_INCREASE_BUFFER,
|
||||
} from '../app/functions/spark/swapAmountUtils';
|
||||
|
||||
const PRICE = 1_000_000; // 1 sat = $1.00 at this mock price (simplifies math)
|
||||
|
||||
describe('calculateFlashnetAmountIn', () => {
|
||||
describe('BTC→USD (isUsdAssetIn = false)', () => {
|
||||
it('applies default 1% buffer to sats', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 10_000,
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: 100_000,
|
||||
});
|
||||
expect(result).toBe(Math.round(10_000 * SEND_AMOUNT_INCREASE_BUFFER));
|
||||
});
|
||||
|
||||
it('caps to maxBalance when buffer exceeds it', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 99_999,
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: 100_000,
|
||||
});
|
||||
expect(result).toBe(100_000);
|
||||
});
|
||||
|
||||
it('accepts a custom bufferMultiplier', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 10_000,
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: 100_000,
|
||||
bufferMultiplier: 1.02,
|
||||
});
|
||||
expect(result).toBe(10_200);
|
||||
});
|
||||
|
||||
it('returns integer', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 9999,
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: 50_000,
|
||||
});
|
||||
expect(Number.isInteger(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('USD→BTC (isUsdAssetIn = true)', () => {
|
||||
// 1_000_000 microdollars = $1.00
|
||||
it('applies default 1% buffer and returns microdollars', () => {
|
||||
// $10.00 in microdollars; PRICE = 1_000_000 means 1 sat = $1.00
|
||||
// dollarBalanceSat = 1_000_000 → satsToDollars(1_000_000, 1_000_000) = $1_000_000
|
||||
const base = 10_000_000;
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: base,
|
||||
isUsdAssetIn: true,
|
||||
dollarBalanceSat: 1_000_000,
|
||||
currentPriceAInB: PRICE,
|
||||
});
|
||||
// buffer: 10.00 * 1.01 = 10.10 → 10_100_000 microdollars
|
||||
expect(result).toBe(10_100_000);
|
||||
});
|
||||
|
||||
it('caps to balance when buffer exceeds it (low balance scenario)', () => {
|
||||
// base = $10 but USD balance only covers $5
|
||||
// dollarBalanceSat = 5 sats, PRICE = 1_000_000 → satsToDollars(5, 1_000_000) = 5
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 10_000_000, // $10
|
||||
isUsdAssetIn: true,
|
||||
dollarBalanceSat: 5, // 5 sats → $5 at PRICE
|
||||
currentPriceAInB: PRICE,
|
||||
});
|
||||
// bufferedDollars = 10_000_000 * 1.01 / 1e6 = 10.10
|
||||
// balanceDollars = satsToDollars(5, 1_000_000) = 5 * 1_000_000 / 1_000_000 = 5
|
||||
// cappedDollars = min(10.10, 5) = 5.00
|
||||
expect(result).toBe(5_000_000);
|
||||
});
|
||||
|
||||
it('falls back to maxBalance/1e6 when dollarBalanceSat and currentPriceAInB are absent', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 5_000_000, // $5.00
|
||||
isUsdAssetIn: true,
|
||||
maxBalance: 10_000_000, // $10.00 in microdollars
|
||||
});
|
||||
// 5.00 * 1.01 = 5.05 → 5_050_000
|
||||
expect(result).toBe(5_050_000);
|
||||
});
|
||||
|
||||
it('returns integer', () => {
|
||||
const result = calculateFlashnetAmountIn({
|
||||
baseAmountIn: 3_333_333,
|
||||
isUsdAssetIn: true,
|
||||
maxBalance: 100_000_000,
|
||||
});
|
||||
expect(Number.isInteger(result)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -48,7 +48,7 @@ export default function AnalyticsBudgetPage() {
|
||||
const { masterInfoObject } = useGlobalContextProvider();
|
||||
const { fiatStats } = useNodeContext();
|
||||
const { bottomPadding } = useGlobalInsets();
|
||||
const { textColor, backgroundOffset } = GetThemeColors();
|
||||
const { textColor, backgroundOffset, backgroundColor } = GetThemeColors();
|
||||
const { theme, darkModeType } = useGlobalThemeContext();
|
||||
const { spentTotal, spentTxCount } = useAnalytics();
|
||||
const { t } = useTranslation();
|
||||
@@ -205,18 +205,13 @@ export default function AnalyticsBudgetPage() {
|
||||
<FormattedSatText
|
||||
balance={budgetAmount}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
styles={[styles.statsValue, { color: COLORS.primary }]}
|
||||
styles={styles.statsValue}
|
||||
neverHideBalance={true}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.divider,
|
||||
{ backgroundColor: textColor, opacity: 0.1 },
|
||||
]}
|
||||
/>
|
||||
<View style={[styles.divider, { backgroundColor }]} />
|
||||
|
||||
{/* Spent this month */}
|
||||
<View style={styles.statsRow}>
|
||||
@@ -238,17 +233,12 @@ export default function AnalyticsBudgetPage() {
|
||||
<FormattedSatText
|
||||
balance={spentTotal}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
styles={[styles.statsValue, { color: textColor }]}
|
||||
styles={styles.statsValue}
|
||||
neverHideBalance={true}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View
|
||||
style={[
|
||||
styles.divider,
|
||||
{ backgroundColor: textColor, opacity: 0.1 },
|
||||
]}
|
||||
/>
|
||||
<View style={[styles.divider, { backgroundColor }]} />
|
||||
|
||||
{/* Left to spend */}
|
||||
<View style={styles.statsRow}>
|
||||
@@ -265,7 +255,7 @@ export default function AnalyticsBudgetPage() {
|
||||
<FormattedSatText
|
||||
balance={leftToSpend}
|
||||
globalBalanceDenomination={userBalanceDenomination}
|
||||
styles={[styles.statsValue, { color: textColor }]}
|
||||
styles={styles.statsValue}
|
||||
neverHideBalance={true}
|
||||
/>
|
||||
</View>
|
||||
@@ -297,13 +287,17 @@ const styles = StyleSheet.create({
|
||||
statusText: {
|
||||
fontSize: SIZES.smedium,
|
||||
fontFamily: FONT.Title_Regular,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
statusDivider: {
|
||||
fontSize: SIZES.smedium,
|
||||
fontFamily: FONT.Title_Regular,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
statusPeriod: {
|
||||
fontSize: SIZES.smedium,
|
||||
fontFamily: FONT.Title_Regular,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
circleContainer: {
|
||||
width: CIRCLE_SIZE,
|
||||
@@ -329,10 +323,6 @@ const styles = StyleSheet.create({
|
||||
includeFontPadding: false,
|
||||
opacity: HIDDEN_OPACITY,
|
||||
},
|
||||
circleCenterAmount: {
|
||||
fontSize: SIZES.large,
|
||||
textAlign: 'center',
|
||||
},
|
||||
statsCard: {
|
||||
width: '100%',
|
||||
borderRadius: 16,
|
||||
@@ -370,11 +360,11 @@ const styles = StyleSheet.create({
|
||||
gap: 8,
|
||||
},
|
||||
statsValue: {
|
||||
fontSize: SIZES.smedium,
|
||||
textAlign: 'right',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
divider: {
|
||||
height: StyleSheet.hairlineWidth,
|
||||
height: 2,
|
||||
marginHorizontal: 16,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -34,8 +34,8 @@ export default function AnalyticsCreateBudgetPage() {
|
||||
|
||||
const formattedPresetAmount =
|
||||
userBalanceDenomination === 'fiat'
|
||||
? numberConverter(existingBudget?.amount, 'fiat', 2, fiatStats)
|
||||
: existingBudget?.amount;
|
||||
? numberConverter(existingBudget?.amount || 0, 'fiat', 2, fiatStats)
|
||||
: existingBudget?.amount || 0;
|
||||
|
||||
const [amountValue, setAmountValue] = useState(
|
||||
existingBudget?.amount ? String(formattedPresetAmount) : '',
|
||||
|
||||
@@ -185,7 +185,7 @@ export default function BalancePieChart() {
|
||||
<View
|
||||
style={[
|
||||
styles.legendRow,
|
||||
{
|
||||
visibleSegments.length === 2 && {
|
||||
[seg.key === 'btc' ? 'marginBottom' : 'marginTop']: 10,
|
||||
},
|
||||
]}
|
||||
@@ -258,20 +258,22 @@ const styles = StyleSheet.create({
|
||||
legendLabel: {
|
||||
fontSize: SIZES.smedium,
|
||||
opacity: 0.6,
|
||||
width: 60,
|
||||
includeFontPadding: false,
|
||||
},
|
||||
legendAmount: {
|
||||
flex: 1,
|
||||
fontSize: SIZES.medium,
|
||||
textAlign: 'right',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
legendPercent: {
|
||||
fontSize: SIZES.smedium,
|
||||
opacity: HIDDEN_OPACITY,
|
||||
textAlign: 'right',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
divider: {
|
||||
height: 1,
|
||||
height: 2,
|
||||
borderRadius: 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -33,7 +33,9 @@ import CustomSearchInput from '../../../../functions/CustomElements/searchInput'
|
||||
import FormattedSatText from '../../../../functions/CustomElements/satTextDisplay';
|
||||
import WordsQrToggle from '../../../../functions/CustomElements/wordsQrToggle';
|
||||
import { dollarsToSats } from '../../../../functions/spark/flashnet';
|
||||
import { validateSplitPayment } from '../../../../functions/payments/validateSplitPayment';
|
||||
import { useFlashnet } from '../../../../../context-store/flashnetContext';
|
||||
import { useUserBalanceContext } from '../../../../../context-store/userBalanceContext';
|
||||
import { keyboardNavigate } from '../../../../functions/customNavigation';
|
||||
import ContactProfileImage from './internalComponents/profileImage';
|
||||
import { useImageCache } from '../../../../../context-store/imageCache';
|
||||
@@ -49,7 +51,7 @@ export default function CreateSplitBill(props) {
|
||||
} = props.route.params || {};
|
||||
const { t } = useTranslation();
|
||||
const { theme, darkModeType } = useGlobalThemeContext();
|
||||
const { backgroundOffset, textInputBackground, textInputColor } =
|
||||
const { backgroundOffset, textInputBackground, textInputColor, textColor } =
|
||||
GetThemeColors();
|
||||
const { globalContactsInformation } = useGlobalContacts();
|
||||
const { masterInfoObject } = useGlobalContextProvider();
|
||||
@@ -57,7 +59,9 @@ export default function CreateSplitBill(props) {
|
||||
const { cache } = useImageCache();
|
||||
const { fiatStats } = useNodeContext();
|
||||
const getServerTime = useServerTimeOnly();
|
||||
const { poolInfoRef } = useFlashnet();
|
||||
const { poolInfoRef, poolInfo, swapLimits, swapUSDPriceDollars } =
|
||||
useFlashnet();
|
||||
const { bitcoinBalance, dollarBalanceSat } = useUserBalanceContext();
|
||||
|
||||
const [memo, setMemo] = useState('');
|
||||
const [totalSats, setTotalSats] = useState(0); //BTC
|
||||
@@ -150,9 +154,50 @@ export default function CreateSplitBill(props) {
|
||||
[totalNative, n],
|
||||
);
|
||||
|
||||
const price = poolInfo?.currentPriceAInB ?? 0;
|
||||
|
||||
const totalAmountSats = useMemo(() => {
|
||||
if (!isUSD) return totalNative;
|
||||
if (price <= 0) return 0;
|
||||
return Math.round(dollarsToSats(totalNative / 100, price));
|
||||
}, [isUSD, totalNative, price]);
|
||||
|
||||
const {
|
||||
canPayBTC,
|
||||
canPayUSD,
|
||||
errorMessage: balanceErrorMessage,
|
||||
} = useMemo(() => {
|
||||
if (paymentType !== 'send' || totalAmountSats <= 0) {
|
||||
return { canPayBTC: true, canPayUSD: true, errorMessage: null };
|
||||
}
|
||||
return validateSplitPayment({
|
||||
totalSats: totalAmountSats,
|
||||
paymentCurrency,
|
||||
bitcoinBalance,
|
||||
dollarBalanceSat,
|
||||
swapLimits,
|
||||
price,
|
||||
masterInfoObject,
|
||||
swapUSDPriceDollars,
|
||||
t,
|
||||
});
|
||||
}, [
|
||||
paymentType,
|
||||
totalAmountSats,
|
||||
paymentCurrency,
|
||||
bitcoinBalance,
|
||||
dollarBalanceSat,
|
||||
swapLimits,
|
||||
price,
|
||||
masterInfoObject,
|
||||
swapUSDPriceDollars,
|
||||
t,
|
||||
]);
|
||||
|
||||
const canConfirm = useMemo(() => {
|
||||
if (totalNative <= 0) return false;
|
||||
if (!memo.trim()) return false;
|
||||
if (paymentType === 'send' && !canPayBTC && !canPayUSD) return false;
|
||||
if (splitMode === 'even') return perPersonNative > 0;
|
||||
// custom: every contact must have a valid positive integer and sum must equal total
|
||||
const allSet = selectedContacts.every(c => {
|
||||
@@ -168,30 +213,49 @@ export default function CreateSplitBill(props) {
|
||||
selectedContacts,
|
||||
customAmounts,
|
||||
customTotal,
|
||||
paymentType,
|
||||
canPayBTC,
|
||||
canPayUSD,
|
||||
]);
|
||||
|
||||
const buildRecipients = useCallback(() => {
|
||||
return selectedContacts.map((contact, index) => {
|
||||
const isLast = index === selectedContacts.length - 1;
|
||||
|
||||
if (isUSD) {
|
||||
const amountCents =
|
||||
splitMode === 'even'
|
||||
? perPersonNative
|
||||
: typeof customAmounts[contact.uuid] === 'number'
|
||||
? customAmounts[contact.uuid]
|
||||
: parseInt(customAmounts[contact.uuid] || '0', 10);
|
||||
let amountCents;
|
||||
if (splitMode === 'even') {
|
||||
amountCents = isLast
|
||||
? totalNative - perPersonNative * (selectedContacts.length - 1)
|
||||
: perPersonNative;
|
||||
} else {
|
||||
amountCents = parseInt(customAmounts[contact.uuid] || '0', 10);
|
||||
}
|
||||
const amountSat = dollarsToSats(
|
||||
amountCents / 100,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
);
|
||||
return { contact, amountSats: amountSat, amountCents, currency: 'USD' };
|
||||
}
|
||||
const amountSats =
|
||||
splitMode === 'even'
|
||||
? perPersonNative
|
||||
: parseInt(customAmounts[contact.uuid] || '0', 10);
|
||||
|
||||
let amountSats;
|
||||
if (splitMode === 'even') {
|
||||
amountSats = isLast
|
||||
? totalNative - perPersonNative * (selectedContacts.length - 1)
|
||||
: perPersonNative;
|
||||
} else {
|
||||
amountSats = parseInt(customAmounts[contact.uuid] || '0', 10);
|
||||
}
|
||||
return { contact, amountSats, amountCents: null, currency: 'BTC' };
|
||||
});
|
||||
}, [selectedContacts, splitMode, perPersonNative, customAmounts, isUSD]);
|
||||
}, [
|
||||
selectedContacts,
|
||||
isUSD,
|
||||
splitMode,
|
||||
perPersonNative,
|
||||
totalNative,
|
||||
customAmounts,
|
||||
]);
|
||||
|
||||
const handleConfirm = useCallback(async () => {
|
||||
if (totalNative <= 0) {
|
||||
@@ -208,7 +272,7 @@ export default function CreateSplitBill(props) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (splitMode === 'custom' && !canConfirm) {
|
||||
if ((splitMode === 'custom' && !canConfirm) || perPersonNative <= 0) {
|
||||
navigate.navigate('ErrorScreen', {
|
||||
errorMessage: t('contacts.splitBill.errors.noAmount', {
|
||||
context: 'custom',
|
||||
@@ -216,7 +280,7 @@ export default function CreateSplitBill(props) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!canConfirm || isLoading) return;
|
||||
if (isLoading) return;
|
||||
|
||||
const recipients = buildRecipients();
|
||||
|
||||
@@ -257,6 +321,11 @@ export default function CreateSplitBill(props) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else {
|
||||
if (balanceErrorMessage) {
|
||||
navigate.navigate('ErrorScreen', { errorMessage: balanceErrorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
const docIds = recipients.map(r => r.contact.uuid);
|
||||
const users = await getDocsByIds('blitzWalletUsers', docIds);
|
||||
|
||||
@@ -294,7 +363,6 @@ export default function CreateSplitBill(props) {
|
||||
);
|
||||
}
|
||||
}, [
|
||||
canConfirm,
|
||||
isLoading,
|
||||
buildRecipients,
|
||||
paymentType,
|
||||
@@ -306,7 +374,11 @@ export default function CreateSplitBill(props) {
|
||||
navigate,
|
||||
t,
|
||||
totalSatsInt,
|
||||
totalCentsInt,
|
||||
splitMode,
|
||||
canConfirm,
|
||||
balanceErrorMessage,
|
||||
perPersonNative,
|
||||
]);
|
||||
|
||||
const contactElements = useMemo(() => {
|
||||
@@ -322,7 +394,12 @@ export default function CreateSplitBill(props) {
|
||||
|
||||
const amountChip = isUSD ? (
|
||||
<ThemeText
|
||||
styles={styles.amountChip}
|
||||
styles={[
|
||||
styles.amountChip,
|
||||
{
|
||||
color: splitMode === 'custom' ? textInputColor : textColor,
|
||||
},
|
||||
]}
|
||||
content={
|
||||
contactAmount > 0 ? `$${(contactAmount / 100).toFixed(2)}` : '$0.00'
|
||||
}
|
||||
@@ -330,7 +407,10 @@ export default function CreateSplitBill(props) {
|
||||
) : (
|
||||
<FormattedSatText
|
||||
autoAdjustFontSize
|
||||
styles={styles.amountChip}
|
||||
styles={{
|
||||
...styles.amountChip,
|
||||
color: splitMode === 'custom' ? textInputColor : textColor,
|
||||
}}
|
||||
balance={contactAmount}
|
||||
/>
|
||||
);
|
||||
@@ -341,6 +421,8 @@ export default function CreateSplitBill(props) {
|
||||
<ContactProfileImage
|
||||
uri={cache[contact.uuid]?.localUri}
|
||||
updated={cache[contact.uuid]?.updated}
|
||||
theme={theme}
|
||||
darkModeType={darkModeType}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
@@ -389,6 +471,8 @@ export default function CreateSplitBill(props) {
|
||||
isUSD,
|
||||
textInputBackground,
|
||||
t,
|
||||
textColor,
|
||||
textInputColor,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -934,14 +934,19 @@ const styles = StyleSheet.create({
|
||||
gap: 10,
|
||||
marginBottom: CONTENT_KEYBOARD_OFFSET,
|
||||
},
|
||||
splitPayContainer: { flexDirection: 'row', alignItems: 'center', gap: 5 },
|
||||
splitPayContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 5,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
splitPayIcon: {
|
||||
width: 25,
|
||||
height: 25,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'absolute',
|
||||
right: -30,
|
||||
right: -25,
|
||||
},
|
||||
contactListLetterImage: {
|
||||
height: 60,
|
||||
|
||||
@@ -57,6 +57,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import DropdownMenu from '../../../../functions/CustomElements/dropdownMenu';
|
||||
import {
|
||||
BTC_ASSET_ADDRESS,
|
||||
calculateFlashnetAmountIn,
|
||||
dollarsToSats,
|
||||
executeSwap,
|
||||
getUserSwapHistory,
|
||||
@@ -678,24 +679,25 @@ export default function CreateGiftDuration(props) {
|
||||
// and the amountOut guard below will catch it with a clean error.
|
||||
const totalAmountIn =
|
||||
determinePaymentMethod === 'BTC'
|
||||
? Math.min(
|
||||
Math.round(
|
||||
? calculateFlashnetAmountIn({
|
||||
// base = target output + explicit pool fee; buffer adds the integrator margin
|
||||
baseAmountIn:
|
||||
totalSatAmount +
|
||||
Math.round(
|
||||
dollarsToSats(
|
||||
Number(simulation.feePaidAssetIn) / Math.pow(10, 6),
|
||||
) +
|
||||
totalSatAmount * (INTEGRATOR_FEE + 0.005),
|
||||
),
|
||||
bitcoinBalance,
|
||||
)
|
||||
: Math.min(
|
||||
Math.round(
|
||||
totalFiatAmount +
|
||||
Number(simulation.feePaidAssetIn) +
|
||||
totalFiatAmount * 0.005,
|
||||
),
|
||||
dollarBalanceToken * Math.pow(10, 6),
|
||||
);
|
||||
),
|
||||
),
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: bitcoinBalance,
|
||||
})
|
||||
: calculateFlashnetAmountIn({
|
||||
// base = target output + explicit pool fee in microdollars
|
||||
baseAmountIn:
|
||||
totalFiatAmount + Number(simulation.feePaidAssetIn),
|
||||
isUsdAssetIn: true,
|
||||
maxBalance: dollarBalanceToken * Math.pow(10, 6),
|
||||
});
|
||||
|
||||
const executionResponse = await executeSwap(currentWalletMnemoinc, {
|
||||
poolId: poolInfoRef.lpPublicKey,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,9 @@
|
||||
import {getApp} from '@react-native-firebase/app';
|
||||
import { utils } from '@react-native-firebase/app';
|
||||
|
||||
export function checkGooglePlayServices() {
|
||||
try {
|
||||
const app = getApp();
|
||||
|
||||
console.log(app.utils());
|
||||
|
||||
const areGoogleServicesEnabled =
|
||||
app.utils().playServicesAvailability.isAvailable;
|
||||
|
||||
utils().playServicesAvailability.isAvailable;
|
||||
return areGoogleServicesEnabled;
|
||||
} catch (err) {
|
||||
console.log('Error getting google services information', err);
|
||||
|
||||
@@ -44,7 +44,11 @@ export async function publishMessage({
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishBulkMessages(messagePayloads) {
|
||||
export async function publishBulkMessages(
|
||||
messagePayloads,
|
||||
privateKey,
|
||||
globalContactsInformation,
|
||||
) {
|
||||
try {
|
||||
crashlyticsLogReport('Beginning to publish bulk contact messages');
|
||||
|
||||
@@ -61,16 +65,30 @@ export async function publishBulkMessages(messagePayloads) {
|
||||
if (!success) return false;
|
||||
|
||||
// Push notifications are best-effort after the atomic write
|
||||
for (const p of messagePayloads) {
|
||||
sendPushNotification({
|
||||
selectedContactUsername: p.selectedContact.uniqueName,
|
||||
myProfile: p.globalContactsInformation.myProfile,
|
||||
data: p.data,
|
||||
privateKey: p.privateKey,
|
||||
retrivedContact: p.retrivedContact,
|
||||
masterInfoObject: p.masterInfoObject,
|
||||
});
|
||||
}
|
||||
const messages = (
|
||||
await Promise.all(
|
||||
messagePayloads.map(async p => {
|
||||
const message = await sendPushNotification({
|
||||
selectedContactUsername: p.selectedContact.uniqueName,
|
||||
myProfile: p.globalContactsInformation.myProfile,
|
||||
data: p.data,
|
||||
privateKey: p.privateKey,
|
||||
retrivedContact: p.retrivedContact,
|
||||
masterInfoObject: p.masterInfoObject,
|
||||
returnOnly: true,
|
||||
});
|
||||
return message || [];
|
||||
}),
|
||||
)
|
||||
).flat();
|
||||
|
||||
// fire and forget
|
||||
await fetchBackend(
|
||||
`bulkPushNoticications`,
|
||||
{ pushNotifications: messages },
|
||||
privateKey,
|
||||
globalContactsInformation.myProfile.uuid,
|
||||
);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
@@ -86,6 +104,7 @@ export async function sendPushNotification({
|
||||
privateKey,
|
||||
retrivedContact,
|
||||
masterInfoObject,
|
||||
returnOnly = false,
|
||||
}) {
|
||||
try {
|
||||
crashlyticsLogReport('Sending push notification');
|
||||
@@ -213,6 +232,11 @@ export async function sendPushNotification({
|
||||
decryptPubKey: retrivedContact.uuid,
|
||||
};
|
||||
}
|
||||
requestData.useNewNotifications = useNewNotifications;
|
||||
|
||||
if (returnOnly) {
|
||||
return requestData;
|
||||
}
|
||||
|
||||
const response = await fetchBackend(
|
||||
`contactsPushNotificationV${useNewNotifications ? '4' : '3'}`,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import displayCorrectDenomination from '../displayCorrectDenomination';
|
||||
import { dollarsToSats, satsToDollars } from '../spark/flashnet';
|
||||
|
||||
/**
|
||||
* Determines whether a split payment can be funded from BTC balance, USD balance,
|
||||
* or neither. Used in both createSplitBill (to block navigation) and
|
||||
* confirmSplitPayment (to derive valid sending options).
|
||||
*
|
||||
* @param {object} params
|
||||
* @param {number} params.totalSats - Total payment amount in sats
|
||||
* @param {string} params.paymentCurrency - 'BTC' | 'USD' — what recipients receive
|
||||
* @param {number} params.bitcoinBalance - User's BTC balance in sats
|
||||
* @param {number} params.dollarBalanceSat - User's USD balance expressed in sats
|
||||
* @param {{usd: number, bitcoin: number}} params.swapLimits - Min swap amounts
|
||||
* @param {number} params.price - poolInfo.currentPriceAInB (sats per dollar)
|
||||
* @param {function} params.t - i18n translation function
|
||||
* @returns {{ canPayBTC: boolean, canPayUSD: boolean, errorMessage: string|null }}
|
||||
*/
|
||||
export function validateSplitPayment({
|
||||
totalSats,
|
||||
paymentCurrency,
|
||||
bitcoinBalance,
|
||||
dollarBalanceSat,
|
||||
swapLimits,
|
||||
price,
|
||||
masterInfoObject,
|
||||
swapUSDPriceDollars,
|
||||
t,
|
||||
}) {
|
||||
const isUSD = paymentCurrency === 'USD';
|
||||
const minUsdSwapSats =
|
||||
price > 0 ? Math.round(dollarsToSats(swapLimits.usd, price)) : Infinity;
|
||||
const minBtcSwapSats = swapLimits.bitcoin;
|
||||
|
||||
// Compare in integer USD cents to avoid fiat→sats round-trip rounding errors.
|
||||
// Both user amount and limits are expressed at the same display precision (2 dp).
|
||||
const totalCents =
|
||||
price > 0 ? Math.round(satsToDollars(totalSats, price) * 100) : 0;
|
||||
const minBtcSwapCents =
|
||||
price > 0
|
||||
? Math.round(satsToDollars(minBtcSwapSats, price) * 100)
|
||||
: Infinity;
|
||||
const minUsdSwapCents = Math.round(swapLimits.usd * 100);
|
||||
|
||||
const hasBtcForAmount = bitcoinBalance >= totalSats;
|
||||
const hasUsdForAmount = dollarBalanceSat >= totalSats;
|
||||
const aboveBtcSwapMin = totalCents >= minBtcSwapCents;
|
||||
const aboveUsdSwapMin = totalCents >= minUsdSwapCents;
|
||||
|
||||
let canPayBTC, canPayUSD;
|
||||
if (isUSD) {
|
||||
// Recipients receive USD tokens: direct USD or BTC→USD swap
|
||||
canPayUSD = hasUsdForAmount;
|
||||
canPayBTC = hasBtcForAmount && aboveBtcSwapMin;
|
||||
} else {
|
||||
// Recipients receive BTC: direct BTC or USD→BTC swap
|
||||
canPayBTC = hasBtcForAmount;
|
||||
canPayUSD = hasUsdForAmount && aboveUsdSwapMin;
|
||||
}
|
||||
|
||||
let errorMessage = null;
|
||||
if (!canPayBTC && !canPayUSD) {
|
||||
if (isUSD && hasBtcForAmount && !aboveBtcSwapMin) {
|
||||
errorMessage = t('wallet.sendPages.acceptButton.swapMinimumError', {
|
||||
currency1: t('constants.bitcoin_upper'),
|
||||
currency2: t('constants.dollars_upper'),
|
||||
amount: displayCorrectDenomination({
|
||||
amount: minBtcSwapSats,
|
||||
masterInfoObject: {
|
||||
...masterInfoObject,
|
||||
userBalanceDenomination: 'fiat',
|
||||
},
|
||||
fiatStats: {
|
||||
value: swapUSDPriceDollars,
|
||||
coin: 'USD',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else if (!isUSD && hasUsdForAmount && !aboveUsdSwapMin) {
|
||||
errorMessage = t('wallet.sendPages.acceptButton.swapMinimumError', {
|
||||
currency1: t('constants.dollars_upper'),
|
||||
currency2: t('constants.bitcoin_upper'),
|
||||
amount: displayCorrectDenomination({
|
||||
amount: minUsdSwapSats,
|
||||
masterInfoObject: {
|
||||
...masterInfoObject,
|
||||
userBalanceDenomination: 'sats',
|
||||
},
|
||||
fiatStats: {
|
||||
value: swapUSDPriceDollars,
|
||||
coin: 'USD',
|
||||
},
|
||||
}),
|
||||
});
|
||||
} else {
|
||||
errorMessage = t('wallet.sendPages.acceptButton.balanceError');
|
||||
}
|
||||
}
|
||||
|
||||
return { canPayBTC, canPayUSD, errorMessage };
|
||||
}
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
swapTokenToBitcoin,
|
||||
USD_ASSET_ADDRESS,
|
||||
} from './flashnet';
|
||||
import {
|
||||
publishMessage,
|
||||
publishBulkMessages,
|
||||
} from '../messaging/publishMessage';
|
||||
import { publishBulkMessages } from '../messaging/publishMessage';
|
||||
import { bulkUpdateSparkTransactions } from './transactions';
|
||||
import getReceiveAddressAndContactForContactsPayment from '../../components/admin/homeComponents/contacts/internalComponents/getReceiveAddressAndKindForPayment';
|
||||
import customUUID from '../customUUID';
|
||||
@@ -173,7 +170,13 @@ export async function bulkSparkPayment(
|
||||
|
||||
// Nothing to fulfill if every invoice failed to generate
|
||||
if (invoiceBatch.length === 0) {
|
||||
return { successful: [], failed, totalPaid: 0, groupId: null };
|
||||
return {
|
||||
successful: [],
|
||||
failed,
|
||||
totalPaid: 0,
|
||||
groupId: null,
|
||||
error: failed[0].error,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Phase 2: Single batch fulfill ─────────────────────────────────────────
|
||||
@@ -198,7 +201,13 @@ export async function bulkSparkPayment(
|
||||
}
|
||||
|
||||
if (!fulfillResult || !fulfillResult?.didWork)
|
||||
return { successful: [], failed, totalPaid: 0, groupId: null };
|
||||
return {
|
||||
successful: [],
|
||||
failed,
|
||||
totalPaid: 0,
|
||||
groupId: null,
|
||||
error: fulfillResult.error,
|
||||
};
|
||||
|
||||
// ── Phase 3: Match results back to recipients ──────────────────────────────
|
||||
// The SDK returns { invoice, transferResponse } / { invoice, error } in each
|
||||
@@ -262,17 +271,17 @@ export async function bulkSparkPayment(
|
||||
currentTime,
|
||||
} = notifyParams;
|
||||
|
||||
for (const {
|
||||
contact,
|
||||
amountSats,
|
||||
transferId,
|
||||
transfer,
|
||||
contactFull,
|
||||
currency,
|
||||
amountCents,
|
||||
} of successful) {
|
||||
try {
|
||||
await publishMessage({
|
||||
const pushNotifications = successful.map(
|
||||
({
|
||||
contact,
|
||||
amountSats,
|
||||
transferId,
|
||||
transfer,
|
||||
contactFull,
|
||||
currency,
|
||||
amountCents,
|
||||
}) => {
|
||||
return {
|
||||
toPubKey: contact.uuid,
|
||||
fromPubKey: globalContactsInformation.myProfile.uuid,
|
||||
data: {
|
||||
@@ -296,16 +305,15 @@ export async function bulkSparkPayment(
|
||||
retrivedContact: contactFull,
|
||||
currentTime,
|
||||
masterInfoObject,
|
||||
});
|
||||
} catch (notifyErr) {
|
||||
// Notification failure is non-fatal
|
||||
console.log(
|
||||
'bulkSparkPayment: notification failed for',
|
||||
contact.uniqueName,
|
||||
notifyErr,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await publishBulkMessages(
|
||||
pushNotifications,
|
||||
privateKey,
|
||||
globalContactsInformation,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Phase 5: Persist to SQLite ─────────────────────────────────────────────
|
||||
@@ -444,16 +452,16 @@ export async function bulkSparkPayment(
|
||||
currentTime,
|
||||
} = notifyParams;
|
||||
|
||||
for (const {
|
||||
contact,
|
||||
amountSats,
|
||||
amountCents,
|
||||
currency,
|
||||
txHash: entryTxHash,
|
||||
contactFull,
|
||||
} of successful) {
|
||||
try {
|
||||
await publishMessage({
|
||||
const pushNotifications = successful.map(
|
||||
({
|
||||
contact,
|
||||
amountSats,
|
||||
amountCents,
|
||||
currency,
|
||||
txHash: entryTxHash,
|
||||
contactFull,
|
||||
}) => {
|
||||
return {
|
||||
toPubKey: contact.uuid,
|
||||
fromPubKey: globalContactsInformation.myProfile.uuid,
|
||||
data: {
|
||||
@@ -463,7 +471,7 @@ export async function bulkSparkPayment(
|
||||
isRequest: false,
|
||||
didSend: true,
|
||||
wasSeen: null,
|
||||
paymentDenomination: 'USD',
|
||||
paymentDenomination: currency === 'USD' ? 'USD' : 'BTC',
|
||||
amountDollars:
|
||||
amountCents != null ? (amountCents / 100).toFixed(2) : null,
|
||||
txid: entryTxHash,
|
||||
@@ -475,15 +483,15 @@ export async function bulkSparkPayment(
|
||||
retrivedContact: contactFull,
|
||||
currentTime,
|
||||
masterInfoObject,
|
||||
});
|
||||
} catch (notifyErr) {
|
||||
console.log(
|
||||
'bulkSparkPayment: USD notification failed for',
|
||||
contact.uniqueName,
|
||||
notifyErr,
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
await publishBulkMessages(
|
||||
pushNotifications,
|
||||
privateKey,
|
||||
globalContactsInformation,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Phase 5: Persist to SQLite ─────────────────────────────────────────────
|
||||
@@ -627,7 +635,11 @@ export async function bulkPaymentRequest(recipients, memo, senderInfo) {
|
||||
return { successful: [], failed };
|
||||
}
|
||||
|
||||
const success = await publishBulkMessages(payloads);
|
||||
const success = await publishBulkMessages(
|
||||
payloads,
|
||||
privateKey,
|
||||
globalContactsInformation,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return {
|
||||
|
||||
@@ -39,23 +39,35 @@ import {
|
||||
import { decode } from 'bolt11';
|
||||
|
||||
// ============================================
|
||||
// CONSTANTS
|
||||
// CONSTANTS & PURE UTILITIES
|
||||
// ============================================
|
||||
import {
|
||||
BTC_ASSET_ADDRESS,
|
||||
USD_ASSET_ADDRESS,
|
||||
FLASHNET_POOL_IDENTITY_KEY,
|
||||
DEFAULT_SLIPPAGE_BPS,
|
||||
SEND_AMOUNT_INCREASE_BUFFER,
|
||||
DEFAULT_MAX_SLIPPAGE_BPS,
|
||||
INTEGRATOR_FEE,
|
||||
INTEGRATOR_FEE_BPS,
|
||||
satsToDollars,
|
||||
dollarsToSats,
|
||||
calculateFlashnetAmountIn,
|
||||
} from './swapAmountUtils';
|
||||
|
||||
// Standard Bitcoin pubkey for pools (constant across Flashnet)
|
||||
export const BTC_ASSET_ADDRESS =
|
||||
'020202020202020202020202020202020202020202020202020202020202020202';
|
||||
export const USD_ASSET_ADDRESS =
|
||||
'3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca';
|
||||
|
||||
export const FLASHNET_POOL_IDENTITY_KEY =
|
||||
'02894808873b896e21d29856a6d7bb346fb13c019739adb9bf0b6a8b7e28da53da';
|
||||
|
||||
// Default slippage tolerance
|
||||
export const DEFAULT_SLIPPAGE_BPS = 100; // 1%
|
||||
export const SEND_AMOUNT_INCREASE_BUFFER = 1.01; // 1%
|
||||
export const DEFAULT_MAX_SLIPPAGE_BPS = 300; // 3% for lightning payments
|
||||
export const INTEGRATOR_FEE = 0.005; // .5%
|
||||
export {
|
||||
BTC_ASSET_ADDRESS,
|
||||
USD_ASSET_ADDRESS,
|
||||
FLASHNET_POOL_IDENTITY_KEY,
|
||||
DEFAULT_SLIPPAGE_BPS,
|
||||
SEND_AMOUNT_INCREASE_BUFFER,
|
||||
DEFAULT_MAX_SLIPPAGE_BPS,
|
||||
INTEGRATOR_FEE,
|
||||
INTEGRATOR_FEE_BPS,
|
||||
satsToDollars,
|
||||
dollarsToSats,
|
||||
calculateFlashnetAmountIn,
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// HELPER FUNCTIONS
|
||||
@@ -364,7 +376,7 @@ export const simulateSwap = async (
|
||||
assetInAddress,
|
||||
assetOutAddress,
|
||||
amountIn,
|
||||
integratorFeeRateBps = 50,
|
||||
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
@@ -432,7 +444,7 @@ export const executeSwap = async (
|
||||
amountIn,
|
||||
minAmountOut, // Optional - will be calculated if not provided
|
||||
maxSlippageBps = DEFAULT_SLIPPAGE_BPS,
|
||||
integratorFeeRateBps = 50,
|
||||
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
@@ -705,7 +717,7 @@ export const getLightningPaymentQuote = async (
|
||||
mnemonic,
|
||||
invoice,
|
||||
tokenAddress,
|
||||
integratorFeeRateBps = 50,
|
||||
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
|
||||
maxSlippageBps = DEFAULT_MAX_SLIPPAGE_BPS,
|
||||
) => {
|
||||
try {
|
||||
@@ -780,7 +792,7 @@ export const payLightningWithToken = async (
|
||||
maxLightningFeeSats = null,
|
||||
rollbackOnFailure = true,
|
||||
useExistingBtcBalance = false,
|
||||
integratorFeeRateBps = 50,
|
||||
integratorFeeRateBps = INTEGRATOR_FEE_BPS,
|
||||
},
|
||||
) => {
|
||||
try {
|
||||
@@ -1386,64 +1398,6 @@ export const getCurrentPrice = async (mnemonic, poolId) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert sats to dollars
|
||||
* @param {string|number|bigint} sats - Amount of satoshis (100,000,000)
|
||||
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
|
||||
* @returns {number} Amount in dollars
|
||||
*/
|
||||
export function satsToDollars(sats, currentPriceAinB) {
|
||||
try {
|
||||
const DOLLAR_DECIMALS = 1_000_000;
|
||||
|
||||
const numSats = typeof sats === 'bigint' ? Number(sats) : Number(sats || 0);
|
||||
const numPrice =
|
||||
typeof currentPriceAinB === 'bigint'
|
||||
? Number(currentPriceAinB)
|
||||
: Number(currentPriceAinB || 0);
|
||||
|
||||
if (isNaN(numSats) || isNaN(numPrice) || numPrice === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (numSats * numPrice) / DOLLAR_DECIMALS;
|
||||
} catch (error) {
|
||||
console.error('Error in satsToDollars:', error, { sats, currentPriceAinB });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dollars to sats
|
||||
* @param {string|number|bigint} dollars - Amount of dollars (1,000,000)
|
||||
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
|
||||
* @returns {number} Amount in sats
|
||||
*/
|
||||
export function dollarsToSats(dollars, currentPriceAinB) {
|
||||
try {
|
||||
const DOLLAR_DECIMALS = 1_000_000;
|
||||
|
||||
const numDollars =
|
||||
typeof dollars === 'bigint' ? Number(dollars) : Number(dollars || 0);
|
||||
const numPrice =
|
||||
typeof currentPriceAinB === 'bigint'
|
||||
? Number(currentPriceAinB)
|
||||
: Number(currentPriceAinB || 0);
|
||||
|
||||
if (isNaN(numDollars) || isNaN(numPrice) || numPrice === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (numDollars * DOLLAR_DECIMALS) / numPrice;
|
||||
} catch (error) {
|
||||
console.error('Error in dollarsToSats:', error, {
|
||||
dollars,
|
||||
currentPriceAinB,
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert exchangeRate to fiat price
|
||||
* @param {string|number|bigint} currentPriceAinB - Price of Bitcoin in dollars
|
||||
|
||||
@@ -28,12 +28,12 @@ import {
|
||||
import sha256Hash from '../hash';
|
||||
import calculateProgressiveBracketFee from './calculateSupportFee';
|
||||
import {
|
||||
calculateFlashnetAmountIn,
|
||||
dollarsToSats,
|
||||
executeSwap,
|
||||
getUserSwapHistory,
|
||||
payLightningWithToken,
|
||||
satsToDollars,
|
||||
SEND_AMOUNT_INCREASE_BUFFER,
|
||||
USD_ASSET_ADDRESS,
|
||||
} from './flashnet';
|
||||
import {
|
||||
@@ -377,17 +377,12 @@ export const sparkPaymenWrapper = async ({
|
||||
let usedUSDB = false;
|
||||
if (needsSwap) {
|
||||
if (usablePaymentMethod === 'USD') {
|
||||
const amountInWithBuffer = Math.min(
|
||||
(swapPaymentQuote.amountIn * SEND_AMOUNT_INCREASE_BUFFER) /
|
||||
Math.pow(10, 6),
|
||||
satsToDollars(
|
||||
swapPaymentQuote.dollarBalanceSat,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
),
|
||||
);
|
||||
const formatted = Math.round(
|
||||
amountInWithBuffer.toFixed(2) * Math.pow(10, 6),
|
||||
);
|
||||
const formatted = calculateFlashnetAmountIn({
|
||||
baseAmountIn: swapPaymentQuote.amountIn,
|
||||
isUsdAssetIn: true,
|
||||
dollarBalanceSat: swapPaymentQuote.dollarBalanceSat,
|
||||
currentPriceAInB: poolInfoRef.currentPriceAInB,
|
||||
});
|
||||
executionResponse = await executeSwap(mnemonic, {
|
||||
poolId: swapPaymentQuote.poolId,
|
||||
assetInAddress: swapPaymentQuote.assetInAddress,
|
||||
@@ -396,11 +391,11 @@ export const sparkPaymenWrapper = async ({
|
||||
});
|
||||
usedUSDB = true;
|
||||
} else {
|
||||
const amountInWithBuffer = Math.min(
|
||||
swapPaymentQuote.amountIn * SEND_AMOUNT_INCREASE_BUFFER,
|
||||
swapPaymentQuote.bitcoinBalance,
|
||||
);
|
||||
const formatted = Math.round(amountInWithBuffer);
|
||||
const formatted = calculateFlashnetAmountIn({
|
||||
baseAmountIn: swapPaymentQuote.amountIn,
|
||||
isUsdAssetIn: false,
|
||||
maxBalance: swapPaymentQuote.bitcoinBalance,
|
||||
});
|
||||
executionResponse = await executeSwap(mnemonic, {
|
||||
poolId: swapPaymentQuote.poolId,
|
||||
assetInAddress: swapPaymentQuote.assetInAddress,
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
// Pure math utilities for Flashnet swap amount calculations.
|
||||
// No SDK imports — safe to test in Jest without mocking native modules.
|
||||
|
||||
export const BTC_ASSET_ADDRESS =
|
||||
'020202020202020202020202020202020202020202020202020202020202020202';
|
||||
export const USD_ASSET_ADDRESS =
|
||||
'3206c93b24a4d18ea19d0a9a213204af2c7e74a6d16c7535cc5d33eca4ad1eca';
|
||||
export const FLASHNET_POOL_IDENTITY_KEY =
|
||||
'02894808873b896e21d29856a6d7bb346fb13c019739adb9bf0b6a8b7e28da53da';
|
||||
|
||||
export const DEFAULT_SLIPPAGE_BPS = 100; // 1%
|
||||
export const SEND_AMOUNT_INCREASE_BUFFER = 1.01; // 1%
|
||||
export const DEFAULT_MAX_SLIPPAGE_BPS = 300; // 3% for lightning payments
|
||||
export const INTEGRATOR_FEE = 0.005; // .5%
|
||||
export const INTEGRATOR_FEE_BPS = 50;
|
||||
|
||||
/**
|
||||
* Convert sats to dollars.
|
||||
* @param {string|number|bigint} sats
|
||||
* @param {string|number|bigint} currentPriceAinB - BTC price in dollars (microdollar units)
|
||||
* @returns {number} Amount in dollars
|
||||
*/
|
||||
export function satsToDollars(sats, currentPriceAinB) {
|
||||
try {
|
||||
const DOLLAR_DECIMALS = 1_000_000;
|
||||
const numSats = typeof sats === 'bigint' ? Number(sats) : Number(sats || 0);
|
||||
const numPrice =
|
||||
typeof currentPriceAinB === 'bigint'
|
||||
? Number(currentPriceAinB)
|
||||
: Number(currentPriceAinB || 0);
|
||||
if (isNaN(numSats) || isNaN(numPrice) || numPrice === 0) return 0;
|
||||
return (numSats * numPrice) / DOLLAR_DECIMALS;
|
||||
} catch (error) {
|
||||
console.error('Error in satsToDollars:', error, { sats, currentPriceAinB });
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dollars to sats.
|
||||
* @param {string|number|bigint} dollars
|
||||
* @param {string|number|bigint} currentPriceAinB - BTC price in dollars (microdollar units)
|
||||
* @returns {number} Amount in sats
|
||||
*/
|
||||
export function dollarsToSats(dollars, currentPriceAinB) {
|
||||
try {
|
||||
const DOLLAR_DECIMALS = 1_000_000;
|
||||
const numDollars =
|
||||
typeof dollars === 'bigint' ? Number(dollars) : Number(dollars || 0);
|
||||
const numPrice =
|
||||
typeof currentPriceAinB === 'bigint'
|
||||
? Number(currentPriceAinB)
|
||||
: Number(currentPriceAinB || 0);
|
||||
if (isNaN(numDollars) || isNaN(numPrice) || numPrice === 0) return 0;
|
||||
return (numDollars * DOLLAR_DECIMALS) / numPrice;
|
||||
} catch (error) {
|
||||
console.error('Error in dollarsToSats:', error, {
|
||||
dollars,
|
||||
currentPriceAinB,
|
||||
});
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the final amountIn for a flashnet swap execution.
|
||||
* Applies a buffer and caps to available balance.
|
||||
* Always errs toward providing more input to avoid falling short of target output.
|
||||
* Especially important when the swap output funds a subsequent payment.
|
||||
*
|
||||
* @param {number} baseAmountIn - In smallest units: sats (BTC path) or microdollars (USD path)
|
||||
* @param {boolean} isUsdAssetIn - true = USD→BTC swap, false = BTC→USD swap
|
||||
* @param {number} [maxBalance] - Balance cap in same units as baseAmountIn (required for BTC
|
||||
* path; USD path can use dollarBalanceSat + currentPriceAInB)
|
||||
* @param {number} [dollarBalanceSat] - USD balance expressed in sats (USD path precision cap)
|
||||
* @param {number} [currentPriceAInB] - Pool price, required when dollarBalanceSat is provided
|
||||
* @param {number} [bufferMultiplier] - Overshoot factor; defaults to SEND_AMOUNT_INCREASE_BUFFER
|
||||
* @returns {number} Integer amountIn ready for executeSwap
|
||||
*/
|
||||
export function calculateFlashnetAmountIn({
|
||||
baseAmountIn,
|
||||
isUsdAssetIn,
|
||||
maxBalance,
|
||||
dollarBalanceSat,
|
||||
currentPriceAInB,
|
||||
bufferMultiplier = SEND_AMOUNT_INCREASE_BUFFER,
|
||||
}) {
|
||||
if (isUsdAssetIn) {
|
||||
// Work in dollars for cent-level precision, then convert back to microdollars.
|
||||
const bufferedDollars = (baseAmountIn * bufferMultiplier) / Math.pow(10, 6);
|
||||
const balanceDollars =
|
||||
dollarBalanceSat != null && currentPriceAInB != null
|
||||
? satsToDollars(dollarBalanceSat, currentPriceAInB)
|
||||
: maxBalance / Math.pow(10, 6);
|
||||
const cappedDollars = Math.min(bufferedDollars, balanceDollars);
|
||||
return Math.round(parseFloat(cappedDollars.toFixed(2)) * Math.pow(10, 6));
|
||||
}
|
||||
// BTC path: stay in sats
|
||||
return Math.round(Math.min(baseAmountIn * bufferMultiplier, maxBalance));
|
||||
}
|
||||
@@ -20,7 +20,7 @@ export async function transformTxToPaymentObject(
|
||||
sparkAddress,
|
||||
forcePaymentType,
|
||||
isRestore,
|
||||
unpaidLNInvoices,
|
||||
unpaidLNInvoices = [],
|
||||
identityPubKey,
|
||||
numTxsBeingRestored = 1,
|
||||
forceOutgoing = false,
|
||||
|
||||
@@ -7,7 +7,23 @@ import { NEAR_BUDGET_LIMIT, OVER_BUDGET_LIMIT } from '../constants';
|
||||
|
||||
// accepting them as parameters (test file defines its own copy inline).
|
||||
export function computeBudgetWarning(budget, spentTotal) {
|
||||
if (!budget || !budget.amount || budget.amount <= 0) {
|
||||
try {
|
||||
if (!budget || !budget.amount || budget.amount <= 0) {
|
||||
return {
|
||||
shouldWarn: false,
|
||||
isOverBudget: false,
|
||||
spentPercent: 0,
|
||||
leftToSpend: 0,
|
||||
};
|
||||
}
|
||||
const budgetAmount = budget.amount;
|
||||
const spentPercent = spentTotal / budgetAmount;
|
||||
const leftToSpend = Math.max(budgetAmount - spentTotal, 0);
|
||||
const shouldWarn = spentPercent >= NEAR_BUDGET_LIMIT;
|
||||
const isOverBudget = spentPercent >= OVER_BUDGET_LIMIT;
|
||||
return { shouldWarn, isOverBudget, spentPercent, leftToSpend };
|
||||
} catch (err) {
|
||||
console.log('compute budget warning error', err);
|
||||
return {
|
||||
shouldWarn: false,
|
||||
isOverBudget: false,
|
||||
@@ -15,12 +31,6 @@ export function computeBudgetWarning(budget, spentTotal) {
|
||||
leftToSpend: 0,
|
||||
};
|
||||
}
|
||||
const budgetAmount = budget.amount;
|
||||
const spentPercent = spentTotal / budgetAmount;
|
||||
const leftToSpend = Math.max(budgetAmount - spentTotal, 0);
|
||||
const shouldWarn = spentPercent >= NEAR_BUDGET_LIMIT;
|
||||
const isOverBudget = spentPercent >= OVER_BUDGET_LIMIT;
|
||||
return { shouldWarn, isOverBudget, spentPercent, leftToSpend };
|
||||
}
|
||||
|
||||
export function useBudgetWarning(sendingAmount = 0) {
|
||||
|
||||
@@ -316,6 +316,7 @@ const styles = StyleSheet.create({
|
||||
budgetStatusLabel: {
|
||||
fontSize: SIZES.smedium,
|
||||
fontWeight: '500',
|
||||
includeFontPadding: false,
|
||||
},
|
||||
progressBarTrack: {
|
||||
height: 6,
|
||||
|
||||
@@ -48,8 +48,8 @@ export default function ConfirmTxPage(props) {
|
||||
const [isAddingContact, setIsAddingContact] = useState(false);
|
||||
const isLNURLAuth = props.route.params?.useLNURLAuth;
|
||||
const transaction = props.route.params?.transaction;
|
||||
const hasError = props.route.params?.error;
|
||||
const paymentInformation = transaction?.details;
|
||||
const paymentInformation = transaction?.details || {};
|
||||
const hasError = props.route.params?.error || !paymentInformation;
|
||||
const lnurlAddress = normalizeLNURLAddress(props.route.params?.lnurlAddress);
|
||||
const isBlitzAddress = isBlitzLNURLAddress(lnurlAddress);
|
||||
const lnurlUsername = lnurlAddress?.split('@')[0]?.toLowerCase();
|
||||
@@ -87,7 +87,7 @@ export default function ConfirmTxPage(props) {
|
||||
|
||||
const paymentFee = paymentInformation?.fee;
|
||||
|
||||
const errorMessage = hasError;
|
||||
const errorMessage = hasError || t('errormessages.genericError');
|
||||
|
||||
const amount = paymentInformation?.amount || 0;
|
||||
|
||||
@@ -200,6 +200,48 @@ export default function ConfirmTxPage(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (props.route.params?.isSplitPayment && props.route.params?.isRequset) {
|
||||
return (
|
||||
<GlobalThemeView useStandardWidth={true} styles={styles.globalConatianer}>
|
||||
<LottieView
|
||||
ref={animationRef}
|
||||
source={didSucceed ? confirmAnimation : errorAnimation}
|
||||
loop={false}
|
||||
style={{
|
||||
width: screenDimensions.width / 1.5,
|
||||
height: screenDimensions.width / 1.5,
|
||||
maxWidth: 400,
|
||||
maxHeight: 400,
|
||||
}}
|
||||
/>
|
||||
<ThemeText
|
||||
styles={{ fontSize: SIZES.large, marginBottom: 10 }}
|
||||
content={t('screens.inAccount.confirmTxPage.bulkSuccess')}
|
||||
/>
|
||||
<CustomButton
|
||||
buttonStyles={{
|
||||
width: INSET_WINDOW_WIDTH,
|
||||
backgroundColor: !theme ? COLORS.primary : COLORS.darkModeText,
|
||||
marginTop: 'auto',
|
||||
paddingHorizontal: 15,
|
||||
}}
|
||||
textStyles={{
|
||||
...styles.buttonText,
|
||||
color: !theme ? COLORS.darkModeText : COLORS.lightModeText,
|
||||
}}
|
||||
actionFunction={() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
navigate.popToTop();
|
||||
});
|
||||
});
|
||||
}}
|
||||
textContent={t('constants.continue')}
|
||||
/>
|
||||
</GlobalThemeView>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<GlobalThemeView useStandardWidth={true} styles={styles.globalConatianer}>
|
||||
<LottieView
|
||||
|
||||
@@ -123,6 +123,9 @@ export default function ConnectingToNodeLoadingScreen({
|
||||
crashlyticsLogReport('Loaded users settings from firebase');
|
||||
}
|
||||
|
||||
// causes error in firebase let acync wait
|
||||
//https://github.com/firebase/firebase-ios-sdk/issues/15974#issuecomment-4155423268
|
||||
//https://github.com/firebase/firebase-ios-sdk/pull/15991
|
||||
toggleContactsPrivateKey(privateKey);
|
||||
console.log(balanceSnapshot, placeholderTxs, 'balance and tx snapshot');
|
||||
|
||||
|
||||
@@ -164,8 +164,7 @@ export default function TechnicalTransactionDetails(props) {
|
||||
<>
|
||||
<ThemeText
|
||||
content={t(
|
||||
'screens.inAccount.technicalTransactionDetails.amountBreakdown',
|
||||
{ defaultValue: 'Amount Breakdown' },
|
||||
'screens.inAccount.technicalTransactionDetails.transferIds',
|
||||
)}
|
||||
styles={styles.headerText}
|
||||
/>
|
||||
@@ -182,15 +181,6 @@ export default function TechnicalTransactionDetails(props) {
|
||||
// BTC: raw sats passed directly.
|
||||
// USD: amountCents * 10_000 = micros (matches DB amount unit and
|
||||
// what FormattedSatText + formatTokensNumber expect for LRC20).
|
||||
const rawAmount = isLRC20Payment
|
||||
? (entry.amountCents ?? 0) * 10_000
|
||||
: entry.amountSats ?? 0;
|
||||
const displayBalance = isLRC20Payment
|
||||
? formatTokensNumber(
|
||||
rawAmount,
|
||||
selectedToken?.tokenMetadata?.decimals,
|
||||
)
|
||||
: rawAmount;
|
||||
|
||||
const isLast = index === successfulGroup.length - 1;
|
||||
|
||||
@@ -219,15 +209,14 @@ export default function TechnicalTransactionDetails(props) {
|
||||
styles={styles.infoName}
|
||||
CustomNumberOfLines={1}
|
||||
/>
|
||||
{!details.isLRC20Payment && (
|
||||
<ThemeText
|
||||
content={entry?.transferId}
|
||||
styles={styles.transferId}
|
||||
CustomNumberOfLines={1}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
<FormattedSatText
|
||||
neverHideBalance={true}
|
||||
styles={styles.infoValue}
|
||||
balance={displayBalance}
|
||||
useCustomLabel={isLRC20Payment}
|
||||
customLabel={selectedToken?.tokenMetadata?.tokenTicker}
|
||||
useMillionDenomination={true}
|
||||
/>
|
||||
</View>
|
||||
{!isLast && (
|
||||
<View style={[styles.rowDivider, { backgroundColor }]} />
|
||||
@@ -271,6 +260,12 @@ const styles = StyleSheet.create({
|
||||
includeFontPadding: false,
|
||||
flexShrink: 1,
|
||||
},
|
||||
transferId: {
|
||||
fontSize: SIZES.small,
|
||||
opacity: HIDDEN_OPACITY,
|
||||
includeFontPadding: false,
|
||||
flexShrink: 1,
|
||||
},
|
||||
infoNameSmall: {
|
||||
fontSize: SIZES.smedium,
|
||||
opacity: HIDDEN_OPACITY,
|
||||
|
||||
@@ -10,27 +10,35 @@ import { useFlashnet } from './flashnetContext';
|
||||
import { getMonthlyTransactions } from '../app/functions/spark/transactions';
|
||||
import { getSatsFromTx } from '../app/functions/getSatsFromTx';
|
||||
import { buildCumulativeData } from '../app/components/admin/homeComponents/analytics/cumulativeLineChartHelpers';
|
||||
import { useAppStatus } from './appStatus';
|
||||
|
||||
const AnalyticsContext = createContext(null);
|
||||
|
||||
export function AnalyticsProvider({ children }) {
|
||||
const { sparkInformation } = useSparkWallet();
|
||||
const { didGetToHomepage } = useAppStatus();
|
||||
const { poolInfoRef } = useFlashnet();
|
||||
const [inTxs, setInTxs] = useState([]);
|
||||
const [outTxs, setOutTxs] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!sparkInformation.identityPubKey) return;
|
||||
if (!sparkInformation.identityPubKey || !didGetToHomepage) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const [incoming, outgoing] = await Promise.all([
|
||||
getMonthlyTransactions(sparkInformation.identityPubKey, 'INCOMING'),
|
||||
getMonthlyTransactions(sparkInformation.identityPubKey, 'OUTGOING'),
|
||||
]);
|
||||
setInTxs(incoming);
|
||||
setOutTxs(outgoing);
|
||||
const elapsed = Date.now() - startTime;
|
||||
const minDuration = 500;
|
||||
await new Promise(resolve =>
|
||||
setTimeout(resolve, Math.max(60, minDuration - elapsed)),
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('AnalyticsContext load error', e);
|
||||
} finally {
|
||||
@@ -38,11 +46,15 @@ export function AnalyticsProvider({ children }) {
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, [sparkInformation.identityPubKey, sparkInformation.transactions]);
|
||||
}, [
|
||||
sparkInformation.identityPubKey,
|
||||
sparkInformation.transactions,
|
||||
didGetToHomepage,
|
||||
]);
|
||||
|
||||
const incomeTotal = useMemo(
|
||||
() =>
|
||||
inTxs.reduce((sum, tx) => {
|
||||
const incomeTotal = useMemo(() => {
|
||||
try {
|
||||
return inTxs.reduce((sum, tx) => {
|
||||
try {
|
||||
return (
|
||||
sum + getSatsFromTx(tx, poolInfoRef.currentPriceAInB, 'INCOMING')
|
||||
@@ -50,13 +62,16 @@ export function AnalyticsProvider({ children }) {
|
||||
} catch {
|
||||
return sum;
|
||||
}
|
||||
}, 0),
|
||||
[inTxs],
|
||||
);
|
||||
}, 0);
|
||||
} catch (err) {
|
||||
console.log('eror calcuating total', err);
|
||||
return 0;
|
||||
}
|
||||
}, [inTxs]);
|
||||
|
||||
const spentTotal = useMemo(
|
||||
() =>
|
||||
outTxs.reduce((sum, tx) => {
|
||||
const spentTotal = useMemo(() => {
|
||||
try {
|
||||
return outTxs.reduce((sum, tx) => {
|
||||
try {
|
||||
return (
|
||||
sum + getSatsFromTx(tx, poolInfoRef.currentPriceAInB, 'OUTGOING')
|
||||
@@ -64,31 +79,40 @@ export function AnalyticsProvider({ children }) {
|
||||
} catch {
|
||||
return sum;
|
||||
}
|
||||
}, 0),
|
||||
[outTxs],
|
||||
);
|
||||
}, 0);
|
||||
} catch (err) {
|
||||
console.log('error calcuating spent', err);
|
||||
return 0;
|
||||
}
|
||||
}, [outTxs]);
|
||||
|
||||
const cumulativeIncomeData = useMemo(
|
||||
() =>
|
||||
buildCumulativeData(
|
||||
const cumulativeIncomeData = useMemo(() => {
|
||||
try {
|
||||
return buildCumulativeData(
|
||||
inTxs,
|
||||
undefined,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
'INCOMING',
|
||||
),
|
||||
[inTxs],
|
||||
);
|
||||
);
|
||||
} catch (err) {
|
||||
console.log('error creating cumulative income data', err);
|
||||
return [];
|
||||
}
|
||||
}, [inTxs]);
|
||||
|
||||
const cumulativeSpentData = useMemo(
|
||||
() =>
|
||||
buildCumulativeData(
|
||||
const cumulativeSpentData = useMemo(() => {
|
||||
try {
|
||||
return buildCumulativeData(
|
||||
outTxs,
|
||||
undefined,
|
||||
poolInfoRef.currentPriceAInB,
|
||||
'OUTGOING',
|
||||
),
|
||||
[outTxs],
|
||||
);
|
||||
);
|
||||
} catch (err) {
|
||||
console.log('error creating cumulative spend data', err);
|
||||
return [];
|
||||
}
|
||||
}, [outTxs]);
|
||||
|
||||
return (
|
||||
<AnalyticsContext.Provider
|
||||
@@ -111,6 +135,7 @@ export function AnalyticsProvider({ children }) {
|
||||
|
||||
export function useAnalytics() {
|
||||
const ctx = useContext(AnalyticsContext);
|
||||
if (!ctx) throw new Error('useAnalytics must be used within AnalyticsProvider');
|
||||
if (!ctx)
|
||||
throw new Error('useAnalytics must be used within AnalyticsProvider');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1210"
|
||||
LastUpgradeVersion = "2640"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
||||
+611
-426
File diff suppressed because it is too large
Load Diff
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "Angebots-ID",
|
||||
"destinationAddress": "Zieladresse",
|
||||
"destinationAsset": "Ziel-Asset",
|
||||
"destinationChain": "Ziel-Kette"
|
||||
"destinationChain": "Ziel-Kette",
|
||||
"transferIds": "Enthaltene Personen"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transaktionen",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "Quote ID",
|
||||
"destinationAddress": "Destination Address",
|
||||
"destinationAsset": "Destination Asset",
|
||||
"destinationChain": "Destination Chain"
|
||||
"destinationChain": "Destination Chain",
|
||||
"transferIds": "People Included"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transactions",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "ID de Cotización",
|
||||
"destinationAddress": "Dirección de Destino",
|
||||
"destinationAsset": "Activo de Destino",
|
||||
"destinationChain": "Cadena de Destino"
|
||||
"destinationChain": "Cadena de Destino",
|
||||
"transferIds": "Personas incluidas"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transacciones",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "ID du Devis",
|
||||
"destinationAddress": "Adresse de Destination",
|
||||
"destinationAsset": "Actif de Destination",
|
||||
"destinationChain": "Chaîne de Destination"
|
||||
"destinationChain": "Chaîne de Destination",
|
||||
"transferIds": "Personnes incluses"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transactions",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "ID Quotazione",
|
||||
"destinationAddress": "Indirizzo di Destinazione",
|
||||
"destinationAsset": "Asset di Destinazione",
|
||||
"destinationChain": "Catena di Destinazione"
|
||||
"destinationChain": "Catena di Destinazione",
|
||||
"transferIds": "Persone incluse"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transazioni",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "ID da cotação",
|
||||
"destinationAddress": "Endereço de destino",
|
||||
"destinationAsset": "Moeda de destino",
|
||||
"destinationChain": "Rede de destino"
|
||||
"destinationChain": "Rede de destino",
|
||||
"transferIds": "Pessoas incluídas"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transações",
|
||||
|
||||
@@ -2038,7 +2038,8 @@
|
||||
"quoteId": "ID котировки",
|
||||
"destinationAddress": "Адрес назначения",
|
||||
"destinationAsset": "Актив назначения",
|
||||
"destinationChain": "Сеть назначения"
|
||||
"destinationChain": "Сеть назначения",
|
||||
"transferIds": "Включённые участники"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Транзакции",
|
||||
|
||||
@@ -2037,7 +2037,8 @@
|
||||
"quoteId": "Offert-ID",
|
||||
"destinationAddress": "Destinationsadress",
|
||||
"destinationAsset": "Destinations-tillgång",
|
||||
"destinationChain": "Destinations-kedja"
|
||||
"destinationChain": "Destinations-kedja",
|
||||
"transferIds": "Inkluderade personer"
|
||||
},
|
||||
"viewAllTxPage": {
|
||||
"title": "Transaktioner",
|
||||
|
||||
+13
-13
@@ -25,13 +25,13 @@
|
||||
"@noble/hashes": "^1.8.0",
|
||||
"@noble/secp256k1": "^3.0.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-firebase/app": "^23.4.0",
|
||||
"@react-native-firebase/auth": "^23.4.0",
|
||||
"@react-native-firebase/crashlytics": "^23.4.0",
|
||||
"@react-native-firebase/firestore": "^23.4.0",
|
||||
"@react-native-firebase/functions": "^23.4.0",
|
||||
"@react-native-firebase/messaging": "^23.4.0",
|
||||
"@react-native-firebase/storage": "^23.4.0",
|
||||
"@react-native-firebase/app": "^24.0.0",
|
||||
"@react-native-firebase/auth": "^24.0.0",
|
||||
"@react-native-firebase/crashlytics": "^24.0.0",
|
||||
"@react-native-firebase/firestore": "^24.0.0",
|
||||
"@react-native-firebase/functions": "^24.0.0",
|
||||
"@react-native-firebase/messaging": "^24.0.0",
|
||||
"@react-native-firebase/storage": "^24.0.0",
|
||||
"@react-native-masked-view/masked-view": "^0.3.2",
|
||||
"@react-navigation/bottom-tabs": "^7.3.13",
|
||||
"@react-navigation/drawer": "^7.3.12",
|
||||
@@ -78,7 +78,7 @@
|
||||
"nostr-tools": "^2.15.0",
|
||||
"react": "19.1.0",
|
||||
"react-i18next": "^16.0.0",
|
||||
"react-native": "0.81.5",
|
||||
"react-native": "0.81.4",
|
||||
"react-native-context-menu-view": "^1.19.0",
|
||||
"react-native-country-flag": "^2.0.2",
|
||||
"react-native-country-picker-modal": "^2.0.0",
|
||||
@@ -93,7 +93,7 @@
|
||||
"react-native-pdf-from-image": "^0.3.6",
|
||||
"react-native-qrcode-svg": "^6.3.15",
|
||||
"react-native-quick-base64": "^2.2.2",
|
||||
"react-native-quick-crypto": "^1.0.16",
|
||||
"react-native-quick-crypto": "0.7.17",
|
||||
"react-native-reanimated": "^4.2.2",
|
||||
"react-native-restart-newarch": "^1.0.85",
|
||||
"react-native-safe-area-context": "~5.6.0",
|
||||
@@ -119,10 +119,10 @@
|
||||
"@react-native-community/cli": "20.0.0",
|
||||
"@react-native-community/cli-platform-android": "20.0.0",
|
||||
"@react-native-community/cli-platform-ios": "20.0.0",
|
||||
"@react-native/babel-preset": "0.81.5",
|
||||
"@react-native/eslint-config": "0.81.5",
|
||||
"@react-native/metro-config": "0.81.5",
|
||||
"@react-native/typescript-config": "0.81.5",
|
||||
"@react-native/babel-preset": "0.81.4",
|
||||
"@react-native/eslint-config": "0.81.4",
|
||||
"@react-native/metro-config": "0.81.4",
|
||||
"@react-native/typescript-config": "0.81.4",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/react": "~19.1.10",
|
||||
"@types/react-test-renderer": "^19.1.0",
|
||||
|
||||
Reference in New Issue
Block a user