New swaps (#704)

* adding basic template for swaps

* adding chain icons

* update tx details page to handle chain swaps

* add stablecoin amount format function

* updating UX + fixing styles

* creating select stablecoin currency page

* adding confirm page for stablecoin swaps

* finalizing sendpage for stablecoin swaps

* process stablecoin status

* adding translations

* fix solana and tron mismatch

* fixed paymentId missing from tx details for flashnet swaps

* fix eslint error

* making sure button text doesn't ruin layout

* fix quote reset while sending

* adding opacity to transition

* fixing estimated out format

* updating activesupport

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

* Update locales/pt-BR/translation.json

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>

---------

Co-authored-by: bitcoinuser <bitcoinuser@users.noreply.github.com>
This commit is contained in:
Blake Kaufman
2026-03-29 10:10:50 -04:00
committed by GitHub
co-authored by bitcoinuser
parent 2ce157ee54
commit ead5119542
37 changed files with 1888 additions and 87 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ ruby ">= 2.6.10"
# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'activesupport', '>= 7.2.3.1'
gem 'xcodeproj', '< 1.26.0'
gem 'concurrent-ruby', '< 1.3.4'
+8 -8
View File
@@ -5,7 +5,7 @@ GEM
base64
nkf
rexml
activesupport (7.2.2.2)
activesupport (7.2.3.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -14,7 +14,7 @@ GEM
drb
i18n (>= 1.6, < 2)
logger (>= 1.4.2)
minitest (>= 5.1)
minitest (>= 5.1, < 6)
securerandom (>= 0.3)
tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
@@ -24,8 +24,8 @@ GEM
json (>= 1.5.1)
atomos (0.1.3)
base64 (0.3.0)
benchmark (0.4.1)
bigdecimal (3.2.3)
benchmark (0.5.0)
bigdecimal (4.1.0)
claide (1.1.0)
cocoapods (1.15.2)
addressable (~> 2.8)
@@ -66,7 +66,7 @@ GEM
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.3)
connection_pool (2.5.4)
connection_pool (3.0.2)
drb (2.2.3)
escape (0.0.4)
ethon (0.15.0)
@@ -77,11 +77,11 @@ GEM
gh_inspector (1.1.3)
httpclient (2.9.0)
mutex_m
i18n (1.14.7)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
json (2.15.0)
logger (1.7.0)
minitest (5.25.5)
minitest (5.27.0)
molinillo (0.8.0)
mutex_m (0.3.0)
nanaimo (0.3.0)
@@ -108,7 +108,7 @@ PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
activesupport (>= 7.2.3.1)
benchmark
bigdecimal
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><clipPath id="a"><circle cx="16" cy="16" r="16"/></clipPath></defs><g clip-path="url(#a)"><rect width="32" height="32" fill="#213147"/><path fill="#12AAFF" d="m17.604 17.621-1.18 3.187a.4.4 0 0 0 0 .274l2.03 5.483 2.348-1.335-2.818-7.609a.203.203 0 0 0-.38 0m2.365-5.359a.203.203 0 0 0-.38 0l-1.18 3.187a.4.4 0 0 0 0 .274l3.326 8.976 2.348-1.335z"/><path fill="#9DCCED" d="M16 5.487a.4.4 0 0 1 .168.043l8.953 5.09a.33.33 0 0 1 .168.286v10.18a.33.33 0 0 1-.168.286l-8.953 5.09a.33.33 0 0 1-.168.043.4.4 0 0 1-.167-.043l-8.953-5.087a.33.33 0 0 1-.168-.286v-10.18a.33.33 0 0 1 .168-.287l8.953-5.09A.35.35 0 0 1 16 5.487m0-1.487c-.318 0-.637.08-.923.244L6.124 9.334A1.81 1.81 0 0 0 5.2 10.91v10.18c0 .65.352 1.25.924 1.575l8.953 5.09a1.88 1.88 0 0 0 1.848 0l8.952-5.09a1.81 1.81 0 0 0 .923-1.575V10.91a1.81 1.81 0 0 0-.923-1.576L17.724 4.244A1.87 1.87 0 0 0 16 4"/><path fill="#213147" d="m10.079 24.913.822-2.221 1.659 1.357-1.551 1.395z"/><path fill="#FFF" d="M15.244 10.18h-2.268a.4.4 0 0 0-.38.263l-4.865 13.136 2.348 1.335 5.358-14.465a.2.2 0 0 0-.193-.268m3.972-.001h-2.27a.4.4 0 0 0-.38.263l-5.556 15 2.348 1.334 6.047-16.328a.2.2 0 0 0-.19-.268"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 111 111" fill="none">
<circle cx="55.5" cy="55.5" r="55.5" fill="#0052FF"/>
<path d="M55.7 94C76.8 94 94 76.6 94 55.5S76.8 17 55.7 17C35.8 17 19.2 32.5 17.3 52.2h50.9v6.6H17.3C19.2 78.5 35.8 94 55.7 94z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#627EEA"/><g fill="#FFF" fill-rule="nonzero"><path fill-opacity=".602" d="M16.498 4v8.87l7.497 3.35z"/><path d="M16.498 4L9 16.22l7.498-3.35z"/><path fill-opacity=".602" d="M16.498 21.968v6.027L24 17.616z"/><path d="M16.498 27.995v-6.028L9 17.616z"/><path fill-opacity=".2" d="M16.498 20.573l7.497-4.353-7.497-3.348z"/><path fill-opacity=".602" d="M9 16.22l7.498 4.353v-7.701z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 526 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><clipPath id="a"><circle cx="16" cy="16" r="16"/></clipPath></defs><g clip-path="url(#a)"><rect width="32" height="32" fill="#FF0420"/><path fill="#FFF" fill-rule="evenodd" d="M5.288 21.067q1.305.933 3.35.933 2.472 0 3.949-1.117 1.477-1.133 2.079-3.416.36-1.4.619-2.884.084-.53.085-.884 0-1.165-.601-1.999a3.6 3.6 0 0 0-1.65-1.266Q12.07 10 10.749 10q-4.858 0-6.026 4.583a53 53 0 0 0-.636 2.884q-.077.447-.087.898 0 1.752 1.288 2.702m6.2-3.7c-.33 1.276-1.235 2.107-2.611 2.107-1.36 0-1.824-.92-1.579-2.107a36 36 0 0 1 .619-2.733c.353-1.379 1.187-2.107 2.608-2.107 1.356 0 1.797.906 1.564 2.107a40 40 0 0 1-.601 2.733m4.625 4.01q.101.12.285.12h2.272a.5.5 0 0 0 .317-.12.48.48 0 0 0 .184-.309l.718-3.36h2.31c1.459 0 2.6-.707 3.435-1.336q1.271-.943 1.688-2.914.1-.465.1-.894 0-1.489-1.135-2.28Q25.16 10 23.68 10h-4.444a.5.5 0 0 0-.317.12.5.5 0 0 0-.184.31l-2.307 11.14a.4.4 0 0 0 .05.31m7.217-7.955c-.21.919-1.01 1.758-1.95 1.758h-1.92l.662-3.158h2.004c.683 0 1.253.135 1.253.886q0 .22-.049.514" clip-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none"><circle fill="#EF0027" cx="16" cy="16" r="16"/><path d="M21.932 9.913L7.5 7.257l7.595 19.112 10.583-12.894-3.746-3.562zm-.232 1.17l2.208 2.099-6.038 1.093 3.83-3.192zm-5.142 2.973l-6.364-5.278 10.402 1.914-4.038 3.364zm-.453.934l-1.038 8.58L9.472 9.487l6.633 5.502zm.96.455l6.687-1.21-7.67 9.343.983-8.133z" fill="#FFF"/></g></svg>

After

Width:  |  Height:  |  Size: 407 B

+5
View File
@@ -0,0 +1,5 @@
<svg data-name="86977684-12db-4850-8f30-233a7c267d11" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2000 2000">
<path d="M1000 2000c554.17 0 1000-445.83 1000-1000S1554.17 0 1000 0 0 445.83 0 1000s445.83 1000 1000 1000z" fill="#2775ca"/>
<path d="M1275 1158.33c0-145.83-87.5-195.83-262.5-216.66-125-16.67-150-50-150-108.34s41.67-95.83 125-95.83c75 0 116.67 25 137.5 87.5 4.17 12.5 16.67 20.83 29.17 20.83h66.66c16.67 0 29.17-12.5 29.17-29.16v-4.17c-16.67-91.67-91.67-162.5-187.5-170.83v-100c0-16.67-12.5-29.17-33.33-33.34h-62.5c-16.67 0-29.17 12.5-33.34 33.34v95.83c-125 16.67-204.16 100-204.16 204.17 0 137.5 83.33 191.66 258.33 212.5 116.67 20.83 154.17 45.83 154.17 112.5s-58.34 112.5-137.5 112.5c-108.34 0-145.84-45.84-158.34-108.34-4.16-16.66-16.66-25-29.16-25h-70.84c-16.66 0-29.16 12.5-29.16 29.17v4.17c16.66 104.16 83.33 179.16 220.83 200v100c0 16.66 12.5 29.16 33.33 33.33h62.5c16.67 0 29.17-12.5 33.34-33.33v-100c125-20.84 208.33-108.34 208.33-220.84z" fill="#fff"/>
<path d="M787.5 1595.83c-325-116.66-491.67-479.16-370.83-800 62.5-175 200-308.33 370.83-370.83 16.67-8.33 25-20.83 25-41.67V325c0-16.67-8.33-29.17-25-33.33-4.17 0-12.5 0-16.67 4.16-395.83 125-612.5 545.84-487.5 941.67 75 233.33 254.17 412.5 487.5 487.5 16.67 8.33 33.34 0 37.5-16.67 4.17-4.16 4.17-8.33 4.17-16.66v-58.34c0-12.5-12.5-29.16-25-37.5zM1229.17 295.83c-16.67-8.33-33.34 0-37.5 16.67-4.17 4.17-4.17 8.33-4.17 16.67v58.33c0 16.67 12.5 33.33 25 41.67 325 116.66 491.67 479.16 370.83 800-62.5 175-200 308.33-370.83 370.83-16.67 8.33-25 20.83-25 41.67V1700c0 16.67 8.33 29.17 25 33.33 4.17 0 12.5 0 16.67-4.16 395.83-125 612.5-545.84 487.5-941.67-75-237.5-258.34-416.67-487.5-491.67z" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><g fill="none" fill-rule="evenodd"><circle cx="16" cy="16" r="16" fill="#26A17B"/><path fill="#FFF" d="M17.922 17.383v-.002c-.11.008-.677.042-1.942.042-1.01 0-1.721-.03-1.971-.042v.003c-3.888-.171-6.79-.848-6.79-1.658 0-.809 2.902-1.486 6.79-1.66v2.644c.254.018.982.061 1.988.061 1.207 0 1.812-.05 1.925-.06v-2.643c3.88.173 6.775.85 6.775 1.658 0 .81-2.895 1.485-6.775 1.657m0-3.59v-2.366h5.414V7.819H8.595v3.608h5.414v2.365c-4.4.202-7.709 1.074-7.709 2.118 0 1.044 3.309 1.915 7.709 2.118v7.582h3.913v-7.584c4.393-.202 7.694-1.073 7.694-2.116 0-1.043-3.301-1.914-7.694-2.117"/></g></svg>

After

Width:  |  Height:  |  Size: 649 B

@@ -37,6 +37,8 @@ import Animated, {
useHandler,
useEvent,
LinearTransition,
FadeIn,
FadeOut,
} from 'react-native-reanimated';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import { TAB_ITEM_HEIGHT } from '../../../../navigation/tabs';
@@ -676,6 +678,8 @@ export default function HomeLightning({ navigation }) {
return (
<Animated.View
key={tx.key}
entering={FadeIn.duration(150)}
exiting={FadeOut.duration(150)}
layout={LinearTransition.duration(300)}
>
{tx.item}
@@ -10,6 +10,7 @@ import {
navigateToSendUsingClipboard,
getQRImage,
} from '../../../../functions';
import handlePreSendPageParsing from '../../../../functions/sendBitcoin/handlePreSendPageParsing';
import { ThemeText } from '../../../../functions/CustomElements';
import { useGlobalContacts } from '../../../../../context-store/globalContacts';
import { useTranslation } from 'react-i18next';
@@ -373,8 +374,27 @@ export default function HalfModalSendOptions({
return;
}
const parsed = handlePreSendPageParsing(input);
if (parsed.error) {
navigate.navigate('ErrorScreen', { errorMessage: parsed.error });
return;
}
if (parsed.navigateToWebView) {
navigate.navigate('CustomWebView', {
headerText: '',
webViewURL: parsed.webViewURL,
});
return;
}
if (parsed.isExternalChain) {
navigate.replace('SelectStablecoinParamsScreen', {
address: parsed.address,
chainFamily: parsed.chainFamily,
});
return;
}
navigate.replace('ConfirmPaymentScreen', {
btcAdress: input,
btcAdress: parsed.btcAdress,
fromPage: '',
});
}, [
@@ -406,6 +426,15 @@ export default function HalfModalSendOptions({
});
return;
}
if (response.isExternalChain) {
navigate.replace('SelectStablecoinParamsScreen', {
address: response.address,
chainFamily: response.chainFamily,
});
return;
}
if (!response.didWork || !response.btcAdress) {
return;
}
@@ -257,11 +257,19 @@ export default function TxFilterHalfModal({
{/* --- Sticky bottom bar --- */}
<View style={[styles.bottomBar, { borderTopColor: inactiveBorderColor }]}>
<TouchableOpacity
style={{ opacity: hasItemsSeleceted ? 1 : HIDDEN_OPACITY }}
style={{
opacity: hasItemsSeleceted ? 1 : HIDDEN_OPACITY,
flexShrink: 1,
}}
onPress={handleClear}
activeOpacity={hasItemsSeleceted ? 0.2 : HIDDEN_OPACITY}
>
<ThemeText content={t(`${ns}.filterClearAll`)} />
<ThemeText
CustomNumberOfLines={2}
adjustsFontSizeToFit={true}
allowFontScaling={true}
content={t(`${ns}.filterClearAll`)}
/>
</TouchableOpacity>
<CustomButton
buttonStyles={styles.applyButton}
@@ -352,6 +360,7 @@ const styles = StyleSheet.create({
justifyContent: 'space-between',
paddingVertical: 14,
borderTopWidth: 1,
gap: 10,
},
clearText: {
fontSize: SIZES.medium,
@@ -34,6 +34,8 @@ import GetThemeColors from '../../../../../hooks/themeColors';
import { copyToClipboard } from '../../../../../functions';
import { useToast } from '../../../../../../context-store/toastManager';
import ThemeIcon from '../../../../../functions/CustomElements/themeIcon';
import { satsToDollars } from '../../../../../functions/spark/flashnet';
import { useFlashnet } from '../../../../../../context-store/flashnetContext';
const confirmTxAnimation = require('../../../../../assets/confirmTxAnimation');
+20
View File
@@ -22,6 +22,15 @@ import dollarIcon from '../assets/icons/dollarIcon.png';
import bitcoinIcon from '../assets/icons/bitcoinIcon.png';
import giftCardIcon from '../assets/icons/giftCardIcon.png';
import nwcLogo from '../assets/icons/nwcLogo.png';
import chain_arbitrum from '../assets/icons/chain-arbitrum.svg';
import chain_base from '../assets/icons/chain-base.svg';
import chain_ethereum from '../assets/icons/chain-ethereum.svg';
import chain_optimism from '../assets/icons/chain-optimism.svg';
import chain_plasma from '../assets/icons/chain-plasma.svg';
import chain_tron from '../assets/icons/chain-tron.svg';
import chain_polygon from '../assets/icons/chain-polygon.png';
import usdcLogo from '../assets/icons/usdc.svg';
import usdtLogo from '../assets/icons/usdt.svg';
export default {
logoIcon,
@@ -61,4 +70,15 @@ export default {
dollarIcon,
bitcoinIcon,
nwcLogo,
// chain logos
chain_arbitrum,
chain_base,
chain_ethereum,
chain_optimism,
chain_plasma,
chain_tron,
chain_polygon,
usdcLogo,
usdtLogo,
};
+10
View File
@@ -70,6 +70,7 @@ import ClaimGiftHomeHalfModal from '../../components/admin/homeComponents/gifts/
import SwapFlowHalfModal from '../../components/admin/homeComponents/swaps/swapFlowHalfModal';
import TxFilterHalfModal from '../../components/admin/homeComponents/homeLightning/txFilterHalfModal';
import PayLinkCurrencySelect from '../../components/admin/homeComponents/payLinks/components/payLinkCurrencySelect';
import StablecoinAssetPickerHalfModal from './stablecoinAssetPickerHalfModal';
export default function CustomHalfModal(props) {
const { theme, darkModeType } = useGlobalThemeContext();
@@ -557,6 +558,15 @@ export default function CustomHalfModal(props) {
setContentHeight={setContentHeight}
/>
);
case 'stablecoinAssetPicker':
return (
<StablecoinAssetPickerHalfModal
selectedChain={props?.route?.params?.selectedChain}
address={props?.route?.params?.address}
handleBackPressFunction={handleBackPressFunction}
setContentHeight={setContentHeight}
/>
);
default:
return <ThemeText content={'TST'} />;
}
@@ -0,0 +1,180 @@
import { useEffect } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { Image } from 'expo-image';
import { ThemeText } from '.';
import GetThemeColors from '../../hooks/themeColors';
import { ICONS, SIZES } from '../../constants';
import { useGlobalThemeContext } from '../../../context-store/theme';
import { ChevronRight } from 'lucide-react-native';
import { HIDDEN_OPACITY } from '../../constants/theme';
import { useTranslation } from 'react-i18next';
const CHAIN_TO_FLASHNET = {
Ethereum: 'ethereum',
Arbitrum: 'arbitrum',
Base: 'base',
Optimism: 'optimism',
Polygon: 'polygon',
Tron: 'tron',
};
const USDT_SUPPORTED_CHAINS = new Set([
'Ethereum',
'Arbitrum',
'Optimism',
'Tron',
]);
const USDC_SUPPORTED_CHAINS = new Set([
'Ethereum',
'Arbitrum',
'Base',
'Optimism',
'Polygon',
]);
const ASSETS = [
{
id: 'USDC',
name: 'USDC',
icon: ICONS.usdcLogo,
},
{
id: 'USDT',
name: 'USDT',
icon: ICONS.usdtLogo,
},
];
export default function StablecoinAssetPickerHalfModal({
selectedChain,
address,
handleBackPressFunction,
setContentHeight,
}) {
const navigate = useNavigation();
const { backgroundOffset, backgroundColor, textColor } = GetThemeColors();
const { theme, darkModeType } = useGlobalThemeContext();
const { t } = useTranslation();
useEffect(() => {
setContentHeight(400);
}, []);
const rowBg = theme && darkModeType ? backgroundColor : backgroundOffset;
const handleAssetSelect = asset => {
handleBackPressFunction(() => {
navigate.replace('StablecoinSendScreen', {
address,
chain: CHAIN_TO_FLASHNET[selectedChain],
chainLabel: selectedChain,
asset,
});
});
};
return (
<View style={styles.container}>
<ThemeText
styles={styles.title}
content={t('wallet.stablecoinSend.assetPicker.header')}
/>
{ASSETS.map(item => {
const isUnsupported =
(item.id === 'USDT' && !USDT_SUPPORTED_CHAINS.has(selectedChain)) ||
(item.id === 'USDC' && !USDC_SUPPORTED_CHAINS.has(selectedChain));
const rowContent = (
<>
<Image
style={styles.assetIcon}
source={item.icon}
contentFit="contain"
/>
<View style={styles.assetInfo}>
<ThemeText styles={styles.assetName} content={item.name} />
{isUnsupported && (
<ThemeText
styles={styles.unsupportedLabel}
content={t('wallet.stablecoinSend.assetPicker.notAvailable', {
chain: selectedChain,
})}
/>
)}
</View>
{!isUnsupported && (
<ChevronRight
size={20}
color={textColor}
style={{ opacity: 0.5 }}
/>
)}
</>
);
if (isUnsupported) {
return (
<View
key={item.id}
style={[styles.row, { backgroundColor: rowBg, opacity: 0.4 }]}
pointerEvents="none"
>
{rowContent}
</View>
);
}
return (
<TouchableOpacity
key={item.id}
style={[styles.row, { backgroundColor: rowBg }]}
onPress={() => handleAssetSelect(item.id)}
activeOpacity={0.7}
>
{rowContent}
</TouchableOpacity>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingBottom: 16,
gap: 10,
},
title: {
fontSize: SIZES.large,
fontWeight: 500,
marginBottom: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
borderRadius: 12,
gap: 12,
},
assetIcon: {
width: 40,
height: 40,
},
assetInfo: {
flex: 1,
gap: 2,
},
assetName: {
fontSize: SIZES.large,
fontWeight: 500,
},
unsupportedLabel: {
fontSize: SIZES.small,
opacity: HIDDEN_OPACITY,
},
});
@@ -3,10 +3,35 @@ import { IS_BLITZ_URL_REGEX, WEBSITE_REGEX } from '../../constants';
import testURLForInvoice from '../testURLForInvoice';
import { convertMerchantQRToLightningAddress } from './getMerchantAddress';
const EVM_REGEX = /^0x[0-9a-fA-F]{40}$/;
const TRON_REGEX = /^T[1-9A-HJ-NP-Za-km-z]{33}$/;
const SOLANA_REGEX = /^[1-9A-HJ-NP-Za-km-z]{43,44}$/;
export default function handlePreSendPageParsing(data) {
try {
if (!data) throw new Error(i18next.t('errormessages.invalidData'));
const trimmed = data.trim();
if (EVM_REGEX.test(trimmed)) {
return {
didWork: true,
error: null,
isExternalChain: true,
address: trimmed,
chainFamily: 'EVM',
};
}
if (TRON_REGEX.test(trimmed)) {
return {
didWork: true,
error: null,
isExternalChain: true,
address: trimmed,
chainFamily: 'Tron',
};
}
if (WEBSITE_REGEX.test(data)) {
if (IS_BLITZ_URL_REGEX.test(data))
throw new Error(i18next.t('errormessages.invalidData'));
@@ -28,6 +53,16 @@ export default function handlePreSendPageParsing(data) {
network: process.env.BOLTZ_ENVIRONMENT,
});
if (!merchantLNAddress && SOLANA_REGEX.test(trimmed)) {
return {
didWork: true,
error: null,
isExternalChain: true,
address: trimmed,
chainFamily: 'Solana',
};
}
return {
didWork: true,
error: null,
+30 -1
View File
@@ -29,6 +29,20 @@ async function navigateToSendUsingClipboard(navigate, callLocation, from, t) {
return;
}
if (preParsingResponse.isExternalChain) {
if (from === 'home')
navigate.navigate('SelectStablecoinParamsScreen', {
address: preParsingResponse.address,
chainFamily: preParsingResponse.chainFamily,
});
else
navigate.replace('SelectStablecoinParamsScreen', {
address: preParsingResponse.address,
chainFamily: preParsingResponse.chainFamily,
});
return;
}
if (from === 'home')
navigate.navigate('ConfirmPaymentScreen', {
btcAdress: preParsingResponse.btcAdress,
@@ -95,7 +109,22 @@ async function getQRImage() {
};
}
if (preParsingResponse.isExternalChain) {
return {
isExternalChain: true,
address: preParsingResponse.address,
chainFamily: preParsingResponse.chainFamily,
didWork: true,
error: '',
};
}
return { btcAdress: preParsingResponse.btcAdress, didWork: true, error: '' };
}
export { navigateToSendUsingClipboard, getQRImage };
function formatStablecoinAmount(rawAmount, decimals = 2) {
const value = Number(rawAmount) / Math.pow(10, 6);
return value.toFixed(decimals);
}
export { navigateToSendUsingClipboard, getQRImage, formatStablecoinAmount };
+66 -1
View File
@@ -31,6 +31,7 @@ import {
} from './transactions';
import { transformTxToPaymentObject } from './transformTxToPayment';
import sha256Hash from '../hash';
import fetchBackend from '../../../db/handleBackend';
const RESTORE_STATE_KEY = 'spark_tx_restore_state';
const MAX_BATCH_SIZE = 400;
@@ -490,12 +491,54 @@ export async function fullRestoreSparkState({
}
}
export async function checkFlashnetStablecoinStatusLogic(
tx,
contactsPrivateKey,
publicKey,
) {
try {
const details =
typeof tx.details === 'string' ? JSON.parse(tx.details) : tx.details;
if (!details?.isFlashnetStablecoin || !details?.quoteId) return null;
const statusResult = await fetchBackend(
'checkFlashnetStablecoinStatus',
{ quoteId: details.quoteId },
contactsPrivateKey,
publicKey,
);
if (!statusResult || statusResult.error) return null;
const newStatus =
statusResult.status === 'completed'
? 'completed'
: statusResult.status === 'failed'
? 'failed'
: null;
if (!newStatus) return null;
return {
id: tx.sparkID,
paymentStatus: newStatus,
paymentType: tx.paymentType,
accountId: tx.accountId,
details,
};
} catch {
return null;
}
}
let isUpdatingSparkTxStatus = false;
export const updateSparkTxStatus = async (
mnemoninc,
accountId,
sendWebViewRequest,
forceRefresh = false,
contactsPrivateKey = null,
publicKey = null,
) => {
try {
if (isUpdatingSparkTxStatus) {
@@ -547,7 +590,13 @@ export const updateSparkTxStatus = async (
accountId,
forceRefresh,
),
processSparkTransactions(txsByType.spark, mnemoninc, sendWebViewRequest),
processSparkTransactions(
txsByType.spark,
mnemoninc,
sendWebViewRequest,
contactsPrivateKey,
publicKey,
),
]);
const updatedTxs = [
@@ -942,11 +991,27 @@ async function processSparkTransactions(
sparkTxs,
mnemonic,
sendWebViewRequest,
contactsPrivateKey = null,
publicKey = null,
) {
let includesGift = false;
let updatedTxs = [];
for (const txStateUpdate of sparkTxs) {
const details = JSON.parse(txStateUpdate.details);
// Stablecoin sends via Flashnet orchestration — delegate to status checker
if (details.isFlashnetStablecoin) {
if (contactsPrivateKey && publicKey) {
const update = await checkFlashnetStablecoinStatusLogic(
txStateUpdate,
contactsPrivateKey,
publicKey,
);
if (update) updatedTxs.push(update);
}
continue;
}
if (IS_SPARK_ID.test(txStateUpdate.sparkID)) {
const findTxResponse = await getSingleTxDetails(
mnemonic,
+85 -1
View File
@@ -27,7 +27,7 @@ import customUUID from '../../functions/customUUID';
import { useGlobalContactsInfo } from '../../../context-store/globalContacts';
import { getSingleContact } from '../../../db';
import { getCachedProfileImage } from '../../functions/cachedImage';
import { INSET_WINDOW_WIDTH } from '../../constants/theme';
import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH } from '../../constants/theme';
import normalizeLNURLAddress from '../../functions/lnurl/normalizeLNURLAddress';
const BLITZ_DOMAINS = [
@@ -92,6 +92,9 @@ export default function ConfirmTxPage(props) {
: transaction?.paymentType;
const showPendingMessage = transaction?.paymentStatus === 'pending';
const isFlashnetStablecoinPending =
paymentInformation?.isFlashnetStablecoin &&
transaction?.paymentStatus === 'pending';
const paymentFee = paymentInformation?.fee;
@@ -127,6 +130,87 @@ export default function ConfirmTxPage(props) {
animationRef.current?.play();
}, []);
if (isFlashnetStablecoinPending) {
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('wallet.stablecoinSend.swapInProgress')}
/>
<ThemeText
styles={{
opacity: HIDDEN_OPACITY,
width: '95%',
maxWidth: 300,
textAlign: 'center',
marginBottom: 50,
fontSize: SIZES.smedium,
}}
content={t('wallet.stablecoinSend.swapInProgressDescription')}
/>
<View style={styles.paymentTable}>
<View style={styles.paymentTableRow}>
<ThemeText
CustomNumberOfLines={1}
styles={styles.labelText}
content={t('constants.fee')}
/>
<ThemeText
content={displayCorrectDenomination({
amount: paymentFee,
masterInfoObject,
fiatStats,
})}
/>
</View>
<View style={styles.paymentTableRow}>
<ThemeText
CustomNumberOfLines={1}
styles={styles.labelText}
content={t('constants.type')}
/>
<ThemeText
styles={{ textTransform: 'capitalize' }}
content={t('wallet.stablecoinSend.chainSwap')}
/>
</View>
</View>
<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
+14 -2
View File
@@ -240,6 +240,8 @@ export default function ExpandedTx(props) {
}
};
const isFlashnetStablecoin = !!transaction.details?.isFlashnetStablecoin;
const isLRC20Payment = transaction.details.isLRC20Payment;
const selectedToken = isLRC20Payment
? sparkInformation.tokens?.[transaction.details.LRC20Token]
@@ -520,7 +522,9 @@ export default function ExpandedTx(props) {
{renderInfoRow(
t('constants.type'),
transactionPaymentType,
isFlashnetStablecoin
? t('screens.inAccount.expandedTxPage.chainSwap')
: transactionPaymentType,
true,
{
textTransform: 'capitalize',
@@ -529,6 +533,15 @@ export default function ExpandedTx(props) {
{renderLRC20TokenRow()}
{isFlashnetStablecoin &&
transaction.paymentStatus !== 'completed' &&
renderInfoRow(
t('wallet.stablecoinSend.swapStatus'),
transaction.paymentStatus,
true,
{ textTransform: 'capitalize' },
)}
{showConversionLine &&
renderInfoRow(
t('constants.rate'),
@@ -962,7 +975,6 @@ const styles = StyleSheet.create({
// fontSize: SIZES.large,
},
tokenText: {
fontSize: SIZES.large,
textTransform: 'uppercase',
flex: 1,
textAlign: 'right',
@@ -0,0 +1,197 @@
import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native';
import { useState, useCallback } from 'react';
import { useNavigation, useRoute } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import { GlobalThemeView, ThemeText } from '../../functions/CustomElements';
import CustomButton from '../../functions/CustomElements/button';
import CustomSettingsTopBar from '../../functions/CustomElements/settingsTopBar';
import GetThemeColors from '../../hooks/themeColors';
import {
CENTER,
COLORS,
CONTENT_KEYBOARD_OFFSET,
FONT,
ICONS,
SIZES,
} from '../../constants';
import { useGlobalThemeContext } from '../../../context-store/theme';
import { useGlobalInsets } from '../../../context-store/insetsProvider';
import { Image } from 'expo-image';
import { HIDDEN_OPACITY, INSET_WINDOW_WIDTH } from '../../constants/theme';
const EVM_CHAINS = ['Base', 'Ethereum', 'Arbitrum', 'Optimism', 'Polygon'];
const TRON_ASSETS = [{ id: 'USDT', label: 'USDT', icon: ICONS.usdtLogo }];
const SOLANA_ASSETS = [{ id: 'USDC', label: 'USDC', icon: ICONS.usdcLogo }];
export default function SelectStablecoinParamsScreen() {
const navigate = useNavigation();
const route = useRoute();
const { address, chainFamily } = route.params;
const { t } = useTranslation();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const { theme, darkModeType } = useGlobalThemeContext();
const { bottomPadding } = useGlobalInsets();
const isTron = chainFamily === 'Tron';
const isSolana = chainFamily === 'Solana';
const chains = isTron || isSolana ? [] : EVM_CHAINS;
const [selectedChain, setSelectedChain] = useState(null);
const handleContinue = useCallback(() => {
navigate.navigate('CustomHalfModal', {
wantedContent: 'stablecoinAssetPicker',
selectedChain,
address,
sliderHight: 0.5,
});
}, [navigate, address, selectedChain]);
const handleNonEvmAssetSelect = useCallback(
asset => {
navigate.navigate('StablecoinSendScreen', {
address,
chain: chainFamily.toLowerCase(),
chainLabel: chainFamily,
asset,
});
},
[navigate, address],
);
const cardBg = backgroundOffset;
return (
<GlobalThemeView useStandardWidth={true}>
<CustomSettingsTopBar
label={t('wallet.stablecoinSend.sendStablecoins')}
containerStyles={{ marginBottom: 0 }}
/>
<ThemeText
styles={styles.sectionTitle}
content={t(
isTron || isSolana
? 'wallet.stablecoinSend.selectAsset'
: 'wallet.stablecoinSend.selectChain',
)}
/>
<View style={styles.innerContainer}>
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={[
styles.scrollContent,
{ paddingBottom: bottomPadding + 20 },
]}
>
<View style={styles.section}>
<View style={styles.grid}>
{isTron || isSolana
? (isTron ? TRON_ASSETS : SOLANA_ASSETS).map(asset => (
<TouchableOpacity
key={asset.id}
style={[
styles.card,
{ backgroundColor: cardBg, borderColor: cardBg },
]}
onPress={() => handleNonEvmAssetSelect(asset.id)}
activeOpacity={0.7}
>
<Image
style={styles.cardIcon}
source={asset.icon}
contentFit="contain"
/>
<ThemeText
styles={styles.cardLabel}
content={asset.label}
/>
</TouchableOpacity>
))
: chains.map(chain => {
const isSelected = selectedChain === chain;
return (
<TouchableOpacity
key={chain}
style={[
styles.card,
{ backgroundColor: cardBg, borderColor: cardBg },
isSelected && {
borderColor:
theme && darkModeType
? COLORS.darkModeText
: COLORS.primary,
},
]}
onPress={() => setSelectedChain(chain)}
activeOpacity={0.7}
>
<Image
style={styles.cardIcon}
source={ICONS[`chain_${chain.toLowerCase()}`]}
contentFit="contain"
/>
<ThemeText styles={styles.cardLabel} content={chain} />
</TouchableOpacity>
);
})}
</View>
</View>
</ScrollView>
{!isTron && !isSolana && (
<CustomButton
textContent={t('constants.continue')}
actionFunction={handleContinue}
disabled={!selectedChain}
buttonStyles={!selectedChain ? { opacity: 0.5 } : undefined}
/>
)}
</View>
</GlobalThemeView>
);
}
const styles = StyleSheet.create({
innerContainer: {
flex: 1,
width: INSET_WINDOW_WIDTH,
...CENTER,
},
scrollContent: {
gap: 16,
paddingTop: 8,
},
section: {
gap: 8,
},
sectionTitle: {
fontSize: SIZES.medium,
opacity: HIDDEN_OPACITY,
textAlign: 'center',
marginBottom: CONTENT_KEYBOARD_OFFSET,
},
grid: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 12,
},
card: {
width: '47%',
borderRadius: 16,
padding: 16,
alignItems: 'center',
gap: 10,
borderColor: 'transparent',
borderWidth: 1.5,
},
cardIcon: {
width: 40,
height: 40,
},
cardLabel: {
fontSize: SIZES.medium,
fontFamily: FONT.Descriptoin_Medium,
textAlign: 'center',
},
});
+31
View File
@@ -129,6 +129,21 @@ export default function SendPaymentHome({ pageViewPage, from }) {
return;
}
if (response.isExternalChain) {
if (from === 'home')
navigate.navigate('SelectStablecoinParamsScreen', {
address: response.address,
chainFamily: response.chainFamily,
});
else
navigate.replace('SelectStablecoinParamsScreen', {
address: response.address,
chainFamily: response.chainFamily,
});
return;
}
if (from === 'home')
navigate.navigate('ConfirmPaymentScreen', {
btcAdress: response.btcAdress,
@@ -205,6 +220,22 @@ export default function SendPaymentHome({ pageViewPage, from }) {
return;
}
if (response.isExternalChain) {
if (from === 'home')
navigate.navigate('SelectStablecoinParamsScreen', {
address: response.address,
chainFamily: response.chainFamily,
});
else
navigate.replace('SelectStablecoinParamsScreen', {
address: response.address,
chainFamily: response.chainFamily,
});
isPhotoeLibraryOpen.current = false;
return;
}
if (!response.didWork || !response.btcAdress) {
isPhotoeLibraryOpen.current = false;
return;
@@ -0,0 +1,777 @@
import {
ActivityIndicator,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from 'react-native';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useNavigation, useRoute } from '@react-navigation/native';
import { useTranslation } from 'react-i18next';
import {
CustomKeyboardAvoidingView,
ThemeText,
} from '../../functions/CustomElements';
import CustomButton from '../../functions/CustomElements/button';
import CustomSettingsTopBar from '../../functions/CustomElements/settingsTopBar';
import FormattedBalanceInput from '../../functions/CustomElements/formattedBalanceInput';
import FormattedSatText from '../../functions/CustomElements/satTextDisplay';
import CustomNumberKeyboard from '../../functions/CustomElements/customNumberKeyboard';
import CustomSearchInput from '../../functions/CustomElements/searchInput';
import ChoosePaymentMethod from '../../components/admin/homeComponents/sendBitcoin/components/choosePaymentMethodContainer';
import SwipeButtonNew from '../../functions/CustomElements/sliderButton';
import GetThemeColors from '../../hooks/themeColors';
import {
APPROXIMATE_SYMBOL,
CENTER,
COLORS,
SIZES,
USDB_TOKEN_ID,
} from '../../constants';
import {
HIDDEN_OPACITY,
INSET_WINDOW_WIDTH,
WINDOWWIDTH,
} from '../../constants/theme';
import { useGlobalThemeContext } from '../../../context-store/theme';
import { useGlobalInsets } from '../../../context-store/insetsProvider';
import { useNodeContext } from '../../../context-store/nodeContext';
import { useKeysContext } from '../../../context-store/keys';
import { useActiveCustodyAccount } from '../../../context-store/activeAccount';
import { useSparkWallet } from '../../../context-store/sparkContext';
import { useUserBalanceContext } from '../../../context-store/userBalanceContext';
import { useFlashnet } from '../../../context-store/flashnetContext';
import { useGlobalContextProvider } from '../../../context-store/context';
import fetchBackend from '../../../db/handleBackend';
import { sendSparkPayment, sendSparkTokens } from '../../functions/spark';
import { bulkUpdateSparkTransactions } from '../../functions/spark/transactions';
import { dollarsToSats, satsToDollars } from '../../functions/spark/flashnet';
import EmojiQuickBar from '../../functions/CustomElements/emojiBar';
import usePaymentInputDisplay from '../../hooks/usePaymentInputDisplay';
import convertTextInputValue from '../../functions/textInputConvertValue';
import SendTransactionFeeInfo from '../../components/admin/homeComponents/sendBitcoin/components/feeInfo';
import { formatStablecoinAmount } from '../../functions/sendBitcoin';
import { SliderProgressAnimation } from '../../functions/CustomElements/sendPaymentAnimation';
import { formatBalanceAmount } from '../../functions';
const QUOTE_TTL_MS = 115_000;
function truncateAddress(addr) {
if (!addr || addr.length <= 16) return addr || '';
return `${addr.slice(0, 8)}...${addr.slice(-6)}`;
}
function formatCountdown(ms) {
const secs = Math.max(0, Math.floor(ms / 1000));
const m = Math.floor(secs / 60);
const s = secs % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
}
export default function StablecoinSendScreen() {
const navigate = useNavigation();
const route = useRoute();
const { address, chain, chainLabel, asset, selectedPaymentMethod } =
route.params;
const { t } = useTranslation();
const { theme, darkModeType } = useGlobalThemeContext();
const { backgroundOffset, backgroundColor } = GetThemeColors();
const { bottomPadding } = useGlobalInsets();
const { fiatStats } = useNodeContext();
const { masterInfoObject } = useGlobalContextProvider();
const { contactsPrivateKey, publicKey } = useKeysContext();
const { currentWalletMnemoinc } = useActiveCustodyAccount();
const { sparkInformation } = useSparkWallet();
const { bitcoinBalance, dollarBalanceToken } = useUserBalanceContext();
const { swapUSDPriceDollars, poolInfoRef } = useFlashnet();
const [screenMode, setScreenMode] = useState('EDIT_AMOUNT'); // 'EDIT_AMOUNT' | 'CONFIRM_PAYMENT'
const [rawInput, setRawInput] = useState('');
const [inputDenomination, setInputDenomination] = useState('fiat');
console.log(rawInput);
const [description, setDescription] = useState('');
const [quote, setQuote] = useState(null);
const [quoteLoading, setQuoteLoading] = useState(false);
const [quoteError, setQuoteError] = useState(null);
const [countdown, setCountdown] = useState(null);
const [sending, setSending] = useState(false);
const [isAmountFocused, setIsAmountFocused] = useState(true);
const [retriggerQuoteFetch, setRetriggerQuoteFetch] = useState(0);
const debounceRef = useRef(null);
const countdownRef = useRef(null);
const quoteExpiresAt = useRef(null);
const isSendingPayment = useRef(null);
const progressAnimationRef = useRef(null);
const sourceMethod = selectedPaymentMethod || 'BTC';
useEffect(() => {
isSendingPayment.current = sending;
}, [sending]);
const {
primaryDisplay,
secondaryDisplay,
conversionFiatStats,
convertSatsToDisplay,
convertDisplayToSats,
getNextDenomination,
convertForToggle,
} = usePaymentInputDisplay({
paymentMode: sourceMethod,
inputDenomination,
fiatStats,
usdFiatStats: { coin: 'USD', value: swapUSDPriceDollars },
masterInfoObject,
isSendingPayment: isSendingPayment.current,
});
const convertedSendAmount = convertDisplayToSats(rawInput);
const clearCountdown = useCallback(() => {
if (countdownRef.current) clearInterval(countdownRef.current);
countdownRef.current = null;
setCountdown(null);
}, []);
const startCountdown = useCallback(
expiresAt => {
clearCountdown();
quoteExpiresAt.current = expiresAt;
countdownRef.current = setInterval(() => {
const remaining = quoteExpiresAt.current - Date.now();
if (remaining <= 0) {
clearInterval(countdownRef.current);
countdownRef.current = null;
setCountdown(null);
setQuote(null);
setRetriggerQuoteFetch(prev => prev + 1);
} else {
setCountdown(remaining);
}
}, 1000);
},
[clearCountdown, t],
);
const fetchQuote = useCallback(
async sats => {
if (sats <= 0 || !contactsPrivateKey || !publicKey) return;
setQuoteError(null);
setQuote(null);
clearCountdown();
try {
const apiSourceMethod = sourceMethod === 'BTC' ? 'spark' : 'usdb';
const result = await fetchBackend(
'createFlashnetStablecoinQuote',
{
recipientAddress: address,
destinationChain: chain,
destinationAsset: asset,
amountSats: sats,
sourceMethod: apiSourceMethod,
refundAddress: sparkInformation.sparkAddress,
},
contactsPrivateKey,
publicKey,
);
if (!result || result.error) {
throw new Error(
result?.error || t('wallet.stablecoinSend.quoteError'),
);
}
const expiresAt = result.expiresAt || Date.now() + QUOTE_TTL_MS;
const formattedFee =
sourceMethod === 'BTC'
? result.fee
: dollarsToSats(
result?.fee / Math.pow(10, 6),
poolInfoRef.currentPriceAInB,
);
setQuote({ ...result, fee: formattedFee, expiresAt });
startCountdown(expiresAt);
} catch (err) {
setQuoteError(err.message || t('wallet.stablecoinSend.quoteError'));
} finally {
setQuoteLoading(false);
}
},
[
address,
chain,
asset,
sourceMethod,
contactsPrivateKey,
publicKey,
clearCountdown,
startCountdown,
t,
],
);
useEffect(() => {
if (debounceRef.current) clearTimeout(debounceRef.current);
if (rawInput > 0) {
setQuoteLoading(true);
const fetchAmount =
sourceMethod === 'BTC'
? convertDisplayToSats(rawInput)
: rawInput * Math.pow(10, 6);
debounceRef.current = setTimeout(() => fetchQuote(fetchAmount), 800);
} else {
setQuoteLoading(false);
setQuote(null);
setQuoteError(null);
clearCountdown();
}
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [rawInput, sourceMethod, fetchQuote, clearCountdown, retriggerQuoteFetch]);
useEffect(() => {
return () => {
clearCountdown();
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, [clearCountdown]);
// Handle back press from CONFIRM_PAYMENT — return to EDIT_AMOUNT
useEffect(() => {
const unsubscribe = navigate.addListener('beforeRemove', e => {
if (screenMode !== 'CONFIRM_PAYMENT') return;
if (isSendingPayment.current) return;
e.preventDefault();
setScreenMode('EDIT_AMOUNT');
});
return unsubscribe;
}, [navigate, screenMode]);
const handleDenominationToggle = () => {
if (!isAmountFocused) return;
if (!convertedSendAmount) {
setInputDenomination(prev => (prev === 'fiat' ? 'sats' : 'fiat'));
return;
}
const nextDenom = getNextDenomination();
const convertedValue = convertForToggle(rawInput, convertTextInputValue);
setInputDenomination(nextDenom);
setRawInput(String(convertedValue));
};
const handleMethodToggle = () => {
navigate.navigate('CustomHalfModal', {
wantedContent: 'SelectPaymentMethod',
selectedPaymentMethod: sourceMethod,
fromPage: 'StablecoinSendScreen',
});
};
const handleSend = useCallback(async () => {
if (!quote || sending) return;
if (Date.now() >= quote.expiresAt) {
setQuoteError(t('wallet.stablecoinSend.quoteExpired'));
setQuote(null);
clearCountdown();
setScreenMode('EDIT_AMOUNT');
return;
}
setSending(true);
clearCountdown();
try {
let result;
if (sourceMethod === 'BTC') {
result = await sendSparkPayment({
receiverSparkAddress: quote.depositAddress,
amountSats: Number(quote.amountIn),
mnemonic: currentWalletMnemoinc,
});
} else {
// USDb: quote.amountIn is in token micro-units (e.g. 1_000_000 = $1)
result = await sendSparkTokens({
tokenIdentifier: USDB_TOKEN_ID,
tokenAmount: Number(quote.amountIn),
receiverSparkAddress: quote.depositAddress,
mnemonic: currentWalletMnemoinc,
});
}
if (!result.didWork) throw new Error(result.error);
const sparkTransferId =
sourceMethod === 'USD' ? result.response : result.response?.id;
const pendingTx = {
id: sparkTransferId,
paymentStatus: 'pending',
paymentType: 'spark',
accountId: sparkInformation.identityPubKey,
details: {
amount: quote.amountIn,
fee: quote?.fee,
totalFee: 0,
supportFee: 0,
description: description || `Send ${asset}`,
address: quote.depositAddress,
time: Date.now(),
createdAt: Date.now(),
direction: 'OUTGOING',
isFlashnetStablecoin: true,
quoteId: quote.quoteId,
destinationAddress: address,
destinationChain: chain,
destinationAsset: asset,
sourceMethod,
isLRC20Payment: sourceMethod === 'USD',
...(sourceMethod === 'USD' ? { LRC20Token: USDB_TOKEN_ID } : {}),
},
};
await bulkUpdateSparkTransactions([pendingTx], 'fullUpdate');
await fetchBackend(
'submitFlashnetStablecoinOrder',
{
quoteId: quote.quoteId,
sparkTxHash: sparkTransferId,
sourceSparkAddress: sparkInformation.sparkAddress,
},
contactsPrivateKey,
publicKey,
);
if (progressAnimationRef.current) {
progressAnimationRef.current.completeProgress();
await new Promise(res => setTimeout(res, 600));
}
requestAnimationFrame(() => {
requestAnimationFrame(() => {
navigate.reset({
index: 0,
routes: [
{
name: 'HomeAdmin',
params: {
screen: 'Home',
},
},
{
name: 'ConfirmTxPage',
params: {
transaction: pendingTx,
},
},
],
});
});
});
} catch (err) {
console.log(err, 'error navigating to bla bla bla');
navigate.navigate('ErrorScreen', { errorMessage: err.message });
}
}, [
quote,
sending,
sourceMethod,
navigate,
currentWalletMnemoinc,
sparkInformation,
description,
address,
chain,
asset,
contactsPrivateKey,
publicKey,
clearCountdown,
t,
]);
const handleEmoji = newDescription => {
setDescription(newDescription);
};
const isQuoteLoading =
(quoteLoading || (quote && countdown === null)) && !sending;
const canConfirm =
!!quote && !quoteLoading && !sending && convertedSendAmount > 0;
const handleReview = useCallback(() => {
if (!convertedSendAmount || convertedSendAmount <= 0) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.noAmount'),
});
return;
}
if (isQuoteLoading) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.quoteStillLoading'),
});
return;
}
if (!quote) {
navigate.navigate('ErrorScreen', {
errorMessage: t('wallet.stablecoinSend.noQuote'),
});
return;
}
const hasEnoughBalance =
sourceMethod === 'BTC'
? convertedSendAmount <= Number(bitcoinBalance)
: Number(rawInput) <= Number(dollarBalanceToken);
if (!hasEnoughBalance) {
navigate.navigate('ErrorScreen', {
errorMessage: t('screens.inAccount.swapsPage.insufficientBalance'),
});
return;
}
if (Date.now() >= quote.expiresAt) {
setQuoteError(t('wallet.stablecoinSend.quoteExpired'));
setQuote(null);
clearCountdown();
return;
}
setIsAmountFocused(true);
setScreenMode('CONFIRM_PAYMENT');
}, [
canConfirm,
quote,
clearCountdown,
t,
convertedSendAmount,
isQuoteLoading,
sourceMethod,
rawInput,
bitcoinBalance,
dollarBalanceToken,
navigate,
]);
const rowBg = backgroundOffset;
const memorizedKeyboardStyle = useMemo(() => {
return {
paddingBottom: !isAmountFocused ? 0 : bottomPadding,
};
}, [isAmountFocused]);
const isConfirmMode = screenMode === 'CONFIRM_PAYMENT';
return (
<CustomKeyboardAvoidingView globalThemeViewStyles={memorizedKeyboardStyle}>
<View style={styles.replacementContainer}>
<CustomSettingsTopBar label={`${t('constants.send')} ${asset}`} />
<ScrollView
showsVerticalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
keyboardShouldPersistTaps="handled"
>
{/* Amount display — tap to toggle denomination (edit mode only) */}
<TouchableOpacity
activeOpacity={1}
onPress={handleDenominationToggle}
style={CENTER}
disabled={!isAmountFocused || isConfirmMode}
>
<FormattedBalanceInput
maxWidth={0.9}
amountValue={rawInput}
inputDenomination={primaryDisplay.denomination}
forceCurrency={primaryDisplay.forceCurrency}
forceFiatStats={primaryDisplay.forceFiatStats}
activeOpacity={!convertedSendAmount ? 0.5 : 1}
/>
<FormattedSatText
neverHideBalance={true}
containerStyles={{
opacity: !convertedSendAmount ? HIDDEN_OPACITY : 1,
}}
styles={{ includeFontPadding: false, ...styles.satValue }}
globalBalanceDenomination={secondaryDisplay.denomination}
forceCurrency={secondaryDisplay.forceCurrency}
balance={convertedSendAmount}
forceFiatStats={secondaryDisplay.forceFiatStats}
/>
</TouchableOpacity>
{/* Confirm mode: fee info */}
{isConfirmMode && (
<SendTransactionFeeInfo
paymentFee={quote?.fee}
isLightningPayment={true}
isDecoding={isQuoteLoading}
/>
)}
{/* Confirm mode: destination info */}
{isConfirmMode && (
<TouchableOpacity
onPress={() =>
navigate.navigate('ErrorScreen', {
errorMessage: address,
})
}
style={[styles.destinationBox, { backgroundColor: rowBg }]}
>
<ThemeText
styles={styles.quoteValue}
content={`${truncateAddress(address)}`}
/>
</TouchableOpacity>
)}
</ScrollView>
{/* Source method picker: shown in edit mode only */}
{!isConfirmMode && (
<ChoosePaymentMethod
theme={theme}
darkModeType={darkModeType}
determinePaymentMethod={sourceMethod}
handleSelectPaymentMethod={handleMethodToggle}
bitcoinBalance={bitcoinBalance}
dollarBalanceToken={dollarBalanceToken}
masterInfoObject={masterInfoObject}
fiatStats={fiatStats}
uiState="EDIT_AMOUNT"
t={t}
containerStyles={{ marginTop: 5 }}
/>
)}
{/* Description input: edit mode only */}
{!isConfirmMode && (
<CustomSearchInput
onFocusFunction={() => setIsAmountFocused(false)}
onBlurFunction={() => setIsAmountFocused(true)}
placeholderText={t('constants.paymentDescriptionPlaceholder')}
setInputText={setDescription}
inputText={description}
textInputMultiline={true}
textAlignVertical="baseline"
maxLength={150}
containerStyles={{
width: INSET_WINDOW_WIDTH,
marginTop: 10,
maxWidth: 350,
...CENTER,
}}
/>
)}
{/* Quote summary */}
{convertedSendAmount > 0 && isAmountFocused && !sending && (
<View style={[styles.quoteBox, { backgroundColor: rowBg }]}>
{isQuoteLoading && (
<View style={styles.quoteLoadingRow}>
<ActivityIndicator
size="small"
color={
theme && darkModeType ? COLORS.darkModeText : COLORS.primary
}
/>
<ThemeText
styles={styles.quoteLoadingText}
content={t('wallet.stablecoinSend.gettingQuote')}
/>
</View>
)}
{!isQuoteLoading && quoteError && (
<ThemeText
styles={[
styles.quoteErrorText,
{
color:
theme && darkModeType
? COLORS.darkModeText
: COLORS.cancelRed,
},
]}
content={quoteError}
/>
)}
{!isQuoteLoading && quote && countdown !== null && (
<>
<View style={styles.quoteRow}>
<ThemeText
styles={styles.quoteLabel}
content={t('wallet.stablecoinSend.recipientGets')}
/>
<ThemeText
styles={styles.quoteValue}
content={`${APPROXIMATE_SYMBOL}${formatBalanceAmount(
formatStablecoinAmount(quote.estimatedOut),
false,
masterInfoObject,
)} ${asset}`}
/>
</View>
<View style={styles.quoteRow}>
<ThemeText
styles={styles.quoteLabel}
content={t('wallet.stablecoinSend.quoteExpiresIn')}
/>
<ThemeText
styles={[
styles.quoteValue,
countdown < 30000 && {
color:
theme && darkModeType
? COLORS.darkModeText
: COLORS.cancelRed,
},
]}
content={formatCountdown(countdown)}
/>
</View>
</>
)}
</View>
)}
{/* EDIT_AMOUNT: keyboard + Review button */}
{!isConfirmMode && isAmountFocused && (
<CustomNumberKeyboard
setInputValue={setRawInput}
showDot={inputDenomination === 'fiat'}
fiatStats={conversionFiatStats}
/>
)}
{!isConfirmMode && isAmountFocused && (
<CustomButton
textContent={t('constants.review')}
actionFunction={handleReview}
buttonStyles={{ ...CENTER }}
useLoading={isQuoteLoading}
/>
)}
{/* CONFIRM_PAYMENT: swipe button */}
{isConfirmMode && (
<View style={styles.buttonContainer}>
{sending ? (
<SliderProgressAnimation
ref={progressAnimationRef}
isVisible={true}
textColor={COLORS.darkModeText}
backgroundColor={
theme && darkModeType ? backgroundOffset : COLORS.primary
}
width={0.95}
/>
) : (
<SwipeButtonNew
onSwipeSuccess={handleSend}
width={0.85}
resetAfterSuccessAnimDuration={true}
shouldResetAfterSuccess={!sending}
containerStyles={{
opacity: canConfirm ? 1 : HIDDEN_OPACITY,
}}
thumbIconStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
railStyles={{
backgroundColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
borderColor:
theme && darkModeType ? backgroundOffset : backgroundColor,
}}
/>
)}
</View>
)}
</View>
{/* Emoji bar for description input */}
{!isAmountFocused && !isConfirmMode && (
<EmojiQuickBar description={description} onEmojiSelect={handleEmoji} />
)}
</CustomKeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
globalContainer: {
flex: 1,
},
replacementContainer: {
flexGrow: 1,
width: WINDOWWIDTH,
...CENTER,
},
scrollContent: {
flexGrow: 1,
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 10,
},
satValue: {
fontSize: SIZES.medium,
marginTop: 4,
},
destinationBox: {
width: '80%',
alignItems: 'center',
justifyContent: 'center',
padding: 8,
borderRadius: 8,
...CENTER,
marginTop: 30,
},
quoteBox: {
marginTop: 12,
marginHorizontal: 16,
padding: 12,
borderRadius: 8,
gap: 10,
},
quoteLoadingRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
quoteLoadingText: {
fontSize: SIZES.medium,
opacity: 0.65,
},
quoteErrorText: {
fontSize: SIZES.small,
},
quoteRow: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
quoteLabel: {
fontSize: SIZES.medium,
opacity: 0.65,
},
quoteValue: {
fontSize: SIZES.medium,
},
buttonContainer: {
width: '100%',
alignItems: 'center',
paddingVertical: 10,
},
});
@@ -10,52 +10,78 @@ import ThemeIcon from '../../functions/CustomElements/themeIcon';
import { INSET_WINDOW_WIDTH } from '../../constants/theme';
export default function TechnicalTransactionDetails(props) {
console.log('Transaction Detials Page');
console.log('Transaction Details Page');
const { showToast } = useToast();
const navigate = useNavigation();
const { t } = useTranslation();
const { transaction } = props.route.params;
const { details, sparkID } = transaction;
const isPending = transaction.paymentStatus === 'pending';
const paymentDetails =
transaction.paymentType === 'spark'
? details.isLRC20Payment
? [t('screens.inAccount.technicalTransactionDetails.txHash')]
: [t('screens.inAccount.technicalTransactionDetails.paymentId')]
: transaction.paymentType === 'lightning'
? [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
t('screens.inAccount.technicalTransactionDetails.preimage'),
t('screens.inAccount.technicalTransactionDetails.address'),
]
: [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
t('screens.inAccount.technicalTransactionDetails.bitcoinTxId'),
t('screens.inAccount.technicalTransactionDetails.address'),
t('screens.inAccount.technicalTransactionDetails.paymentExp'),
].slice(0, details.direction === 'OUTGOING' && isPending ? 4 : 3);
let paymentDetails = [];
let infoValues = [];
if (transaction.paymentType === 'spark') {
if (details.isFlashnetStablecoin) {
paymentDetails = [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
t('screens.inAccount.technicalTransactionDetails.quoteId'),
t('screens.inAccount.technicalTransactionDetails.destinationAddress'),
t('screens.inAccount.technicalTransactionDetails.destinationAsset'),
t('screens.inAccount.technicalTransactionDetails.destinationChain'),
];
infoValues = [
sparkID,
details.quoteId,
details.destinationAddress,
details.destinationAsset,
details.destinationChain,
];
} else if (details.isLRC20Payment) {
paymentDetails = [
t('screens.inAccount.technicalTransactionDetails.txHash'),
];
infoValues = [sparkID];
} else {
paymentDetails = [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
];
infoValues = [sparkID];
}
} else if (transaction.paymentType === 'lightning') {
paymentDetails = [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
t('screens.inAccount.technicalTransactionDetails.preimage'),
t('screens.inAccount.technicalTransactionDetails.address'),
];
infoValues = [sparkID, details.preimage, details.address];
} else {
// bitcoin
const bitcoinLabels = [
t('screens.inAccount.technicalTransactionDetails.paymentId'),
t('screens.inAccount.technicalTransactionDetails.bitcoinTxId'),
t('screens.inAccount.technicalTransactionDetails.address'),
t('screens.inAccount.technicalTransactionDetails.paymentExp'),
];
const bitcoinValues = [
sparkID,
details.onChainTxid,
details.address,
details.expiresAt || 'N/A',
];
const count = details.direction === 'OUTGOING' && isPending ? 4 : 3;
paymentDetails = bitcoinLabels.slice(0, count);
infoValues = bitcoinValues.slice(0, count);
}
const infoElements = paymentDetails.map((item, id) => {
const txItem =
transaction.paymentType === 'spark'
? sparkID
: transaction.paymentType === 'lightning'
? id === 0
? sparkID
: id === 1
? details.preimage
: details.address
: id === 0
? sparkID
: id === 1
? details.onChainTxid
: id === 2
? details.address
: details.expiresAt || 'N/A';
const txItem = infoValues[id];
const isBitcoinTxId =
transaction.paymentType === 'bitcoin' &&
item === t('screens.inAccount.technicalTransactionDetails.bitcoinTxId');
return (
<View key={id}>
@@ -79,11 +105,7 @@ export default function TechnicalTransactionDetails(props) {
</View>
<TouchableOpacity
onPress={() => {
if (
transaction.paymentType === 'bitcoin' &&
item ===
t('screens.inAccount.technicalTransactionDetails.bitcoinTxId')
) {
if (isBitcoinTxId) {
navigate.navigate('CustomWebView', {
webViewURL: `https://mempool.space/tx/${txItem}`,
});
@@ -110,20 +132,6 @@ export default function TechnicalTransactionDetails(props) {
style={styles.innerContainer}
>
{infoElements}
{/* {transaction.paymentType === 'spark' && (
<ThemeText
content={t(
'screens.inAccount.technicalTransactionDetails.privacyMessage',
{
type: t(
`screens.inAccount.technicalTransactionDetails.${
details?.direction === 'OUTGOING' ? 'receivers' : 'senders'
}`,
),
},
)}
/>
)} */}
</ScrollView>
</GlobalThemeView>
);
+8
View File
@@ -697,6 +697,8 @@ const SparkWalletProvider = ({ children }) => {
sparkInfoRef.current.identityPubKey,
sendWebViewRequest,
true,
contactsPrivateKey,
publicKey,
);
const didUpdateStatus = updated.find(
tx =>
@@ -1216,6 +1218,9 @@ const SparkWalletProvider = ({ children }) => {
currentMnemonicRef.current,
sparkInfoRef.current.identityPubKey,
sendWebViewRequest,
false,
contactsPrivateKey,
publicKey,
);
if (updatePendingPaymentsIntervalRef.current) {
@@ -1255,6 +1260,9 @@ const SparkWalletProvider = ({ children }) => {
currentMnemonicRef.current,
sparkInfoRef.current.identityPubKey,
sendWebViewRequest,
false,
contactsPrivateKey,
publicKey,
);
if (response.shouldCheck) {
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Konten",
"to": "An",
"from": "Von",
"asset": "Asset",
"chain": "Kette",
"description": "Beschreibung",
"regenerate": "Erneut generieren",
"restore": "Wiederherstellen",
@@ -1161,6 +1163,33 @@
"paymentsCounter": "{{number}} Zahlungen",
"paymentsCounterTracker": "{{number}} von {{total}}",
"loadingMessage": "Gespeicherte Transaktionen werden geladen..."
},
"stablecoinSend": {
"sendStablecoins": "Stablecoins senden",
"selectChain": "Netzwerk auswählen",
"network": "Netzwerk",
"selectAsset": "Asset auswählen",
"recipient": "Empfänger",
"gettingQuote": "Angebot wird abgerufen…",
"quoteExpired": "Angebot abgelaufen. Bitte versuche es erneut.",
"quoteError": "Angebot konnte nicht abgerufen werden. Bitte versuche es erneut.",
"recipientGets": "Empfänger erhält",
"youSend": "Du sendest",
"quoteExpiresIn": "Angebot läuft ab in",
"payWithLightning": "Mit Lightning bezahlen",
"payWithUSDb": "Mit USDb bezahlen",
"chainNotSupportedForAsset": "Nicht verfügbar in diesem Netzwerk",
"swapInProgress": "Tausch läuft",
"swapInProgressDescription": "Deine Mittel wurden gesendet. Der Stablecoin-Tausch wird verarbeitet und kann einige Minuten dauern.",
"noAmount": "Bitte gib einen Betrag zum Senden ein.",
"quoteStillLoading": "Angebot wird abgerufen, bitte einen Moment warten.",
"noQuote": "Angebot konnte nicht abgerufen werden. Bitte versuche es erneut.",
"swapStatus": "Tauschstatus",
"chainSwap": "Chain-Tausch",
"assetPicker": {
"header": "Wähle die Währung aus, die du senden möchtest.",
"notAvailable": "Nicht verfügbar auf {{chain}}"
}
}
},
"contacts": {
@@ -1843,7 +1872,8 @@
"contactPaymentType": "Kontakt",
"converstionRate": "{{satAmount}} pro {{USDAmount}}",
"gift": "Geschenk",
"claimPayment": "Zahlung einfordern"
"claimPayment": "Zahlung einfordern",
"chainSwap": "Chain-Tausch"
},
"explorePage": {
"timeLeft": "({{time}} verbleibend)",
@@ -1944,7 +1974,11 @@
"noInformationLabel": "{{item}} wird nicht angezeigt, da diese Zahlung aus dem Verlauf wiederhergestellt wurde oder eine Rechnung mit Nullbetrag verwendet wurde.",
"privacyMessage": "Um die Privatsphäre des {{type}} zu wahren, sind alle weiteren Informationen ausgeblendet.",
"receivers": "Empfänger",
"senders": "Absender"
"senders": "Absender",
"quoteId": "Angebots-ID",
"destinationAddress": "Zieladresse",
"destinationAsset": "Ziel-Asset",
"destinationChain": "Ziel-Kette"
},
"viewAllTxPage": {
"title": "Transaktionen",
+36 -2
View File
@@ -81,6 +81,8 @@
"accounts": "Accounts",
"to": "To",
"from": "From",
"asset": "Asset",
"chain": "Chain",
"description": "Description",
"regenerate": "Regenerate",
"restore": "Restore",
@@ -1160,6 +1162,33 @@
"paymentsCounter": "{{number}} payments",
"paymentsCounterTracker": "{{number}} of {{total}}",
"loadingMessage": "Loading saved transactions"
},
"stablecoinSend": {
"sendStablecoins": "Send Stablecoins",
"selectChain": "Select Network",
"network": "Network",
"selectAsset": "Select Asset",
"recipient": "Recipient",
"gettingQuote": "Getting quote…",
"quoteExpired": "Quote expired. Please try again.",
"quoteError": "Failed to get quote. Please try again.",
"recipientGets": "Recipient gets",
"youSend": "You send",
"quoteExpiresIn": "Quote expires in",
"payWithLightning": "Pay with Lightning",
"payWithUSDb": "Pay with USDb",
"chainNotSupportedForAsset": "Not available on this network",
"swapInProgress": "Swap in Progress",
"swapInProgressDescription": "Your funds have been sent. The stablecoin swap is processing and may take a few minutes.",
"noAmount": "Please enter an amount to send.",
"quoteStillLoading": "Getting a quote, please wait a moment.",
"noQuote": "Could not get a quote. Please try again.",
"swapStatus": "Swap Status",
"chainSwap": "Chain Swap",
"assetPicker": {
"header": "Select the currency you want to send.",
"notAvailable": "Not available on {{chain}}"
}
}
},
"contacts": {
@@ -1842,7 +1871,8 @@
"contactPaymentType": "Contact",
"converstionRate": "{{satAmount}} per {{dollarAmount}}",
"gift": "Gift",
"claimPayment": "Claim payment"
"claimPayment": "Claim payment",
"chainSwap": "Chain Swap"
},
"explorePage": {
"timeLeft": "({{time}} left)",
@@ -1944,7 +1974,11 @@
"noInformationLabel": "{{item}} is not shown since this payment was restored from history or used a zero amount invoice.",
"privacyMessage": "To preserve the {{type}} privacy, all other information is hidden.",
"receivers": "receiver's",
"senders": "sender's"
"senders": "sender's",
"quoteId": "Quote ID",
"destinationAddress": "Destination Address",
"destinationAsset": "Destination Asset",
"destinationChain": "Destination Chain"
},
"viewAllTxPage": {
"title": "Transactions",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Cuentas",
"to": "A",
"from": "De",
"asset": "Activo",
"chain": "Cadena",
"description": "Descripción",
"regenerate": "Regenerar",
"restore": "Restaurar",
@@ -1020,6 +1022,33 @@
"paymentsCounter": "{{number}} pagos",
"paymentsCounterTracker": "{{number}} de {{total}}",
"loadingMessage": "Cargando transacciones guardadas"
},
"stablecoinSend": {
"sendStablecoins": "Enviar Stablecoins",
"selectChain": "Seleccionar Red",
"network": "Red",
"selectAsset": "Seleccionar Activo",
"recipient": "Destinatario",
"gettingQuote": "Obteniendo cotización…",
"quoteExpired": "Cotización vencida. Por favor, inténtalo de nuevo.",
"quoteError": "No se pudo obtener la cotización. Por favor, inténtalo de nuevo.",
"recipientGets": "El destinatario recibe",
"youSend": "Tú envías",
"quoteExpiresIn": "La cotización vence en",
"payWithLightning": "Pagar con Lightning",
"payWithUSDb": "Pagar con USDb",
"chainNotSupportedForAsset": "No disponible en esta red",
"swapInProgress": "Intercambio en Proceso",
"swapInProgressDescription": "Tus fondos han sido enviados. El intercambio de stablecoin está procesándose y puede tardar unos minutos.",
"noAmount": "Por favor, ingresa un monto para enviar.",
"quoteStillLoading": "Obteniendo cotización, por favor espera un momento.",
"noQuote": "No se pudo obtener una cotización. Por favor, inténtalo de nuevo.",
"swapStatus": "Estado del Intercambio",
"chainSwap": "Intercambio en Cadena",
"assetPicker": {
"header": "Selecciona la moneda que deseas enviar.",
"notAvailable": "No disponible en {{chain}}"
}
}
},
"contacts": {
@@ -1703,7 +1732,8 @@
"contactPaymentType": "Contacto",
"converstionRate": "{{satAmount}} por {{dollarAmount}}",
"gift": "Regalo",
"claimPayment": "Reclamar pago"
"claimPayment": "Reclamar pago",
"chainSwap": "Intercambio en Cadena"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
@@ -1804,7 +1834,11 @@
"noInformationLabel": "{{item}} no se muestra ya que este pago fue restaurado desde el historial o usó una factura de monto cero.",
"privacyMessage": "Para preservar la privacidad {{type}}, toda la demás información se oculta.",
"receivers": "del receptor",
"senders": "del remitente"
"senders": "del remitente",
"quoteId": "ID de Cotización",
"destinationAddress": "Dirección de Destino",
"destinationAsset": "Activo de Destino",
"destinationChain": "Cadena de Destino"
},
"viewAllTxPage": {
"title": "Transacciones",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Comptes",
"to": "Pour",
"from": "De",
"asset": "Actif",
"chain": "Chaîne",
"description": "Description",
"regenerate": "Régénérer",
"restore": "Restaurer",
@@ -1159,6 +1161,33 @@
"paymentsCounter": "{{number}} paiements",
"paymentsCounterTracker": "{{number}} de {{total}}",
"loadingMessage": "Chargement des transactions enregistrées"
},
"stablecoinSend": {
"sendStablecoins": "Envoyer des Stablecoins",
"selectChain": "Sélectionner le Réseau",
"network": "Réseau",
"selectAsset": "Sélectionner l'Actif",
"recipient": "Destinataire",
"gettingQuote": "Obtention du devis…",
"quoteExpired": "Devis expiré. Veuillez réessayer.",
"quoteError": "Impossible d'obtenir un devis. Veuillez réessayer.",
"recipientGets": "Le destinataire reçoit",
"youSend": "Vous envoyez",
"quoteExpiresIn": "Le devis expire dans",
"payWithLightning": "Payer avec Lightning",
"payWithUSDb": "Payer avec USDb",
"chainNotSupportedForAsset": "Non disponible sur ce réseau",
"swapInProgress": "Échange en Cours",
"swapInProgressDescription": "Vos fonds ont été envoyés. L'échange de stablecoin est en cours de traitement et peut prendre quelques minutes.",
"noAmount": "Veuillez saisir un montant à envoyer.",
"quoteStillLoading": "Obtention d'un devis, veuillez patienter un instant.",
"noQuote": "Impossible d'obtenir un devis. Veuillez réessayer.",
"swapStatus": "Statut de l'Échange",
"chainSwap": "Échange de Chaîne",
"assetPicker": {
"header": "Sélectionnez la devise que vous souhaitez envoyer.",
"notAvailable": "Non disponible sur {{chain}}"
}
}
},
"contacts": {
@@ -1841,7 +1870,8 @@
"contactPaymentType": "Contact",
"converstionRate": "{{satAmount}} par {{dollarAmount}}",
"gift": "Cadeau",
"claimPayment": "Réclamer le paiement"
"claimPayment": "Réclamer le paiement",
"chainSwap": "Échange de Chaîne"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
@@ -1942,7 +1972,11 @@
"noInformationLabel": "{{item}} n'apparaît pas car ce paiement a été restauré à partir de l'historique ou a utilisé une facture d'un montant nul.",
"privacyMessage": "Pour préserver la vie privée de {{type}}, toutes les autres informations sont cachées.",
"receivers": "du destinataire",
"senders": "de l'expéditeur"
"senders": "de l'expéditeur",
"quoteId": "ID du Devis",
"destinationAddress": "Adresse de Destination",
"destinationAsset": "Actif de Destination",
"destinationChain": "Chaîne de Destination"
},
"viewAllTxPage": {
"title": "Transactions",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Accounts",
"to": "A",
"from": "Da",
"asset": "Asset",
"chain": "Catena",
"description": "Descrizione",
"regenerate": "Rigenera",
"restore": "Ripristina",
@@ -1159,6 +1161,33 @@
"paymentsCounter": "{{number}} pagamenti",
"paymentsCounterTracker": "{{number}} di {{total}}",
"loadingMessage": "Caricamento delle transazioni salvate"
},
"stablecoinSend": {
"sendStablecoins": "Invia Stablecoin",
"selectChain": "Seleziona Rete",
"network": "Rete",
"selectAsset": "Seleziona Asset",
"recipient": "Destinatario",
"gettingQuote": "Ottenendo quotazione…",
"quoteExpired": "Quotazione scaduta. Riprova.",
"quoteError": "Impossibile ottenere la quotazione. Riprova.",
"recipientGets": "Il destinatario riceve",
"youSend": "Tu invii",
"quoteExpiresIn": "La quotazione scade tra",
"payWithLightning": "Paga con Lightning",
"payWithUSDb": "Paga con USDb",
"chainNotSupportedForAsset": "Non disponibile su questa rete",
"swapInProgress": "Scambio in Corso",
"swapInProgressDescription": "I tuoi fondi sono stati inviati. Lo scambio di stablecoin è in elaborazione e potrebbe richiedere alcuni minuti.",
"noAmount": "Inserisci un importo da inviare.",
"quoteStillLoading": "Ottenendo una quotazione, attendi un momento.",
"noQuote": "Impossibile ottenere una quotazione. Riprova.",
"swapStatus": "Stato Scambio",
"chainSwap": "Chain Swap",
"assetPicker": {
"header": "Seleziona la valuta che vuoi inviare.",
"notAvailable": "Non disponibile su {{chain}}"
}
}
},
"contacts": {
@@ -1841,7 +1870,8 @@
"converstionRate": "{{satAmount}} per {{dollarAmount}}",
"confirmMessage_sent": "Importo inviato",
"gift": "Regalo",
"claimPayment": "Richiedi pagamento"
"claimPayment": "Richiedi pagamento",
"chainSwap": "Chain Swap"
},
"explorePage": {
"timeLeft": "({{time}} rimanenti)",
@@ -1942,7 +1972,11 @@
"noInformationLabel": "{{item}} non è mostrato poiché questo pagamento è stato ripristinato dalla cronologia o ha utilizzato una fattura con importo zero.",
"privacyMessage": "Per preservare la privacy {{type}}, tutte le altre informazioni sono nascoste.",
"receivers": "del destinatario",
"senders": "del mittente"
"senders": "del mittente",
"quoteId": "ID Quotazione",
"destinationAddress": "Indirizzo di Destinazione",
"destinationAsset": "Asset di Destinazione",
"destinationChain": "Catena di Destinazione"
},
"viewAllTxPage": {
"title": "Transazioni",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Contas",
"to": "Para",
"from": "De",
"asset": "Ativo",
"chain": "Rede",
"description": "Descrição",
"regenerate": "Gerar",
"restore": "Restaurar",
@@ -1159,6 +1161,33 @@
"paymentsCounter": "{{number}} pagamentos",
"paymentsCounterTracker": "{{number}} do {{total}}",
"loadingMessage": "Carregando transações salvas"
},
"stablecoinSend": {
"sendStablecoins": "Enviar dólar digital",
"selectChain": "Selecionar rede",
"network": "Rede",
"selectAsset": "Selecionar ativo",
"recipient": "Destinatário",
"gettingQuote": "Obtendo cotação…",
"quoteExpired": "Cotação expirada. Por favor, tente novamente.",
"quoteError": "Falha ao obter cotação. Por favor, tente novamente.",
"recipientGets": "O destinatário recebe",
"youSend": "Você envia",
"quoteExpiresIn": "Cotação expira em",
"payWithLightning": "Pagar com Bitcoin",
"payWithUSDb": "Pagar com USDB",
"chainNotSupportedForAsset": "Não disponível nesta rede",
"swapInProgress": "Troca em Andamento",
"swapInProgressDescription": "Seus fundos foram enviados. A troca de dólar digital está sendo processada e pode levar alguns minutos.",
"noAmount": "Por favor, insira um valor para enviar.",
"quoteStillLoading": "Obtendo cotação, aguarde um momento.",
"noQuote": "Não foi possível obter uma cotação. Por favor, tente novamente.",
"swapStatus": "Status da troca",
"chainSwap": "Troca entre redes",
"assetPicker": {
"header": "Selecione a moeda que deseja enviar.",
"notAvailable": "Não disponível em {{chain}}"
}
}
},
"contacts": {
@@ -1841,7 +1870,8 @@
"contactPaymentType": "Contato",
"converstionRate": "{{satAmount}} por {{dollarAmount}}",
"gift": "Vale-Blitz",
"claimPayment": "Reivindicar pagamento"
"claimPayment": "Reivindicar pagamento",
"chainSwap": "Troca em Cadeia"
},
"explorePage": {
"timeLeft": "({{time}} restantes)",
@@ -1942,7 +1972,11 @@
"noInformationLabel": "{{item}} não é mostrado porque este pagamento foi restaurado do histórico ou usou uma fatura de valor zero.",
"privacyMessage": "Para preservar a privacidade {{type}} ,todas as outras informações ficam ocultas.",
"receivers": "do recebedor",
"senders": "do pagador"
"senders": "do pagador",
"quoteId": "ID da Cotação",
"destinationAddress": "Endereço de Destino",
"destinationAsset": "Ativo de Destino",
"destinationChain": "Rede de Destino"
},
"viewAllTxPage": {
"title": "Transações",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Счета",
"to": "Куда",
"from": "Откуда",
"asset": "Актив",
"chain": "Сеть",
"description": "Описание",
"regenerate": "Сгенерировать снова",
"restore": "Восстановить",
@@ -1160,6 +1162,33 @@
"paymentsCounter": "{{number}} платежей",
"paymentsCounterTracker": "{{number}} из {{total}}",
"loadingMessage": "Загрузка транзакций"
},
"stablecoinSend": {
"sendStablecoins": "Отправить стейблкоины",
"selectChain": "Выбрать сеть",
"network": "Сеть",
"selectAsset": "Выбрать актив",
"recipient": "Получатель",
"gettingQuote": "Получение котировки…",
"quoteExpired": "Срок котировки истёк. Попробуйте снова.",
"quoteError": "Не удалось получить котировку. Попробуйте снова.",
"recipientGets": "Получатель получает",
"youSend": "Вы отправляете",
"quoteExpiresIn": "Котировка истекает через",
"payWithLightning": "Оплатить через Lightning",
"payWithUSDb": "Оплатить через USDb",
"chainNotSupportedForAsset": "Недоступно в этой сети",
"swapInProgress": "Обмен выполняется",
"swapInProgressDescription": "Ваши средства отправлены. Обмен стейблкоина обрабатывается и может занять несколько минут.",
"noAmount": "Пожалуйста, введите сумму для отправки.",
"quoteStillLoading": "Получение котировки, подождите немного.",
"noQuote": "Не удалось получить котировку. Попробуйте снова.",
"swapStatus": "Статус обмена",
"chainSwap": "Обмен в сети",
"assetPicker": {
"header": "Выберите валюту для отправки.",
"notAvailable": "Недоступно в сети {{chain}}"
}
}
},
"contacts": {
@@ -1842,7 +1871,8 @@
"contactPaymentType": "Контакт",
"converstionRate": "{{satAmount}} за {{dollarAmount}}",
"gift": "Подарок",
"claimPayment": "Получить платёж"
"claimPayment": "Получить платёж",
"chainSwap": "Обмен в сети"
},
"explorePage": {
"timeLeft": "(осталось {{time}})",
@@ -1943,7 +1973,11 @@
"noInformationLabel": "{{item}} не показан (платеж восстановлен из истории или нулевой инвойс).",
"privacyMessage": "Для сохранения приватности {{type}}, остальная информация скрыта.",
"receivers": "получателя",
"senders": "отправителя"
"senders": "отправителя",
"quoteId": "ID котировки",
"destinationAddress": "Адрес назначения",
"destinationAsset": "Актив назначения",
"destinationChain": "Сеть назначения"
},
"viewAllTxPage": {
"title": "Транзакции",
+36 -2
View File
@@ -82,6 +82,8 @@
"accounts": "Konton",
"to": "Till",
"from": "Från",
"asset": "Tillgång",
"chain": "Kedja",
"description": "Beskrivning",
"regenerate": "Regenerera",
"restore": "Återställ",
@@ -1159,6 +1161,33 @@
"paymentsCounter": "{{number}} Betalningar",
"paymentsCounterTracker": "{{number}} av {{total}}",
"loadingMessage": "Läser in sparade transaktioner"
},
"stablecoinSend": {
"sendStablecoins": "Skicka Stablecoins",
"selectChain": "Välj Nätverk",
"network": "Nätverk",
"selectAsset": "Välj Tillgång",
"recipient": "Mottagare",
"gettingQuote": "Hämtar offert…",
"quoteExpired": "Offerten har gått ut. Försök igen.",
"quoteError": "Det gick inte att hämta en offert. Försök igen.",
"recipientGets": "Mottagaren får",
"youSend": "Du skickar",
"quoteExpiresIn": "Offerten löper ut om",
"payWithLightning": "Betala med Lightning",
"payWithUSDb": "Betala med USDb",
"chainNotSupportedForAsset": "Inte tillgängligt i detta nätverk",
"swapInProgress": "Byte pågår",
"swapInProgressDescription": "Dina pengar har skickats. Stablecoin-bytet bearbetas och kan ta några minuter.",
"noAmount": "Ange ett belopp att skicka.",
"quoteStillLoading": "Hämtar offert, vänta ett ögonblick.",
"noQuote": "Det gick inte att hämta en offert. Försök igen.",
"swapStatus": "Bytestatus",
"chainSwap": "Kedjeswap",
"assetPicker": {
"header": "Välj den valuta du vill skicka.",
"notAvailable": "Inte tillgängligt på {{chain}}"
}
}
},
"contacts": {
@@ -1841,7 +1870,8 @@
"contactPaymentType": "Kontakt",
"converstionRate": "{{satAmount}} per {{dollarAmount}}",
"gift": "Gåva",
"claimPayment": "Ta emot betalning"
"claimPayment": "Ta emot betalning",
"chainSwap": "Kedjeswap"
},
"explorePage": {
"timeLeft": "({{time}} kvar)",
@@ -1942,7 +1972,11 @@
"noInformationLabel": "{{item}} visas inte eftersom denna betalning återställdes från historiken eller använde en faktura med nollbelopp.",
"privacyMessage": "För att bevara {{type}}:s integritet är all annan information dold.",
"receivers": "mottagarens",
"senders": "avsändarens"
"senders": "avsändarens",
"quoteId": "Offert-ID",
"destinationAddress": "Destinationsadress",
"destinationAsset": "Destinations-tillgång",
"destinationChain": "Destinations-kedja"
},
"viewAllTxPage": {
"title": "Transaktioner",
+11
View File
@@ -101,6 +101,8 @@ import {
import ConversionHistory from '../app/components/admin/homeComponents/swaps/swapHistory';
import GiftsPageHome from '../app/screens/inAccount/giftsHome';
import ReclaimGift from '../app/components/admin/homeComponents/gifts/reclaimGift';
import SelectStablecoinParamsScreen from '../app/screens/inAccount/selectStablecoinParamsScreen';
import StablecoinSendScreen from '../app/screens/inAccount/stablecoinSendScreen';
const SLIDE_FROM_BOTTOM_SCREENS = [
{ name: 'CustomWebView', component: CustomWebView },
@@ -202,6 +204,15 @@ const SLIDE_FROM_RIGHT_SCREENS = [
{ name: 'SavingsGoalDetails', component: SavingsGoalDetails },
{ name: 'GiftsPageHome', component: GiftsPageHome },
{ name: 'ReclaimGift', component: ReclaimGift },
{
name: 'SelectStablecoinParamsScreen',
component: SelectStablecoinParamsScreen,
},
{
name: 'StablecoinSendScreen',
component: StablecoinSendScreen,
options: { gestureEnabled: false },
},
];
const FADE_SCREENS = [